Porównaj commity

...

25 Commity

Autor SHA1 Wiadomość Data
jbruce12000 2df86242f3 turn off debug logging 2022-12-31 11:51:31 -05:00
Jason Bruce a503944e2d
Merge pull request #124 from chipgarner/fastsim
Fastsim
2022-12-31 11:45:16 -05:00
Jason Bruce 418dfd900e
Merge pull request #126 from chipgarner/seek
Seek
2022-12-31 11:29:27 -05:00
James Kirikland Garner 37f2a53aec Set start temperature for simulation using sim_t_env in config. 2022-12-30 12:53:06 -08:00
James Kirikland Garner 3c515761e8 skip sink on API start with start time set 2022-12-26 20:32:51 -08:00
James Kirikland Garner b960bb4710 Fixed running seek on auto restart bug. 2022-12-24 13:56:07 -08:00
James Kirikland Garner ee70ba1667 Seek is working with log, pytests added. 2022-12-23 15:01:35 -08:00
James Kirikland Garner 4c03cfa8a6 git ignore 2022-12-23 11:56:20 -08:00
James Kirikland Garner 21b2655867 Really put back ovenWatcher I hope; 2022-12-18 10:54:35 -08:00
James Kirikland Garner d5af5bcf7d Put back as it was 2022-12-18 10:50:46 -08:00
James Kirikland Garner c43770ace5 Removed changes not needed for PR 2022-12-18 10:30:07 -08:00
James Kirikland Garner 3a097e5098 revert to 5df0bc5 as in pull request 2022-12-17 19:56:01 -08:00
James Kirikland Garner d8c1f7cb00 fussing 2022-12-16 19:24:03 -08:00
James Kirikland Garner 5df0bc503a Merge branch 'fastsim' into blinka 2022-12-16 09:25:11 -08:00
James Kirikland Garner ab0b0e11c0 Merge branch 'blinka' of https://github.com/jbruce12000/kiln-controller into blinka 2022-12-16 09:24:18 -08:00
James Kirikland Garner 9900bb4421 Oven watcher time hard coded, added test json 2022-12-15 14:49:32 -08:00
James Kirikland Garner 8d770b3086 Blinka merge, playing with config settings. 2022-12-15 10:23:08 -08:00
James Kirikland Garner a6674c5ddc now is fixed 2022-12-15 09:59:22 -08:00
James Kirikland Garner c9ee92c93d now bug 2022-12-15 09:38:14 -08:00
James Kirikland Garner 83ce8fb5f5 fix now bug 2022-12-15 09:22:58 -08:00
James Kirikland Garner 656e808f59 merge 2022-12-15 08:52:46 -08:00
James Kirikland Garner 945fcf4187 Works at 1000 times speed, a little messy. 2022-12-14 19:22:42 -08:00
James Kirikland Garner f5336ec2a1 Speeding up simulator. 2022-12-14 16:32:10 -08:00
jbruce 9b78ea005c clean up my mess 2022-11-15 11:02:21 -05:00
jbruce bfde2e0421 fix ticks to be even hours/mins/secs on graph 2022-11-15 10:46:00 -05:00
9 zmienionych plików z 177 dodań i 18 usunięć

1
.gitignore vendored
Wyświetl plik

