forked from jbruce12000/kiln-controller
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathkiln-tuner.py
executable file
·208 lines (157 loc) · 6.39 KB
/
kiln-tuner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python
import os
import sys
import csv
import time
import argparse
def recordprofile(csvfile, targettemp):
try:
sys.dont_write_bytecode = True
import config
sys.dont_write_bytecode = False
except ImportError:
print("Could not import config file.")
print("Copy config.py.EXAMPLE to config.py and adapt it for your setup.")
exit(1)
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir + '/lib/')
from oven import RealOven, SimulatedOven
# open the file to log data to
f = open(csvfile, 'w')
csvout = csv.writer(f)
csvout.writerow(['time', 'temperature'])
# 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
#
# We record the temperature every second
try:
stage = 'heating'
if not config.simulate:
oven.output.heat(1, tuning=True)
while True:
temp = oven.board.temp_sensor.temperature + \
config.thermocouple_offset
csvout.writerow([time.time(), temp])
f.flush()
if stage == 'heating':
if temp >= targettemp:
if not config.simulate:
oven.output.heat(0)
stage = 'cooling'
elif stage == 'cooling':
if temp < targettemp:
break
print("stage = %s, actual = %s, target = %s" % (stage,temp,targettemp))
time.sleep(1)
f.close()
finally:
# ensure we always shut the oven down!
if not config.simulate:
oven.output.heat(0)
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
# output to the user
print("pid_kp = %s" % (Kp))
print("pid_ki = %s" % (1 / Ki))
print("pid_kd = %s" % (Kd))
if showplot:
plot(xdata, ydata,
tangent_min, tangent_max, tangent_slope, tangent_offset,
lower_crossing_x, upper_crossing_x)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Kiln tuner')
subparsers = parser.add_subparsers()
parser.set_defaults(mode='')
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')
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)")
parser_zn.add_argument('--showplot', action='store_true', help="If set, also plot results (requires pyplot to be pip installed)")
parser_zn.add_argument('--tangentdivisor', type=float, default=8, help="Adjust the tangent calculation to fit better. Must be >= 2 (default 8).")
parser_zn.set_defaults(mode='zn')
args = parser.parse_args()
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)
elif args.mode == '':
parser.print_help()
exit(1)
else:
raise NotImplementedError("Unknown mode %s" % args.mode)