Sunday, May 9, 2021

Tcontrol_PWM Python Program on a Raspberry Pi

Here's a video of one of my home projects:

Note: to view it in high resolution, after starting it, click on the YouTube logo to watch it on a YouTube page.


In this video, we controlled the surface temperature of a 3-watt wirewound resistor by employing a feedback control loop that nudges the current through the resistor up and down while continuously reading a temperature sensor glued to the surface of the resistor. The current is supplied by the 9-volt battery shown through a Darlington-pair bipolar junction transistor (BJT) acting as a high-speed switch. The on/off state of the BJT is set by a pulse width modulation (PWM) signal from the 3.3 volt GPIO18 pin of the Raspberry Pi computer. The pulses are output at 100 Hz, and the duty cycle of the pulse signal controls the effective average current through the resistor.


Please see my previously posted "Thermistors" article for further information on the temperature sensing hardware, Python program libraries, and interactive UI on the smartphone. The PWM signal is implemented using the gpiozero library that ships with the standard Raspberry Pi Python installation.

Here's the Python code:


# TCONTROL_PWM PROGRAM FOR CONTROL OF TEMPERATURE
# Version 0.5, 08-May-2021
# Copyright © 2021 Joe Czapski
# Contact: xejgyczlionautomeasuretigercom replace lion with
# an at sign and tiger with a dot.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This program was written to run on a Raspberry Pi 4B computer running
# Raspbian Linux, with a Pi-16ADC voltage-measuring HAT board screwed on top,
# and with resistors and capacitors soldered onto the HAT to form the
# temperature sensor circuit. The program commands and reads from the LTC2497
# ADC chip on the HAT via the I2C communication bus.
# The heater is a high-wattage resistor, and the heat output is controlled
# by varying the current through the resistor using a pulse width modulation
# (PWM) signal from the GPIO18 pin to a transistor switch.
#
# This program uses a UI library and live-updating web-server engine
# called REMI (https://github.com/dddomodossola/remi).
# The program's structure is based on usage examples from the
# REMI codebase. The strip chart is drawn using the Matplotlib library
# (https://matplotlib.org).

# Comments that begin with [DEV] describe potential improvements that can be made.

import math
import threading
from smbus import SMBus
import io
import time
from time import sleep
import remi
from remi.gui import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
from gpiozero import PWMLED

#SET CONSTANTS AND INITIALIZE HARDWARE
#Nsensors is the number of temperature sensors to poll continuously.
#The ADC can poll up to 16 sensors. This application uses just one sensor.
#Note: there are architecture elements in the code for polling 3 sensors
#because the program and hardware employed 3 sensors in an earlier version.
Nsensors = 1   #number of temperature sensors to poll continuously
#Strip chart plots include: 1) T Measured, 2) T Setpoint, 3) Heater Power.
Nbuffers = 3   #number of plot lines for the strip chart
i2cbus = SMBus(1)   #initialize connection to the I2C bus
i2caddr = 0x76   #ADC board jumper positions
adc_ch = [0xB0, 0xB8, 0xB1]   #ADC voltage input channel addresses
#Reference resistors are metal-film resistors with TC<0.5ohm/degC.
Rref = [4976.4, 5003.9, 4994.8]   #values in ohms
vplus = 5.0   #for best accuracy, use nominal value of supply +5V to the ADC board, not actual value
max_counts = 16777215.0   #2^24 - 1
readlenbytes = 6   #I2C bus read data block length
buswait = 0.17   #seconds to wait between I2C actions (effectively the shortest polling interval possible)
update_interval_sec = 0.33   #web page redraw interval for REMI's server (also sets button-click response time)
xspan = 60.0   #strip chart X axis span in seconds
buffer_length = 500   #must be more than xspan/buswait
yAuto = False   #autorange the Y axis?
ymin = 15.0; ymax = 55.0   #initial Y axis range in degrees C (fixed range if yAuto is False)
y2min = 0.0; y2max = 2.0   #Y2 axis range, fixed, which is the right-side scale for Heater Power
plotColors = ['black', 'red', 'blue']
plotNames = ['T Measured', 'T Setpoint', 'H Power']   #displayed in the legend inset
#Steinhart–Hart equation coefficients for TDK NTC Thermistor B57863S0103F040:
coeffA = 1.12532e-3; coeffB = 2.34873e-4; coeffC = 8.59509e-8
heater = PWMLED(18, frequency=100)   #initialize GPIO18 as a 100 Hz pulse source
heater.value = 0.0   #start with heater off
hmax = 0.825   #maximum PWM duty cycle allowed, 0.0 to 1.0 (sets maximum heater watts)
vce_drop = 0.8   #voltage drop across the BJT when on in saturation
#Voltage measurement on a mid-life 9V Duracell varied from 8.4V at 15mA load current,
#to 7.7V at 200mA load current.
vhpower = 8.0   #typical voltage from 9V battery
vheater = vhpower - vce_drop
rheater = 33.0   #heater resistor ohms
Tctrlband = 12.0   #outside of this +/- band, make heater fully on or fully off
#These gain values set the behavior of the feedback control when within the control band:
gainI = -0.007 * update_interval_sec   #incremental gain
gainD = -0.025 / update_interval_sec   #differential gain

