2021-05-01 09:29:10 +00:00
#!/usr/bin/env python
2021-04-30 21:23:11 +00:00
import os
import sys
import csv
import time
import argparse
2021-05-01 13:18:27 +00:00
def recordprofile ( csvfile , targettemp ) :
2021-04-30 21:23:11 +00:00
2021-05-01 13:18:27 +00:00
try :
sys . dont_write_bytecode = True
import config
sys . dont_write_bytecode = False
2021-04-30 21:23:11 +00:00
2021-05-01 13:25:13 +00:00
except ImportError :
2021-05-01 13:18:27 +00:00
print ( " Could not import config file. " )
print ( " Copy config.py.EXAMPLE to config.py and adapt it for your setup. " )
exit ( 1 )
2021-04-30 21:23:11 +00:00
2021-05-01 13:18:27 +00:00
script_dir = os . path . dirname ( os . path . realpath ( __file__ ) )
sys . path . insert ( 0 , script_dir + ' /lib/ ' )
2021-04-30 21:23:11 +00:00
2021-05-01 13:18:27 +00:00
from oven import RealOven , SimulatedOven
2021-04-30 21:23:11 +00:00
# open the file to log data to
f = open ( csvfile , ' w ' )
csvout = csv . writer ( f )
2021-05-01 09:38:16 +00:00
csvout . writerow ( [ ' time ' , ' temperature ' ] )
2021-04-30 21:23:11 +00:00
# construct the oven
if config . simulate :
oven = SimulatedOven ( )
else :
oven = RealOven ( )
# Main loop:
#
# * heat the oven to the target temperature at maximum burn.
# * when we reach it turn the heating off completely.
# * wait for it to decay back to the target again.
# * quit
#
2021-05-01 13:22:47 +00:00
# We record the temperature every second
2021-04-30 21:23:11 +00:00
try :
stage = ' heating '
if not config . simulate :
2021-07-03 16:13:14 +00:00
oven . output . heat ( 0 )
2021-04-30 21:23:11 +00:00
while True :
temp = oven . board . temp_sensor . temperature + \
config . thermocouple_offset
csvout . writerow ( [ time . time ( ) , temp ] )
2021-05-01 08:44:55 +00:00
f . flush ( )
2021-04-30 21:23:11 +00:00
if stage == ' heating ' :
2021-05-01 09:38:49 +00:00
if temp > = targettemp :
2021-04-30 21:23:11 +00:00
if not config . simulate :
2021-07-03 16:15:16 +00:00
oven . output . cool ( 0 )
2021-05-01 09:35:14 +00:00
stage = ' cooling '
2021-04-30 21:23:11 +00:00
2021-05-01 09:35:14 +00:00
elif stage == ' cooling ' :
2021-04-30 21:23:11 +00:00
if temp < targettemp :
break
2021-06-09 02:23:17 +00:00
print ( " stage = %s , actual = %s , target = %s " % ( stage , temp , targettemp ) )
2021-05-01 13:22:47 +00:00
time . sleep ( 1 )
2021-04-30 21:23:11 +00:00
f . close ( )
finally :
# ensure we always shut the oven down!
if not config . simulate :
2021-07-03 16:13:14 +00:00
oven . output . cool ( 0 )
2021-04-30 21:23:11 +00:00
2021-05-01 13:18:27 +00:00
def line ( a , b , x ) :
return a * x + b
def invline ( a , b , y ) :
return ( y - b ) / a
def plot ( xdata , ydata ,
tangent_min , tangent_max , tangent_slope , tangent_offset ,
lower_crossing_x , upper_crossing_x ) :
from matplotlib import pyplot
minx = min ( xdata )
maxx = max ( xdata )
miny = min ( ydata )
maxy = max ( ydata )
pyplot . scatter ( xdata , ydata )
pyplot . plot ( [ minx , maxx ] , [ miny , miny ] , ' -- ' , color = ' purple ' )
pyplot . plot ( [ minx , maxx ] , [ maxy , maxy ] , ' -- ' , color = ' purple ' )
pyplot . plot ( tangent_min [ 0 ] , tangent_min [ 1 ] , ' v ' , color = ' red ' )
pyplot . plot ( tangent_max [ 0 ] , tangent_max [ 1 ] , ' v ' , color = ' red ' )
pyplot . plot ( [ minx , maxx ] , [ line ( tangent_slope , tangent_offset , minx ) , line ( tangent_slope , tangent_offset , maxx ) ] , ' -- ' , color = ' red ' )
pyplot . plot ( [ lower_crossing_x , lower_crossing_x ] , [ miny , maxy ] , ' -- ' , color = ' black ' )
pyplot . plot ( [ upper_crossing_x , upper_crossing_x ] , [ miny , maxy ] , ' -- ' , color = ' black ' )
pyplot . show ( )
def calculate ( filename , tangentdivisor , showplot ) :
# parse the csv file
xdata = [ ]
ydata = [ ]
filemintime = None
with open ( filename ) as f :
for row in csv . DictReader ( f ) :
try :
time = float ( row [ ' time ' ] )
temp = float ( row [ ' temperature ' ] )
if filemintime is None :
filemintime = time
xdata . append ( time - filemintime )
ydata . append ( temp )
except ValueError :
continue # just ignore bad values!
# gather points for tangent line
miny = min ( ydata )
maxy = max ( ydata )
midy = ( maxy + miny ) / 2
yoffset = int ( ( maxy - miny ) / tangentdivisor )
tangent_min = tangent_max = None
for i in range ( 0 , len ( xdata ) ) :
rowx = xdata [ i ]
rowy = ydata [ i ]
if rowy > = ( midy - yoffset ) and tangent_min is None :
tangent_min = ( rowx , rowy )
elif rowy > = ( midy + yoffset ) and tangent_max is None :
tangent_max = ( rowx , rowy )
# calculate tangent line to the main temperature curve
tangent_slope = ( tangent_max [ 1 ] - tangent_min [ 1 ] ) / ( tangent_max [ 0 ] - tangent_min [ 0 ] )
tangent_offset = tangent_min [ 1 ] - line ( tangent_slope , 0 , tangent_min [ 0 ] )
# determine the point at which the tangent line crosses the min/max temperaturess
lower_crossing_x = invline ( tangent_slope , tangent_offset , miny )
upper_crossing_x = invline ( tangent_slope , tangent_offset , maxy )
# compute parameters
L = lower_crossing_x - min ( xdata )
T = upper_crossing_x - lower_crossing_x
# Magic Ziegler-Nicols constants ahead!
Kp = 1.2 * ( T / L )
Ti = 2 * L
Td = 0.5 * L
Ki = Kp / Ti
Kd = Kp * Td
2021-06-08 14:21:44 +00:00
# output to the user
print ( " pid_kp = %s " % ( Kp ) )
print ( " pid_ki = %s " % ( 1 / Ki ) )
print ( " pid_kd = %s " % ( Kd ) )
2021-05-01 13:18:27 +00:00
if showplot :
plot ( xdata , ydata ,
tangent_min , tangent_max , tangent_slope , tangent_offset ,
lower_crossing_x , upper_crossing_x )
2021-04-30 21:23:11 +00:00
if __name__ == " __main__ " :
2021-05-01 13:21:13 +00:00
parser = argparse . ArgumentParser ( description = ' Kiln tuner ' )
2021-05-01 13:18:27 +00:00
subparsers = parser . add_subparsers ( )
2021-05-01 14:12:21 +00:00
parser . set_defaults ( mode = ' ' )
2021-05-01 13:18:27 +00:00
parser_profile = subparsers . add_parser ( ' recordprofile ' , help = ' Record kiln temperature profile ' )
parser_profile . add_argument ( ' csvfile ' , type = str , help = " The CSV file to write to. " )
parser_profile . add_argument ( ' --targettemp ' , type = int , default = 400 , help = " The target temperature to drive the kiln to (default 400). " )
parser_profile . set_defaults ( mode = ' recordprofile ' )
parser_zn = subparsers . add_parser ( ' zn ' , help = ' Calculate Ziegler-Nicols parameters ' )
2021-05-01 13:20:49 +00:00
parser_zn . add_argument ( ' csvfile ' , type = str , help = " The CSV file to read from. Must contain two columns called time (time in seconds) and temperature (observed temperature) " )
2021-05-01 13:18:27 +00:00
parser_zn . add_argument ( ' --showplot ' , action = ' store_true ' , help = " If set, also plot results (requires pyplot to be pip installed) " )
2021-05-01 13:34:57 +00:00
parser_zn . add_argument ( ' --tangentdivisor ' , type = float , default = 8 , help = " Adjust the tangent calculation to fit better. Must be >= 2 (default 8). " )
2021-05-01 13:18:27 +00:00
parser_zn . set_defaults ( mode = ' zn ' )
2021-04-30 21:23:11 +00:00
args = parser . parse_args ( )
2021-05-01 13:18:27 +00:00
if args . mode == ' recordprofile ' :
recordprofile ( args . csvfile , args . targettemp )
elif args . mode == ' zn ' :
if args . tangentdivisor < 2 :
raise ValueError ( " tangentdivisor must be >= 2 " )
calculate ( args . csvfile , args . tangentdivisor , args . showplot )
2021-05-01 14:12:21 +00:00
elif args . mode == ' ' :
parser . print_help ( )
exit ( 1 )
2021-05-01 13:18:27 +00:00
else :
2021-06-09 02:23:17 +00:00
raise NotImplementedError ( " Unknown mode %s " % args . mode )