@ -8,3 +8,4 @@ thumbs.db
#storage/profiles
#config.py
.idea/*
state.json

Wyświetl plik

@ -0,0 +1 @@
{"data": [[0, 200], [3600, 200], [4200, 500], [10800, 500], [14400, 2250], [16400, 2000], [19400, 2250]], "type": "profile", "name": "test-fast"}

Wyświetl plik

@ -0,0 +1 @@
{"data": [[0, 200], [3600, 200], [10800, 2000], [14400, 2250], [16400, 2250], [19400, 700]], "type": "profile", "name": "test-fast"}

Wyświetl plik

@ -0,0 +1,80 @@
from lib.oven import Profile
import os
import json
def get_profile(file = "test-fast.json"):
profile_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'Test', file))
print(profile_path)
with open(profile_path) as infile:
profile_json = json.dumps(json.load(infile))
profile = Profile(profile_json)
return profile
def test_get_target_temperature():
profile = get_profile()
temperature = profile.get_target_temperature(3000)
assert int(temperature) == 200
temperature = profile.get_target_temperature(6004)
assert temperature == 801.0
def test_find_time_from_temperature():
profile = get_profile()
time = profile.find_next_time_from_temperature(500)
assert time == 4800
time = profile.find_next_time_from_temperature(2004)
assert time == 10857.6
time = profile.find_next_time_from_temperature(1900)
assert time == 10400.0
def test_find_time_odd_profile():
profile = get_profile("test-cases.json")
time = profile.find_next_time_from_temperature(500)
assert time == 4200
time = profile.find_next_time_from_temperature(2023)
assert time == 16676.0
def test_find_x_given_y_on_line_from_two_points():
profile = get_profile()
y = 500
p1 = [3600, 200]
p2 = [10800, 2000]
time = profile.find_x_given_y_on_line_from_two_points(y, p1, p2)
assert time == 4800
y = 500
p1 = [3600, 200]
p2 = [10800, 200]
time = profile.find_x_given_y_on_line_from_two_points(y, p1, p2)
assert time == 0
y = 500
p1 = [3600, 600]
p2 = [10800, 600]
time = profile.find_x_given_y_on_line_from_two_points(y, p1, p2)
assert time == 0
y = 500
p1 = [3600, 500]
p2 = [10800, 500]
time = profile.find_x_given_y_on_line_from_two_points(y, p1, p2)
assert time == 0

Wyświetl plik

@ -75,6 +75,13 @@ max31856 = 0
# ThermocoupleType.S
# ThermocoupleType.T
########################################################################
#
# If your kiln is above the scheduled starting temperature when you click the Start button this
# feature will start the schedule at that temperature.
#
seek_start = True
########################################################################
#
# duty cycle of the entire system in seconds
@ -109,7 +116,7 @@ stop_integral_windup = True
#
# Simulation parameters
simulate = True
sim_t_env = 60.0 # deg C
sim_t_env = 255 # deg C
sim_c_heat = 500.0 # J/K heat capacity of heat element
sim_c_oven = 5000.0 # J/K heat capacity of oven
sim_p_heat = 5450.0 # W heating power of oven
@ -117,6 +124,7 @@ sim_R_o_nocool = 0.5 # K/W thermal resistance oven -> environment
sim_R_o_cool = 0.05 # K/W " with cooling
sim_R_ho_noair = 0.1 # K/W thermal resistance heat element -> oven
sim_R_ho_air = 0.05 # K/W " with internal air circulation
sim_speedup_factor = 1000
########################################################################
@ -148,7 +156,7 @@ kiln_must_catch_up = True
# or 100% off because the kiln is too hot. No integral builds up
# outside the window. The bigger you make the window, the more
# integral you will accumulate. This should be a positive integer.
pid_control_window = 5 #degrees
pid_control_window = 5 #degrees
# thermocouple offset
# If you put your thermocouple in ice water and it reads 36F, you can

Wyświetl plik

@ -73,6 +73,11 @@ def handle_api():
if 'startat' in bottle.request.json:
startat = bottle.request.json['startat']
#Shut off seek if start time has been set
allow_seek = True
if startat > 0:
allow_seek = False
# get the wanted profile/kiln schedule
profile = find_profile(wanted)
if profile is None:
@ -81,7 +86,7 @@ def handle_api():
# FIXME juggling of json should happen in the Profile class
profile_json = json.dumps(profile)
profile = Profile(profile_json)
oven.run_profile(profile,startat=startat)
oven.run_profile(profile, startat=startat, allow_seek=allow_seek)
ovenWatcher.record(profile)
if bottle.request.json['cmd'] == 'stop':

Wyświetl plik

@ -103,7 +103,7 @@ class TempSensorSimulated(TempSensor):
'''Simulates a temperature sensor '''
def __init__(self):
TempSensor.__init__(self)
self.simulated_temperature = 0
self.simulated_temperature = config.sim_t_env
def temperature(self):
return self.simulated_temperature
@ -329,10 +329,28 @@ class Oven(threading.Thread):
self.heat = 0
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
def run_profile(self, profile, startat=0):
@staticmethod
def get_start_from_temperature(profile, temp):
target_temp = profile.get_target_temperature(0)
if temp > target_temp + 5:
startat = profile.find_next_time_from_temperature(temp)
log.info("seek_start is in effect, starting at: {} s, {} deg".format(round(startat), round(temp)))
else:
startat = 0
return startat
def run_profile(self, profile, startat=0, allow_seek=True):
log.debug('run_profile run on thread' + threading.current_thread().name)
runtime = startat * 60
if allow_seek:
if self.state == 'IDLE':
if config.seek_start:
temp = self.board.temp_sensor.temperature() # Defined in a subclass
runtime += self.get_start_from_temperature(profile, temp)
self.reset()
self.startat = startat * 60
self.runtime = self.startat
self.runtime = runtime
self.start_time = datetime.datetime.now() - datetime.timedelta(seconds=self.startat)
self.profile = profile
self.totaltime = profile.get_duration()
@ -344,6 +362,9 @@ class Oven(threading.Thread):
self.reset()
self.save_automatic_restart_state()
def get_start_time(self):
return datetime.datetime.now() - datetime.timedelta(milliseconds = self.runtime * 1000)
def kiln_must_catch_up(self):
'''shift the whole schedule forward in time by one time_step
to wait for the kiln to catch up'''
@ -353,11 +374,11 @@ class Oven(threading.Thread):
# kiln too cold, wait for it to heat up
if self.target - temp > config.pid_control_window:
log.info("kiln must catch up, too cold, shifting schedule")
self.start_time = datetime.datetime.now() - datetime.timedelta(milliseconds = self.runtime * 1000)
self.start_time = self.get_start_time()
# kiln too hot, wait for it to cool down
if temp - self.target > config.pid_control_window:
log.info("kiln must catch up, too hot, shifting schedule")
self.start_time = datetime.datetime.now() - datetime.timedelta(milliseconds = self.runtime * 1000)
self.start_time = self.get_start_time()
def update_runtime(self):
@ -468,7 +489,7 @@ class Oven(threading.Thread):
with open(profile_path) as infile:
profile_json = json.dumps(json.load(infile))
profile = Profile(profile_json)
self.run_profile(profile,startat=startat)
self.run_profile(profile, startat=startat, allow_seek=False) # We don't want a seek on an auto restart.
self.cost = d["cost"]
time.sleep(1)
self.ovenwatcher.record(profile)
@ -479,6 +500,7 @@ class Oven(threading.Thread):
def run(self):
while True:
log.debug('Oven running on ' + threading.current_thread().name)
if self.state == "IDLE":
if self.should_i_automatic_restart() == True:
self.automatic_restart()
@ -505,9 +527,10 @@ class SimulatedOven(Oven):
self.R_o_nocool = config.sim_R_o_nocool
self.R_ho_noair = config.sim_R_ho_noair
self.R_ho = self.R_ho_noair
self.speedup_factor = config.sim_speedup_factor
# set temps to the temp of the surrounding environment
self.t = self.t_env # deg C temp of oven
self.t = config.sim_t_env # deg C or F temp of oven
self.t_h = self.t_env #deg C temp of heating element
super().__init__()
@ -516,6 +539,20 @@ class SimulatedOven(Oven):
self.start()
log.info("SimulatedOven started")
# runtime is in sped up time, start_time is actual time of day
def get_start_time(self):
return datetime.datetime.now() - datetime.timedelta(milliseconds = self.runtime * 1000 / self.speedup_factor)
def update_runtime(self):
runtime_delta = datetime.datetime.now() - self.start_time
if runtime_delta.total_seconds() < 0:
runtime_delta = datetime.timedelta(0)
self.runtime = runtime_delta.total_seconds() * self.speedup_factor
def update_target_temp(self):
self.target = self.profile.get_target_temperature(self.runtime)
def heating_energy(self,pid):
# using pid here simulates the element being on for
# only part of the time_step
@ -539,9 +576,11 @@ class SimulatedOven(Oven):
self.board.temp_sensor.simulated_temperature = self.t
def heat_then_cool(self):
now_simulator = self.start_time + datetime.timedelta(milliseconds = self.runtime * 1000)
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature() +
config.thermocouple_offset)
config.thermocouple_offset, now_simulator)
heat_on = float(self.time_step * pid)
heat_off = float(self.time_step * (1 - pid))
@ -553,7 +592,7 @@ class SimulatedOven(Oven):
if heat_on > 0:
self.heat = heat_on
log.info("simulation: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(self.p_heat * pid),
log.info("simulation: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(self.p_heat * pid),
self.t_h,
int(self.p_ho),
self.t,
@ -580,7 +619,7 @@ class SimulatedOven(Oven):
# we don't actually spend time heating & cooling during
# a simulation, so sleep.
time.sleep(self.time_step)
time.sleep(self.time_step / self.speedup_factor)
class RealOven(Oven):
@ -602,8 +641,9 @@ class RealOven(Oven):
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature() +
config.thermocouple_offset)
self.board.temp_sensor.temperature +
config.thermocouple_offset, datetime.datetime.now())
heat_on = float(self.time_step * pid)
heat_off = float(self.time_step * (1 - pid))
@ -643,6 +683,28 @@ class Profile():
def get_duration(self):
return max([t for (t, x) in self.data])
# x = (y-y1)(x2-x1)/(y2-y1) + x1
@staticmethod
def find_x_given_y_on_line_from_two_points(y, point1, point2):
if point1[0] > point2[0]: return 0 # time2 before time1 makes no sense in kiln segment
if point1[1] >= point2[1]: return 0 # Zero will crach. Negative temeporature slope, we don't want to seek a time.
x = (y - point1[1]) * (point2[0] -point1[0] ) / (point2[1] - point1[1]) + point1[0]
return x
def find_next_time_from_temperature(self, temperature):
time = 0 # The seek function will not do anything if this returns zero, no useful intersection was found
for index, point2 in enumerate(self.data):
if point2[1] >= temperature:
if index > 0: # Zero here would be before the first segment
if self.data[index - 1][1] <= temperature: # We have an intersection
time = self.find_x_given_y_on_line_from_two_points(temperature, self.data[index - 1], point2)
if time == 0:
if self.data[index - 1][1] == point2[1]: # It's a flat segment that matches the temperature
time = self.data[index - 1][0]
break
return time
def get_surrounding_points(self, time):
if time > self.get_duration():
return (None, None)
@ -685,8 +747,7 @@ class PID():
# settled on -50 to 50 and then divide by 50 at the end. This results
# in a larger PID control window and much more accurate control...
# instead of what used to be binary on/off control.
def compute(self, setpoint, ispoint):
now = datetime.datetime.now()
def compute(self, setpoint, ispoint, now):
timeDelta = (now - self.lastNow).total_seconds()
window_size = 100

Wyświetl plik

@ -33,7 +33,7 @@ class OvenWatcher(threading.Thread):
self.recording = False
self.notify_all(oven_state)
time.sleep(self.oven.time_step)
def lastlog_subset(self,maxpts=50):
'''send about maxpts from lastlog by skipping unwanted data'''
totalpts = len(self.last_log)
@ -79,6 +79,7 @@ class OvenWatcher(threading.Thread):
def notify_all(self,message):
message_json = json.dumps(message)
log.debug("sending to %d clients: %s"%(len(self.observers),message_json))
for wsock in self.observers:
if wsock:
try:

Wyświetl plik

@ -0,0 +1 @@
{"data": [[0, 200], [3600, 200], [10800, 2000], [14400, 2250], [16400, 2250], [19400, 70]], "type": "profile", "name": "test-fast"}