#FUNCTION CtoF() converts Celcius to Fahrenheit
#Note: this version of the program only uses degrees Celcius.
def CtoF(degrees):
    return 1.8 * degrees + 32.0
#FUNCTION adc_channel_init() starts measuring on the specified channel on the ADC chip.
#Call this function once to initialize on the first channel of the round-robin.
def adc_channel_init(first_ch_index):
    i2cbus.write_byte(i2caddr, adc_ch[first_ch_index])

#FUNCTION read_temperature() reads a channel's ADC counts via the I2C bus
#and then calculates voltage then temperature. This function also sets
#the next channel address to start measuring on.
def read_temperature(ch_index, next_ch_index):
    readarray = i2cbus.read_i2c_block_data(i2caddr, adc_ch[next_ch_index], readlenbytes)
    counts = readarray[0]*65536.0 + readarray[1]*256.0 + readarray[2]
    volts = vplus * counts / max_counts
    Rt = volts * Rref[ch_index] / (vplus - volts)  #calculate thermistor resistance
    lnR = math.log(Rt)
    invT = coeffA + coeffB*lnR + coeffC*lnR*lnR*lnR  #Steinhart–Hart equation
    T = 1.0/invT - 273.0   #convert inverse Kelvin to Celcius
    return T

#CLASS CircBuff
#This class implements a continuous wrap-around buffer to hold and serve out
#the temperature and time data for the strip chart.
#Two instances of CircBuff are spawned for each strip chart plot needed.
#If for example Nbuffers = 3, then 6 CircBuff instances are spawned.
#[DEV] Within this class, need to implement locking of all methods per instance, to prevent
#issues arising from multiple threads simultaneously acting on a buffer.
class CircBuff:
    def __init__(self, buflen, useFilter=False):
        self.buflen = buflen
        self.buffer = [0.0] * self.buflen
        self.ptr = 0
        self.full = False
        self.useFilter = useFilter
        self.filtsum = 0.0
        
    def add(self, value):
        if self.useFilter:
            if self.size() == 0:
                newval = value
            else:
                newval = 0.35*value + 0.65*self.filtsum
            self.filtsum = newval
        else:
            newval = value
        self.buffer[self.ptr] = newval
        self.ptr += 1
        if self.ptr >= self.buflen:
            self.ptr = 0
            self.full = True
            
    def size(self):
        if self.full:
            return self.buflen
        else:
            return self.ptr
        
    def read(self, count=-1):
        s = self.size()
        p = self.ptr
        b = self.buffer.copy()  #grab snapshot of buffer to ignore colliding changes
        if self.full:
            if p == 0:
                rlist = b
            else:
                rlist = b[p:] + b[0: p]
        else:
            rlist = b[0: p]
        if count >= 0 and count < self.buflen:
            return rlist[max(s - count, 0):]
        else:
            return rlist
        
    def readtimespan(self, tspan):
        tlist = self.read()
        tstart = tlist[-1] - tspan
        i = -1
        for t in tlist:
            i += 1
            if t > tstart:
                break
        return tlist[i:]
        
    def readlast(self):
        if self.ptr == 0:
            return self.buffer[self.buflen - 1]
        else:
            return self.buffer[self.ptr - 1]
        
    def clear(self):
        self.ptr = 0
        self.full = False

