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)