From 2cb6472133fc4526c664819ee93cc3851ac6d991 Mon Sep 17 00:00:00 2001 From: Kelvin on RPi Date: Fri, 9 Aug 2024 20:49:54 +0100 Subject: [PATCH] Separate out classes into separate files --- controller.py | 64 ++++++++++++ humidifier.py | 101 +++++++++++++++++++ humidifier_v3.py | 23 +++++ mock_serial.py | 43 ++++++++ shroom_controller.py | 233 +++---------------------------------------- 5 files changed, 247 insertions(+), 217 deletions(-) create mode 100644 controller.py create mode 100644 humidifier.py create mode 100644 humidifier_v3.py create mode 100644 mock_serial.py diff --git a/controller.py b/controller.py new file mode 100644 index 0000000..8b1fce2 --- /dev/null +++ b/controller.py @@ -0,0 +1,64 @@ +class Controller: + def __init__(self, humidifiers): + self.target_lower = 85 + self.target_upper = 90 + self.feedforward_coeff = 50 + self.last_toggle = 0 + + self._manual_mode = False + self.manual_on = False + self.manual_timeout = 0 + self.manual_duration = 40 + + self.humidifier_history = np.zeros(50) + self.first_sample = False + self.humidifiers = humidifiers + + @property + def manual_mode(self): + return self._manual_mode + + @manual_mode.setter + def manual_mode(self, on): + self._manual_mode = on + send_update({"status": {"manual_mode": on}}) + + def set_checked(self, s, humidifier, on): + if time.time() - self.last_toggle > 0.8: + if humidifier is HumidifierV3: + humidifier.set(s, on) + else: + humidifier.toggle(s) + self.last_toggle = time.time() + + def update(self, humidity): + if self.first_sample: + self.humidifier_history[:] = humidity + self.first_sample = False + else: + self.humidifier_history[:-1] = self.humidifier_history[1:] + self.humidifier_history[-1] = humidity + + # compensate for the slow response time by adding a little feed forward + # using the slope of the humidifier data + slope = (self.humidifier_history[-1] - self.humidifier_history[0])/self.humidifier_history.shape[0] + comp_humidity = humidity + self.feedforward_coeff*slope + + if self.manual_mode and time.time() > self.manual_timeout: + self.manual_mode = False + + if self.manual_mode: + for humidifier in self.humidifiers: + if humidifier.off and self.manual_on: + self.set_checked(s, humidifier, True) + elif humidifier.on and not self.manual_on: + self.set_checked(s, humidifier, False) + else: + if comp_humidity < self.target_lower: + for humidifier in self.humidifiers: + if humidifier.off: + self.set_checked(s, humidifier, True) + elif comp_humidity > self.target_upper: + for humidifier in self.humidifiers: + if humidifier.on: + self.set_checked(s, humidifier, False) diff --git a/humidifier.py b/humidifier.py new file mode 100644 index 0000000..64c005a --- /dev/null +++ b/humidifier.py @@ -0,0 +1,101 @@ +class Humidifier: + def __init__(self): + self.off_threshold = 0.4 + self.on_threshold = 1.9 + self.toggle_cooldown = 16 + + self._on = False + self.history = np.zeros(10) + self.switch_timeout = 0 + + @property + def on(self): + return self._on + + @on.setter + def on(self, nv): + old_on = self._on + self._on = nv + if nv: + print("send hum on") + else: + print("send hum off") + send_update({"status": {"humidifier": nv}}) + + @property + def off(self): + return not self.on + + def update(self, volts): + self.history[1:] = self.history[:-1] + self.history[0] = volts + #print(self.history) + avg = np.sum(self.history)/self.history.shape[0] + if self.on: + if avg < self.off_threshold: + self.on = False + self.switch_timeout = time.time() + 1 + else: + if avg > self.on_threshold: + self.on = True + self.switch_timeout = time.time() + 1 + + def toggle(self, s): + if time.time() > self.switch_timeout: + s.write(b"h") + s.flush() + self.switch_timeout = time.time() + self.toggle_cooldown + +# the wiring's a little different so the thresholds for detecting on/off are inverted but hopefully a lot more reliable than the original +class HumidifierV2: + def __init__(self, toggle_cmd=b"i", humidifier_id="humidifier2"): + self.on_threshold = 1.5 + self.off_threshold = 2.5 + self.toggle_cooldown = 7 + self.toggle_command = toggle_cmd + + self._on = False + self.history = np.zeros(10) + self.switch_timeout = 0 + self.humidifier_id = humidifier_id + + @property + def on(self): + return self._on + + @on.setter + def on(self, nv): + old_on = self._on + self._on = nv + if nv: + print("send {} on".format(self.humidifier_id)) + else: + print("send {} off".format(self.humidifier_id)) + send_update({"status": {self.humidifier_id: nv}}) + + @property + def off(self): + return not self.on + + def update(self, volts): + self.history[1:] = self.history[:-1] + self.history[0] = volts + #print(self.history) + avg = np.sum(self.history)/self.history.shape[0] + if self.on: + if avg > self.off_threshold: + self.on = False + self.switch_timeout = time.time() + 3 + else: + if avg < self.on_threshold: + self.on = True + self.switch_timeout = time.time() + 3 + + def toggle(self, s): + if time.time() > self.switch_timeout: + print("toggling {}".format(self.humidifier_id)) + s.write(self.toggle_command) + s.flush() + self.switch_timeout = time.time() + self.toggle_cooldown + + diff --git a/humidifier_v3.py b/humidifier_v3.py new file mode 100644 index 0000000..9bf7421 --- /dev/null +++ b/humidifier_v3.py @@ -0,0 +1,23 @@ +import time + +# this is driving a SSR that's powering a AC-powered humidifier +# we don't need voltage readings for this one +class HumidifierV3: + def __init__(self): + self.last_toggle = 0 + self.last_state = False + self.cooldown = 5 + + def set(self, s, on): + if on != self.last_state: + if (time.time() - self.last_toggle) < self.cooldown: + return + if on: + s.write(b"Z") + s.flush() + else: + s.write(b"z") + s.flush() + if self.last_state != on: + self.last_toggle = time.time() + self.last_state = on diff --git a/mock_serial.py b/mock_serial.py new file mode 100644 index 0000000..06ee398 --- /dev/null +++ b/mock_serial.py @@ -0,0 +1,43 @@ +import numpy as np +import time + +class MockSerial: + def __init__(self): + self.humidity = np.zeros(100) + self.humidifier_on = False + self.humidity[:] = 80 + self.humidity[-1] = 20 + self.humidity[0] = 20 + + def flush(self): + pass + + def write(self, msg): + if msg == b'h': + print("mock hum toggle") + self.humidifier_on = not self.humidifier_on + + def read(self, _): + t = time.time() + temp = 25 + np.sin(0.01*2*np.pi*t) + 0.5*np.sin(0.0001*2*np.pi*t + 7) + + # very janky model of humidity diffusion + # fix end conditions + for _ in range(20): + self.humidity[-1] = 0.2*20 + 0.8*self.humidity[-2] + self.humidity[0] = 20 + if self.humidifier_on: + self.humidity[20] = 2 + # use the gradient to determine the change in humidity + avg = 0.5*(self.humidity[:-2] + self.humidity[2:]) + self.humidity[1:-1] += 0.10*(avg - self.humidity[1:-1]) + #print(self.humidity) + + humidity = self.humidity[60] + np.random.random()*0.003 + if self.humidifier_on: + hv = 3.3 + else: + hv = 0.0 + return bytes("{},{},{}\n".format(humidity, temp, hv), "utf8") + + diff --git a/shroom_controller.py b/shroom_controller.py index c79796b..20884d4 100644 --- a/shroom_controller.py +++ b/shroom_controller.py @@ -7,6 +7,10 @@ import subprocess import threading import time +from humidifier import Humidifier, HumidifierV2 +from humidifier_v3 import HumidifierV3 +from controller import Controller + SERIAL_PATH = "/dev/ttyACM0" SERIAL_BAUD = 115200 @@ -25,7 +29,8 @@ def start_process(): if is_mock: process = subprocess.Popen(["/usr/bin/env", "python", "/home/kelvin/src/shroom-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) else: - process = subprocess.Popen(["ssh", "shrooms@threefortiethofonehamster.com", "/usr/bin/env", "python3", "/home/shrooms/shrooms-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + #process = subprocess.Popen(["ssh", "shrooms@threefortiethofonehamster.com", "/usr/bin/env", "python3", "/home/shrooms/shrooms-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + process = subprocess.Popen(["ssh", "shrooms@35.211.7.97", "/usr/bin/env", "python3", "/home/shrooms/shrooms-server/shroom_pipe.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) start_process() def send_update(msg): @@ -33,47 +38,9 @@ def send_update(msg): process.stdin.write(bytes(json.dumps(msg) + "\n", "utf8")) process.stdin.flush() -class MockSerial: - def __init__(self): - self.humidity = np.zeros(100) - self.humidifier_on = False - self.humidity[:] = 80 - self.humidity[-1] = 20 - self.humidity[0] = 20 - - def flush(self): - pass - - def write(self, msg): - if msg == b'h': - print("mock hum toggle") - self.humidifier_on = not self.humidifier_on - - def read(self, _): - t = time.time() - temp = 25 + np.sin(0.01*2*np.pi*t) + 0.5*np.sin(0.0001*2*np.pi*t + 7) - - # very janky model of humidity diffusion - # fix end conditions - for _ in range(20): - self.humidity[-1] = 0.2*20 + 0.8*self.humidity[-2] - self.humidity[0] = 20 - if self.humidifier_on: - self.humidity[20] = 2 - # use the gradient to determine the change in humidity - avg = 0.5*(self.humidity[:-2] + self.humidity[2:]) - self.humidity[1:-1] += 0.10*(avg - self.humidity[1:-1]) - #print(self.humidity) - - humidity = self.humidity[60] + np.random.random()*0.003 - if self.humidifier_on: - hv = 3.3 - else: - hv = 0.0 - return bytes("{},{},{}\n".format(humidity, temp, hv), "utf8") - if is_mock: - s = MockSerial() + import mock_serial + s = mock_serial.MockSerial() else: s = serial.Serial(SERIAL_PATH, SERIAL_BAUD, timeout = 0.3) print("pausing for bootloader...") @@ -87,175 +54,8 @@ def reset_serial(): s = serial.Serial(SERIAL_PATH, SERIAL_BAUD, timeout = 0.3) time.sleep(10) -class Humidifier: - def __init__(self): - self.off_threshold = 0.4 - self.on_threshold = 1.9 - self.toggle_cooldown = 7 - - self._on = False - self.history = np.zeros(10) - self.switch_timeout = 0 - - @property - def on(self): - return self._on - - @on.setter - def on(self, nv): - old_on = self._on - self._on = nv - if nv: - print("send hum on") - else: - print("send hum off") - send_update({"status": {"humidifier": nv}}) - - @property - def off(self): - return not self.on - - def update(self, volts): - self.history[1:] = self.history[:-1] - self.history[0] = volts - #print(self.history) - avg = np.sum(self.history)/self.history.shape[0] - if self.on: - if avg < self.off_threshold: - self.on = False - self.switch_timeout = time.time() + 1 - else: - if avg > self.on_threshold: - self.on = True - self.switch_timeout = time.time() + 1 - - def toggle(self, s): - if time.time() > self.switch_timeout: - s.write(b"h") - s.flush() - self.switch_timeout = time.time() + self.toggle_cooldown - -# the wiring's a little different so the thresholds for detecting on/off are inverted but hopefully a lot more reliable than the original -class HumidifierV2: - def __init__(self, toggle_cmd=b"i", humidifier_id="humidifier2"): - self.on_threshold = 1.5 - self.off_threshold = 2.5 - self.toggle_cooldown = 7 - self.toggle_command = toggle_cmd - - self._on = False - self.history = np.zeros(10) - self.switch_timeout = 0 - - @property - def on(self): - return self._on - - @on.setter - def on(self, nv): - old_on = self._on - self._on = nv - if nv: - print("send hum 2 on") - else: - print("send hum 2 off") - send_update({"status": {humidifier_id: nv}}) - - @property - def off(self): - return not self.on - - def update(self, volts): - self.history[1:] = self.history[:-1] - self.history[0] = volts - #print(self.history) - avg = np.sum(self.history)/self.history.shape[0] - if self.on: - if avg > self.off_threshold: - self.on = False - self.switch_timeout = time.time() + 1 - else: - if avg < self.on_threshold: - self.on = True - self.switch_timeout = time.time() + 1 - - def toggle(self, s): - if time.time() > self.switch_timeout: - s.write(self.toggle_command) - s.flush() - self.switch_timeout = time.time() + self.toggle_cooldown - - -class Controller: - def __init__(self): - self.target_lower = 85 - self.target_upper = 90 - self.feedforward_coeff = 50 - self.last_toggle = 0 - - self._manual_mode = False - self.manual_on = False - self.manual_timeout = 0 - self.manual_duration = 40 - - self.humidifier_history = np.zeros(50) - self.first_sample = False - - @property - def manual_mode(self): - return self._manual_mode - - @manual_mode.setter - def manual_mode(self, on): - self._manual_mode = on - send_update({"status": {"manual_mode": on}}) - - def toggle_checked(self, humidifier, s): - if time.time() - self.last_toggle > 0.8: - humidifier.toggle(s) - self.last_toggle = time.time() - - def update(self, humidifier, humidifier2, humidity): - if self.first_sample: - self.humidifier_history[:] = humidity - self.first_sample = False - else: - self.humidifier_history[:-1] = self.humidifier_history[1:] - self.humidifier_history[-1] = humidity - - # compensate for the slow response time by adding a little feed forward - # using the slope of the humidifier data - slope = (self.humidifier_history[-1] - self.humidifier_history[0])/self.humidifier_history.shape[0] - comp_humidity = humidity + self.feedforward_coeff*slope - - if self.manual_mode and time.time() > self.manual_timeout: - self.manual_mode = False - - # TODO add in support for manual control of second humidifier - if self.manual_mode: - if humidifier.off and self.manual_on: - self.toggle_checked(humidifier, s) - elif humidifier.on and not self.manual_on: - self.toggle_checked(humidifier, s) - if humidifier2.off and self.manual_on: - self.toggle_checked(humidifier2, s) - elif humidifier2.on and not self.manual_on: - self.toggle_checked(humidifier2, s) - else: - if comp_humidity < self.target_lower: - if humidifier.off: - self.toggle_checked(humidifier, s) - if humidifier2.off: - self.toggle_checked(humidifier2, s) - elif comp_humidity > self.target_upper: - if humidifier.on: - self.toggle_checked(humidifier, s) - if humidifier2.on: - self.toggle_checked(humidifier2, s) - -humidifier = HumidifierV2(b"h", "humidifier") -humidifier2 = HumidifierV2() -controller = Controller() +humidifier = HumidifierV3() +controller = Controller([humidifier]) exiting = False # run thread to process data from process's stdout @@ -346,13 +146,12 @@ try: parts = resp.split(b",") humidity = float(parts[0]) temp = float(parts[1]) - volts = float(parts[2]) - volts2 = float(parts[3]) + #volts = float(parts[2]) + #volts2 = float(parts[3]) + #print(parts) try: - humidifier.update(volts) - humidifier2.update(volts2) - controller.update(humidifier, humidifier2, humidity) + controller.update(humidity) if frame_num == 0: #print(humidity, temp, volts) @@ -361,8 +160,8 @@ try: "time": int(now*1000), "temp": temp, "hum": humidity, - "hv": volts, - "hv2": volts2, + "hv": 1 if humidifier.on else 0, + #"hv2": volts2, } } send_update(update)