#CLASS MatplotImage
#This class is taken intact from REMI example programs for drawing graphs.
#It contains the interface functions for displaying a Matplotlib graph image
#onto the web page. This latest version is flicker-free.
class MatplotImage(Image):
    ax = None
    app_instance = None
    
    def __init__(self, **kwargs):
        super(MatplotImage, self).__init__("/%s/get_image_data?index=0" % str(id(self)), **kwargs)
        self._fig = Figure(figsize=(8, 4))
        self.ax = self._fig.add_subplot(111)

    def search_app_instance(self, node):
        if issubclass(node.__class__, remi.server.App):
            return node
        if not hasattr(node, "get_parent"):
            return None
        return self.search_app_instance(node.get_parent()) 

    def update(self, *args):
        if self.app_instance==None:
            self.app_instance = self.search_app_instance(self)
            if self.app_instance==None:
                return
        self.app_instance.execute_javascript("""
            url = '/%(id)s/get_image_data?index=%(frame_index)s';
            xhr = null;
            xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'blob'
            xhr.onload = function(e){
                urlCreator = window.URL || window.webkitURL;
                urlCreator.revokeObjectURL(document.getElementById('%(id)s').src);
                imageUrl = urlCreator.createObjectURL(this.response);
                document.getElementById('%(id)s').src = imageUrl;
            }
            xhr.send();
            """ % {'id': self.identifier, 'frame_index': str(time.time())})

    def get_image_data(self, index=0):
        Image.set_image(self, '/%(id)s/get_image_data?index=%(frame_index)s'% {'id': self.identifier, 'frame_index': str(time.time())})
        self._set_updated()
        try:
            data = None
            canv = FigureCanvasAgg(self._fig)
            buf = io.BytesIO()
            canv.print_figure(buf, format='png')
            buf.seek(0)
            data = buf.read()
            headers = {'Content-type': 'image/png', 'Cache-Control': 'no-cache'}
            return [data, headers]
        except Exception:
            pass
            #print(traceback.format_exc())
        return None, None

