From 4c03cfa8a69856f8d69cec3db1dc671814231f41 Mon Sep 17 00:00:00 2001 From: James Kirikland Garner Date: Fri, 23 Dec 2022 11:56:20 -0800 Subject: [PATCH 1/5] git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 35300ae..d17affe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ thumbs.db #storage/profiles #config.py .idea/* +state.json From ee70ba1667b5504f438f16b466228e627cdeb602 Mon Sep 17 00:00:00 2001 From: James Kirikland Garner Date: Fri, 23 Dec 2022 15:01:35 -0800 Subject: [PATCH 2/5] Seek is working with log, pytests added. --- Test/test-cases.json | 1 + Test/test-fast.json | 1 + Test/test_Profile.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ config.py | 9 ++++- lib/oven.py | 44 ++++++++++++++++++++++-- 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 Test/test-cases.json create mode 100644 Test/test-fast.json create mode 100644 Test/test_Profile.py diff --git a/Test/test-cases.json b/Test/test-cases.json new file mode 100644 index 0000000..10f1cb6 --- /dev/null +++ b/Test/test-cases.json @@ -0,0 +1 @@ +{"data": [[0, 200], [3600, 200], [4200, 500], [10800, 500], [14400, 2250], [16400, 2000], [19400, 2250]], "type": "profile", "name": "test-fast"} diff --git a/Test/test-fast.json b/Test/test-fast.json new file mode 100644 index 0000000..1e317c5 --- /dev/null +++ b/Test/test-fast.json @@ -0,0 +1 @@ +{"data": [[0, 200], [3600, 200], [10800, 2000], [14400, 2250], [16400, 2250], [19400, 700]], "type": "profile", "name": "test-fast"} diff --git a/Test/test_Profile.py b/Test/test_Profile.py new file mode 100644 index 0000000..b82fc8d --- /dev/null +++ b/Test/test_Profile.py @@ -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 + + diff --git a/config.py b/config.py index 7077bbb..1b5fd77 100644 --- a/config.py +++ b/config.py @@ -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 @@ -204,7 +211,7 @@ ignore_tc_too_many_errors = False # cleaned up (deleted) by the OS on boot. # The state file is written to disk every sensor_time_wait seconds (2s by default) # and is written in the same directory as config.py. -automatic_restarts = True +automatic_restarts = False automatic_restart_window = 15 # max minutes since power outage automatic_restart_state_file = os.path.abspath(os.path.join(os.path.dirname( __file__ ),'state.json')) diff --git a/lib/oven.py b/lib/oven.py index a756fe3..20624b4 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -103,7 +103,7 @@ class TempSensorSimulated(TempSensor): '''Simulates a temperature sensor ''' def __init__(self): TempSensor.__init__(self) - self.simulated_temperature = 0 + self.simulated_temperature = 255 def temperature(self): return self.simulated_temperature @@ -329,10 +329,26 @@ class Oven(threading.Thread): self.heat = 0 self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp) + @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") + else: + startat = 0 + return startat + def run_profile(self, profile, startat=0): + runtime = startat * 60 + 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() @@ -507,7 +523,7 @@ class SimulatedOven(Oven): self.R_ho = self.R_ho_noair # set temps to the temp of the surrounding environment - self.t = self.t_env # deg C temp of oven + self.t = 255 # self.t_env # deg C temp of oven self.t_h = self.t_env #deg C temp of heating element super().__init__() @@ -643,6 +659,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) From b960bb4710cdb65f76519137d74ec6131503b781 Mon Sep 17 00:00:00 2001 From: James Kirikland Garner Date: Sat, 24 Dec 2022 13:56:07 -0800 Subject: [PATCH 3/5] Fixed running seek on auto restart bug. --- config.py | 2 +- lib/oven.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 1b5fd77..79ce694 100644 --- a/config.py +++ b/config.py @@ -211,7 +211,7 @@ ignore_tc_too_many_errors = False # cleaned up (deleted) by the OS on boot. # The state file is written to disk every sensor_time_wait seconds (2s by default) # and is written in the same directory as config.py. -automatic_restarts = False +automatic_restarts = True automatic_restart_window = 15 # max minutes since power outage automatic_restart_state_file = os.path.abspath(os.path.join(os.path.dirname( __file__ ),'state.json')) diff --git a/lib/oven.py b/lib/oven.py index 20624b4..48fe985 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -339,12 +339,13 @@ class Oven(threading.Thread): startat = 0 return startat - def run_profile(self, profile, startat=0): + def run_profile(self, profile, startat=0, auto_start=False): runtime = startat * 60 - 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) + if not auto_start: + 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 @@ -484,7 +485,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, auto_start=True) self.cost = d["cost"] time.sleep(1) self.ovenwatcher.record(profile) From 3c515761e85d1822035e1a01d37019435f6f2dfa Mon Sep 17 00:00:00 2001 From: James Kirikland Garner Date: Mon, 26 Dec 2022 20:32:51 -0800 Subject: [PATCH 4/5] skip sink on API start with start time set --- config.py | 2 +- kiln-controller.py | 7 ++++++- lib/oven.py | 10 ++++++---- lib/ovenWatcher.py | 1 + 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/config.py b/config.py index 79ce694..8d2a424 100644 --- a/config.py +++ b/config.py @@ -8,7 +8,7 @@ import busio # General options ### Logging -log_level = logging.INFO +log_level = logging.DEBUG log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' ### Server diff --git a/kiln-controller.py b/kiln-controller.py index 17fc3f9..9b23c1a 100755 --- a/kiln-controller.py +++ b/kiln-controller.py @@ -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': diff --git a/lib/oven.py b/lib/oven.py index 48fe985..9eafd06 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -334,14 +334,15 @@ class Oven(threading.Thread): 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") + 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, auto_start=False): + 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 not auto_start: + if allow_seek: if self.state == 'IDLE': if config.seek_start: temp = self.board.temp_sensor.temperature() # Defined in a subclass @@ -485,7 +486,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, auto_start=True) + 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) @@ -496,6 +497,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() diff --git a/lib/ovenWatcher.py b/lib/ovenWatcher.py index 3e47e4f..3a420e5 100644 --- a/lib/ovenWatcher.py +++ b/lib/ovenWatcher.py @@ -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: From 37f2a53aec7279bfda477063125d04aea9316bf3 Mon Sep 17 00:00:00 2001 From: James Kirikland Garner Date: Fri, 30 Dec 2022 12:53:06 -0800 Subject: [PATCH 5/5] Set start temperature for simulation using sim_t_env in config. --- config.py | 2 +- lib/oven.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 8d2a424..dc257b3 100644 --- a/config.py +++ b/config.py @@ -116,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 diff --git a/lib/oven.py b/lib/oven.py index 9eafd06..f5efc42 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -103,7 +103,7 @@ class TempSensorSimulated(TempSensor): '''Simulates a temperature sensor ''' def __init__(self): TempSensor.__init__(self) - self.simulated_temperature = 255 + self.simulated_temperature = config.sim_t_env def temperature(self): return self.simulated_temperature @@ -526,7 +526,7 @@ class SimulatedOven(Oven): self.R_ho = self.R_ho_noair # set temps to the temp of the surrounding environment - self.t = 255 # 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__()