#CLASS Thermistors
#This class is the main program structured as a REMI App object.
class Thermistors(remi.App):
    def __init__(self, *args, **kwargs):
        if not 'editing_mode' in kwargs.keys():
            super(Thermistors, self).__init__(*args, static_file_path={'my_res': './res/'})

    #FUNCTION daq_process() is the data acquisition process running as a separate thread.
    #Each ADC channel takes about 170 milliseconds to access and read.
    #Temperature readings are written to the circular buffers that were allocated at start-up.
    #[DEV] Provide an option for this function to also write readings to a file.
    def daq_process(self):
        adc_channel_init(0)  #set the ADC chip to the first channel that will be read
        while self.daq_running:
            #In this For loop, i is the adc channel being read,
            #and j is the next adc channel that will be read.
            t = self.reftime
            for i in range(0, Nsensors):
                j = i + 1
                if j >= Nsensors:
                    j = 0
                sleep(buswait)  #this wait between reads is critical to avoid I2C I/O errors
                while self.daq_paused:
                    sleep(buswait)
                if not self.daq_running:
                    break
                t = time.time() - self.reftime   #capture timestamp before reading T
                self.ydata[i].add(read_temperature(i, j))   #read T and write to Y buffer
                self.xdata[i].add(t)   #write timestamp to X buffer after T is done reading
            self.ydata[1].add(self.Tsetpoint)   #use the second buffer for T setpoint
            self.xdata[1].add(t)
            self.ydata[2].add(self.pheater)   #use the third buffer for heater watts
            self.xdata[2].add(t)

    #FUNCTION idle() is called by the REMI engine once per display update interval.
    #This function's job is to update all of the numeric indicators and strip chart on the UI.
    def idle(self):
        allChannelsIn = True
        T = -500.0
        n = self.ydata[0].size()
        if n > 0:
            T = self.ydata[0].readlast()   #get the latest T reading from the DAQ thread
            if self.unitIsCelcius:
                Tdisp = T
            else:
                Tdisp = CtoF(T)   #convert Celcius to Fahrenheit (if so configured)
            Tstr = f'{Tdisp:.2f}'   #display value to 2 decimal places
        else:
            allChannelsIn = False
            Tstr = '-.--'
        self.mainTabs.children['Control'].children['labelTmeasured'].set_text(Tstr)
        if self.Henable:
            if T > -499.0 and self.Tprev > -499.0 and self.Tcontrol_on and self.daq_running:
                Tdiff = T - self.Tsetpoint   #calculate the difference between the measured temperature and the setpoint
                Tslope = T - self.Tprev   #calculate the difference between the current temperature reading and the previous reading
                if abs(Tdiff) > Tctrlband:
                    if Tdiff > 0.0:
                        self.heat = 0.0
                    else:
                        self.heat = hmax
                else:
                    #Adjust heater current using the feeback control gains:
                    self.heat = self.heat + (gainI * Tdiff) + (gainD * Tslope)
                    if self.heat > hmax:
                        self.heat = hmax
                    if self.heat < 0.0:
                        self.heat = 0.0
                heater.value = self.heat
        else:
            self.heat = 0.0
            heater.value = self.heat
        iheater = self.heat * vheater / rheater   #calculate heater amps from the duty cycle and heater resistance
        self.pheater = vheater * iheater   #calculate heater power in watts
        iheater_mA = iheater * 1000.0   #convert heater current to milliamps for display
        self.mainTabs.children['Control'].children['labelHcurrent'].set_text(f'{iheater_mA:.1f}')
        self.mainTabs.children['Control'].children['labelHpower'].set_text(f'{self.pheater:.3f}')
        if T > -499.0:
            self.Tprev = T
        
        #Generate the strip chart if all sensors have at least one measured sample in.
        if allChannelsIn:
            x2a = []; y1a = []; y2a = []
            p = []   #initialize array of Matplotlib plot objects, needed for making the legend
            self.mpl.ax.clear()  #Clear the previous plots. Unfortunately, this clears the axis labels, too.
            self.mpl.y2ax.clear()
            for i in range(0, Nbuffers):
                xlist = self.xdata[i].readtimespan(xspan + update_interval_sec)  #read the X data (time) from the buffer
                ylist = self.ydata[i].read(len(xlist))  #read the Y data (temperature or power) from the buffer
                if not self.unitIsCelcius:
                    ylist = [CtoF(y) for y in ylist]
                #Plot the data using each plot's assigned color and name. Append each plot object to p[].
                if i < 2:
                    p.append(self.mpl.ax.plot(xlist, ylist, color=plotColors[i], label=plotNames[i])[0])
                else:
                    p.append(self.mpl.y2ax.plot(xlist, ylist, color=plotColors[i], label=plotNames[i])[0])
                x2a.append(xlist[-1])
                y1a.append(min(ylist))
                y2a.append(max(ylist))
            
            #Calculate the X and Y axes limits (x1, x2) and (y1, y2)
            x2 = max(x2a)
            if x2 < xspan:
                x2 = xspan
                x1 = 0.0
            else:
                x1 = x2 - xspan
            if yAuto:
                y1 = math.floor(min(y1a)); y2 = math.ceil(max(y2a))
            elif self.unitIsCelcius:
                y1 = ymin; y2 = ymax
            else:
                y1 = math.floor(CtoF(ymin)); y2 = math.ceil(CtoF(ymax))
                
            #Generate the new graph and update the display
            self.mpl.ax.set(xlabel='Time (s)',
                ylabel='Temperature (deg ' + ('C' if self.unitIsCelcius else 'F') + ')',
                autoscale_on=False, xlim=(x1, x2), ylim=(y1, y2),
                title='Temperature and Heater Power vs. Time')
            self.mpl.y2ax.set(ylabel='Heater Power (W)', autoscale_on=False, ylim=(y2min, y2max))
            self.mpl.ax.legend(handles=p, loc='upper left')
            self.mpl.update()

    #FUNCTION main() is called by the REMI engine once at program start.
    def main(self):
        self.xdata = []; self.ydata = []
        for i in range(0, Nbuffers):   #allocate the data buffers
            self.xdata.append(CircBuff(buffer_length))
            #self.ydata.append(CircBuff(buffer_length, True))  #True for filtering
            self.ydata.append(CircBuff(buffer_length))
        self.unitIsCelcius = True
        self.reftime = time.time()
        self.Tprev = -500.0
        self.heat = 0.0
        self.pheater = 0.0
        self.Henable = False
        self.Tcontrol_on = False
        self.Tsetpoint = 20.0
        self.daq_paused = False
        self.daq_running = True
        t = threading.Thread(target=self.daq_process)
        t.start()   #start the DAQ Process thread
        return self.construct_ui()

    #FUNCTION construct_ui() consists mostly of Python code generated by the REMI Editor,
    #which you can use to layout the web page and its indicators, text, images, and controls.
    def construct_ui(self):
        #DON'T MAKE CHANGES HERE, THIS METHOD GETS OVERWRITTEN WHEN SAVING IN THE EDITOR
        mainTabs = TabBox()
        mainTabs.attr_editor_newclass = False
        mainTabs.css_height = "340.0px"
        mainTabs.css_left = "15.0px"
        mainTabs.css_margin = "0px"
        mainTabs.css_position = "absolute"
        mainTabs.css_top = "15.0px"
        mainTabs.css_width = "720.0px"
        mainTabs.variable_name = "mainTabs"
        contControl = Container()
        contControl.attr_editor_newclass = False
        contControl.css_height = "260.0px"
        contControl.css_left = "15.0px"
        contControl.css_margin = "0px"
        contControl.css_position = "absolute"
        contControl.css_top = "45.0px"
        contControl.css_width = "554.0px"
        contControl.variable_name = "contControl"
        checkHenable = CheckBoxLabel()
        checkHenable.attr_editor_newclass = False
        checkHenable.css_align_items = "center"
        checkHenable.css_display = "flex"
        checkHenable.css_flex_direction = "row"
        checkHenable.css_height = "30.0px"
        checkHenable.css_justify_content = "space-around"
        checkHenable.css_left = "20px"
        checkHenable.css_margin = "0px"
        checkHenable.css_position = "absolute"
        checkHenable.css_top = "10px"
        checkHenable.css_width = "165.0px"
        checkHenable.text = "Heater Power Enable"
        checkHenable.variable_name = "checkHenable"
        contControl.append(checkHenable,'checkHenable')
        checkTCenable = CheckBoxLabel()
        checkTCenable.attr_editor_newclass = False
        checkTCenable.css_align_items = "center"
        checkTCenable.css_display = "flex"
        checkTCenable.css_flex_direction = "row"
        checkTCenable.css_height = "30.0px"
        checkTCenable.css_justify_content = "space-around"
        checkTCenable.css_left = "20px"
        checkTCenable.css_margin = "0px"
        checkTCenable.css_position = "absolute"
        checkTCenable.css_top = "40px"
        checkTCenable.css_width = "270.0px"
        checkTCenable.text = "Temperature Feedback Control Enable"
        checkTCenable.variable_name = "checkTCenable"
        contControl.append(checkTCenable,'checkTCenable')
        labelTsetpoint = Label()
        labelTsetpoint.attr_editor_newclass = False
        labelTsetpoint.css_border_style = "solid"
        labelTsetpoint.css_border_width = "1px"
        labelTsetpoint.css_font_size = "30px"
        labelTsetpoint.css_font_weight = "bold"
        labelTsetpoint.css_height = "36px"
        labelTsetpoint.css_left = "19.0px"
        labelTsetpoint.css_margin = "0px"
        labelTsetpoint.css_position = "absolute"
        labelTsetpoint.css_top = "105.0px"
        labelTsetpoint.css_width = "115.0px"
        labelTsetpoint.text = f'{self.Tsetpoint:.2f}'
        labelTsetpoint.variable_name = "labelTsetpoint"
        contControl.append(labelTsetpoint,'labelTsetpoint')
        label0 = Label()
        label0.attr_editor_newclass = False
        label0.css_height = "22.0px"
        label0.css_left = "21.0px"
        label0.css_margin = "0px"
        label0.css_position = "absolute"
        label0.css_top = "87px"
        label0.css_width = "102.0px"
        label0.text = "T Setpoint"
        label0.variable_name = "label0"
        contControl.append(label0,'label0')
        label1 = Label()
        label1.attr_editor_newclass = False
        label1.css_font_size = "30px"
        label1.css_height = "37.0px"
        label1.css_left = "142.0px"
        label1.css_margin = "0px"
        label1.css_position = "absolute"
        label1.css_top = "106px"
        label1.css_width = "44.0px"
        label1.text = "C"
        label1.variable_name = "label1"
        contControl.append(label1,'label1')
        labelTmeasured = Label()
        labelTmeasured.attr_editor_newclass = False
        labelTmeasured.css_border_style = "solid"
        labelTmeasured.css_border_width = "1px"
        labelTmeasured.css_font_size = "30px"
        labelTmeasured.css_font_weight = "bold"
        labelTmeasured.css_height = "36px"
        labelTmeasured.css_left = "190px"
        labelTmeasured.css_margin = "0px"
        labelTmeasured.css_position = "absolute"
        labelTmeasured.css_top = "105.0px"
        labelTmeasured.css_width = "115px"
        labelTmeasured.text = "-.--"
        labelTmeasured.variable_name = "labelTmeasured"
        contControl.append(labelTmeasured,'labelTmeasured')
        label2 = Label()
        label2.attr_editor_newclass = False
        label2.css_height = "23.0px"
        label2.css_left = "192.0px"
        label2.css_margin = "0px"
        label2.css_position = "absolute"
        label2.css_top = "88.0px"
        label2.css_width = "92.0px"
        label2.text = "T Measured"
        label2.variable_name = "label2"
        contControl.append(label2,'label2')
        label3 = Label()
        label3.attr_editor_newclass = False
        label3.css_font_size = "30px"
        label3.css_height = "38.0px"
        label3.css_left = "312px"
        label3.css_margin = "0px"
        label3.css_position = "absolute"
        label3.css_top = "106px"
        label3.css_width = "47.0px"
        label3.text = "C"
        label3.variable_name = "label3"
        contControl.append(label3,'label3')
        labelHcurrent = Label()
        labelHcurrent.attr_editor_newclass = False
        labelHcurrent.css_border_style = "solid"
        labelHcurrent.css_border_width = "1px"
        labelHcurrent.css_font_size = "30px"
        labelHcurrent.css_font_weight = "bold"
        labelHcurrent.css_height = "36px"
        labelHcurrent.css_left = "363px"
        labelHcurrent.css_margin = "0px"
        labelHcurrent.css_position = "absolute"
        labelHcurrent.css_top = "105px"
        labelHcurrent.css_width = "115px"
        labelHcurrent.text = "-.-"
        labelHcurrent.variable_name = "labelHcurrent"
        contControl.append(labelHcurrent,'labelHcurrent')
        label4 = Label()
        label4.attr_editor_newclass = False
        label4.css_height = "21.0px"
        label4.css_left = "365.0px"
        label4.css_margin = "0px"
        label4.css_position = "absolute"
        label4.css_top = "88.0px"
        label4.css_width = "88.0px"
        label4.text = "H Current"
        label4.variable_name = "label4"
        contControl.append(label4,'label4')
        label5 = Label()
        label5.attr_editor_newclass = False
        label5.css_font_size = "30px"
        label5.css_height = "40.0px"
        label5.css_left = "486.0px"
        label5.css_margin = "0px"
        label5.css_position = "absolute"
        label5.css_top = "105px"
        label5.css_width = "64.0px"
        label5.text = "mA"
        label5.variable_name = "label5"
        contControl.append(label5,'label5')
        labelHpower = Label()
        labelHpower.attr_editor_newclass = False
        labelHpower.css_border_style = "solid"
        labelHpower.css_border_width = "1px"
        labelHpower.css_font_size = "30px"
        labelHpower.css_font_weight = "bold"
        labelHpower.css_height = "36px"
        labelHpower.css_left = "364px"
        labelHpower.css_margin = "0px"
        labelHpower.css_position = "absolute"
        labelHpower.css_top = "185px"
        labelHpower.css_width = "115px"
        labelHpower.text = "-.---"
        labelHpower.variable_name = "labelHpower"
        contControl.append(labelHpower,'labelHpower')
        label6 = Label()
        label6.attr_editor_newclass = False
        label6.css_height = "23.0px"
        label6.css_left = "366.0px"
        label6.css_margin = "0px"
        label6.css_position = "absolute"
        label6.css_top = "168.0px"
        label6.css_width = "79.0px"
        label6.text = "H Power"
        label6.variable_name = "label6"
        contControl.append(label6,'label6')
        label7 = Label()
        label7.attr_editor_newclass = False
        label7.css_font_size = "30px"
        label7.css_height = "39.0px"
        label7.css_left = "487px"
        label7.css_margin = "0px"
        label7.css_position = "absolute"
        label7.css_top = "185px"
        label7.css_width = "50.0px"
        label7.text = "W"
        label7.variable_name = "label7"
        contControl.append(label7,'label7')
        buttonAdd5 = Button()
        buttonAdd5.attr_editor_newclass = False
        buttonAdd5.css_font_size = "25px"
        buttonAdd5.css_height = "39.0px"
        buttonAdd5.css_left = "24.0px"
        buttonAdd5.css_margin = "0px"
        buttonAdd5.css_position = "absolute"
        buttonAdd5.css_top = "160.0px"
        buttonAdd5.css_width = "43.0px"
        buttonAdd5.text = "+5"
        buttonAdd5.variable_name = "buttonAdd5"
        contControl.append(buttonAdd5,'buttonAdd5')
        buttonSubtract5 = Button()
        buttonSubtract5.attr_editor_newclass = False
        buttonSubtract5.css_font_size = "25px"
        buttonSubtract5.css_height = "39px"
        buttonSubtract5.css_left = "89px"
        buttonSubtract5.css_margin = "0px"
        buttonSubtract5.css_position = "absolute"
        buttonSubtract5.css_top = "160.0px"
        buttonSubtract5.css_width = "43px"
        buttonSubtract5.text = "-5"
        buttonSubtract5.variable_name = "buttonSubtract5"
        contControl.append(buttonSubtract5,'buttonSubtract5')
        buttonAdd1 = Button()
        buttonAdd1.attr_editor_newclass = False
        buttonAdd1.css_font_size = "25px"
        buttonAdd1.css_height = "39px"
        buttonAdd1.css_left = "24.0px"
        buttonAdd1.css_margin = "0px"
        buttonAdd1.css_position = "absolute"
        buttonAdd1.css_top = "211px"
        buttonAdd1.css_width = "43px"
        buttonAdd1.text = "+1"
        buttonAdd1.variable_name = "buttonAdd1"
        contControl.append(buttonAdd1,'buttonAdd1')
        buttonSubtract1 = Button()
        buttonSubtract1.attr_editor_newclass = False
        buttonSubtract1.css_font_size = "25px"
        buttonSubtract1.css_height = "39px"
        buttonSubtract1.css_left = "89px"
        buttonSubtract1.css_margin = "0px"
        buttonSubtract1.css_position = "absolute"
        buttonSubtract1.css_top = "211.0px"
        buttonSubtract1.css_width = "43px"
        buttonSubtract1.text = "-1"
        buttonSubtract1.variable_name = "buttonSubtract1"
        contControl.append(buttonSubtract1,'buttonSubtract1')
        buttonEndProgram = Button()
        buttonEndProgram.attr_editor_newclass = False
        buttonEndProgram.css_height = "51.0px"
        buttonEndProgram.css_left = "452.0px"
        buttonEndProgram.css_margin = "0px"
        buttonEndProgram.css_position = "absolute"
        buttonEndProgram.css_top = "19.0px"
        buttonEndProgram.css_width = "81.0px"
        buttonEndProgram.text = "End Server Process"
        buttonEndProgram.variable_name = "buttonEndProgram"
        contControl.append(buttonEndProgram,'buttonEndProgram')
        buttonClearChart = Button()
        buttonClearChart.attr_editor_newclass = False
        buttonClearChart.css_height = "51px"
        buttonClearChart.css_left = "246px"
        buttonClearChart.css_margin = "0px"
        buttonClearChart.css_position = "absolute"
        buttonClearChart.css_top = "200.0px"
        buttonClearChart.css_width = "62.0px"
        buttonClearChart.text = "Clear Chart"
        buttonClearChart.variable_name = "buttonClearChart"
        contControl.append(buttonClearChart,'buttonClearChart')
        mainTabs.append(contControl,'Control')
        graphContainer = Container()
        graphContainer.attr_editor_newclass = False
        graphContainer.css_border_color = "rgb(25,31,37)"
        graphContainer.css_border_style = "solid"
        graphContainer.css_border_width = "1%"
        graphContainer.css_display = "none"
        graphContainer.css_height = "280px"
        graphContainer.css_left = "15px"
        graphContainer.css_margin = "0px"
        graphContainer.css_position = "absolute"
        graphContainer.css_top = "45px"
        graphContainer.css_width = "680px"
        graphContainer.variable_name = "graphContainer"
        self.mpl = MatplotImage(width=660, height=265)
        self.mpl.style['margin'] = '0px'
        self.mpl.ax.set(xlabel='Time (s)', ylabel='Temperature (deg C)',
            autoscale_on=False, xlim=(0.0, xspan), ylim=(ymin, ymax),
            title='Temperature and Heater Power vs. Time')
        self.mpl.y2ax = self.mpl.ax.twinx()
        self.mpl.y2ax.set(ylabel='Heater Power (W)', autoscale_on=False, ylim=(y2min, y2max))
        self.mpl.y2ax.yaxis.label.set_color(plotColors[2])
        tkw = dict(size=4, width=1.5)
        self.mpl.y2ax.tick_params(axis='y', colors=plotColors[2], **tkw)
        self.mpl.update()
        graphContainer.append(self.mpl)
        mainTabs.append(graphContainer,'Graph')
        mainTabs.children['Control'].children['checkHenable'].onchange.do(self.onchange_checkHenable)
        mainTabs.children['Control'].children['checkTCenable'].onchange.do(self.onchange_checkTCenable)
        mainTabs.children['Control'].children['buttonAdd5'].onclick.do(self.onclick_buttonAdd5)
        mainTabs.children['Control'].children['buttonSubtract5'].onclick.do(self.onclick_buttonSubtract5)
        mainTabs.children['Control'].children['buttonAdd1'].onclick.do(self.onclick_buttonAdd1)
        mainTabs.children['Control'].children['buttonSubtract1'].onclick.do(self.onclick_buttonSubtract1)
        mainTabs.children['Control'].children['buttonEndProgram'].onclick.do(self.onclick_buttonEndProgram)
        mainTabs.children['Control'].children['buttonClearChart'].onclick.do(self.onclick_buttonClearChart)
 
        self.mainTabs = mainTabs
        return self.mainTabs
    
    def onchange_checkHenable(self, emitter, value):
        self.Henable = value
        
    def onchange_checkTCenable(self, emitter, value):
        self.Tcontrol_on = value
        
    def onclick_buttonAdd5(self, emitter):
        self.Tsetpoint += 5.0
        self.mainTabs.children['Control'].children['labelTsetpoint'].set_text(f'{self.Tsetpoint:.2f}')
        
    def onclick_buttonSubtract5(self, emitter):
        self.Tsetpoint -= 5.0
        self.mainTabs.children['Control'].children['labelTsetpoint'].set_text(f'{self.Tsetpoint:.2f}')
        
    def onclick_buttonAdd1(self, emitter):
        self.Tsetpoint += 1.0
        self.mainTabs.children['Control'].children['labelTsetpoint'].set_text(f'{self.Tsetpoint:.2f}')
        
    def onclick_buttonSubtract1(self, emitter):
        self.Tsetpoint -= 1.0
        self.mainTabs.children['Control'].children['labelTsetpoint'].set_text(f'{self.Tsetpoint:.2f}')
        
    def onclick_buttonClearChart(self, emitter):
        self.daq_paused = True   #Pause DAQ thread and wait for it to actually pause
        sleep(1.0*buswait)
        self.reftime = time.time()
        for i in range(0, Nbuffers):
            self.xdata[i].clear()
            self.ydata[i].clear()
        self.daq_paused = False   #Unpause DAQ thread now that buffers, pointers, and flags are fully cleared
        
    def onclick_buttonEndProgram(self, emitter):
        self.daq_running = False
        self.daq_paused = False
        sleep(2.0*buswait)
        heater.value = 0.0
        sleep(1.0*buswait)
        self.close()

#configuration hash set below is just for the REMI UI Editor
configuration = {'config_project_name': 'Thermistors', 'config_resourcepath': './res/'}

#The REMI start() function is called below to start serving the web page.
localmode = False
if localmode:
    ipaddr = '127.0.0.1'; show_browser = True
else:
    ipaddr = '192.168.1.193'; show_browser = False

if __name__ == "__main__":
    remi.start(Thermistors, address=ipaddr, port=8081, update_interval=update_interval_sec,
          multiple_instance=False, enable_file_cache=False, start_browser=show_browser)