aboutsummaryrefslogtreecommitdiffstats
path: root/simulator
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2024-03-08 22:39:47 +0000
committers-ol <s+removethis@s-ol.nu>2024-03-09 00:12:11 +0000
commita445ad7a3a9981da0d150dc8638cbd18c5af3d38 (patch)
treeb685588776b3c25233ec5bd65a18ed6296f9ceaa /simulator
parentadd READMEs for each component (diff)
downloadt937-serial-a445ad7a3a9981da0d150dc8638cbd18c5af3d38.tar.gz
t937-serial-a445ad7a3a9981da0d150dc8638cbd18c5af3d38.zip
simulator: implement direct control commands/loop
Diffstat (limited to 'simulator')
-rw-r--r--simulator/__init__.py215
1 files changed, 164 insertions, 51 deletions
diff --git a/simulator/__init__.py b/simulator/__init__.py
index c8ea719..3924499 100644
--- a/simulator/__init__.py
+++ b/simulator/__init__.py
@@ -1,74 +1,187 @@
+import random
import serial
-import sys
import time
+import sys
+
+ADDR_CTRL = bytes([0xDB, 0x96])
+ADDR_OVEN = bytes([0xAA, 0x55])
+
+# used by oven
+FUNC_O_CONNECT = 0x01
+FUNC_O_TEMPS = 0x02
+FUNC_O_READ = 0x03
+FUNC_O_ACK_WRITE = 0x06
+
+# used by CTRL
+FUNC_C_PWM_HEAT = 0x08
+FUNC_C_PWM_COOL = 0x0D
+FUNC_C_STARTSTOP = 0x10
+FUNC_C_REQ_READ = 0x11
+FUNC_C_WRITE = 0x20
+FUNC_C_ACK_CONNECT = 0xF1
+FUNC_C_ACK_READ = 0xF2
+
+# for STARSTOP, REQUEST_READ
+ARG_STARTSTOP_START = 0x11
+ARG_STARTSTOP_STOP = 0x22
+ARG_REQUEST_READ = 0x11
+
+# for CONNECT / ACK_CONNECT
+ARG_CONNECT_CONNECTING = bytes([0x01, 0x00])
+ARG_CONNECT_CONNECTED = bytes([0x02, 0x00])
+
+# for *_PWM
+ARG_PWM_ON = 0x44
+ARG_PWM_OFF = 0x88
+
+
+def ru16(b):
+ assert len(b) == 2
+ return b[0] << 8 | b[1]
-ADDR_CTRL = bytes([0xdb, 0x96])
-ADDR_OVEN = bytes([0xaa, 0x55])
-def u16(b):
- assert len(b) == 2
- return b[0] << 8 | b[1]
+def wu16(b):
+ b = int(b)
+ hi, lo = b >> 8, b & 0xFF
+ return bytes([hi, lo])
+
+
+def pct(n):
+ if n is None:
+ return "off"
+
+ return str(int(n * 100)) + "%"
+
class MessageParser:
- def __init__(self):
- self.buffer = bytes()
+ def __init__(self):
+ self.buffer = bytes()
- def encode(self, addr, func, data):
- msg = addr + bytes([func, len(data)]) + data
- chk = sum(msg)
- return msg + bytes([chk >> 8, chk & 0xff])
+ def encode(self, addr, func, data):
+ msg = addr + bytes([func, len(data)]) + data
+ chk = sum(msg)
+ return msg + bytes([chk >> 8, chk & 0xFF])
- def try_read_message(self, data):
- if not data:
- return
- self.buffer += data
+ def try_read(self, data):
+ if data:
+ self.buffer += data
- if len(self.buffer) < 4 + 2:
- return
+ while len(self.buffer) >= 4 + 2:
+ data_len = self.buffer[3]
+ msg_len = 4 + data_len + 2
+ if len(self.buffer) < msg_len:
+ return
- data_len = self.buffer[3]
- msg_len = 4 + data_len + 2
- if len(self.buffer) < msg_len:
- return
+ (msg, self.buffer) = (self.buffer[:msg_len], self.buffer[msg_len:])
+ assert sum(msg[:-2]) == ru16(msg[-2:])
+ assert msg[:2] == ADDR_OVEN
- (msg, self.buffer) = (self.buffer[:msg_len], self.buffer[msg_len:])
- assert sum(msg[:-2]) == u16(msg[-2:])
- assert msg[:2] == ADDR_OVEN
+ func = msg[2]
+ data = msg[4 : 4 + data_len]
- func = msg[2]
- data = msg[4:4+data_len]
+ yield (func, data)
- return (func, data)
-def run(port):
- parser = MessageParser()
+class DummyOven:
+ def __init__(self):
+ self.temps = [24, 24, 24]
+ self.heat = None
+ self.cool = None
- print("broadcasting connection...")
- while True:
- port.write(parser.encode(ADDR_CTRL, 0x01, bytes([0x01, 0x00])))
- time.sleep(1)
- msg = parser.try_read_message(port.read(port.in_waiting))
+ def step(self):
+ if self.heat is not None:
+ targets = [350, 350, 100]
+ rate = 12 * self.heat
+ elif self.cool is not None:
+ rate = -8 * self.cool
+ else:
+ rate = -4
- if msg:
- if msg[0] == 0xf1 and msg[1] == bytes([0x02, 0x00]):
- break
+ for i, temp in enumerate(self.temps):
+ mult = random.uniform(0.7, 1) if i < 2 else 0.01
+ temp = temp + rate * mult
+ self.temps[i] = max(24, min(temp, 350))
- print("unexpected message", msg)
+ def print_state(self):
+ print(f"heat PWM: {pct(self.heat)}, cool PWM: {pct(self.cool)}")
- print("accepting connection...")
- port.write(parser.encode(ADDR_CTRL, 0x01, bytes([0x02, 0x00])))
+ def start(self, i):
+ print(f"starting profile {i+1}")
- while True:
- port.write(parser.encode(ADDR_CTRL, 0x02, bytes([0, 26])*3))
- time.sleep(1)
- msg = parser.try_read_message(port.read(port.in_waiting))
+ def stop(self, i):
+ print(f"stopping profile {i+1}")
- if msg:
- print(msg)
- break
+
+def wait_connect(port, parser):
+ print("broadcasting connection...")
+ while True:
+ port.write(parser.encode(ADDR_CTRL, FUNC_O_CONNECT, ARG_CONNECT_CONNECTING))
+ time.sleep(1)
+
+ for msg in parser.try_read(port.read(port.in_waiting)):
+ if msg[0] == FUNC_C_ACK_CONNECT and msg[1] == ARG_CONNECT_CONNECTED:
+ return
+ else:
+ print("unexpected message", msg)
+
+
+def run(port):
+ parser = MessageParser()
+ wait_connect(port, parser)
+
+ print("accepting connection...")
+ port.write(parser.encode(ADDR_CTRL, FUNC_O_CONNECT, ARG_CONNECT_CONNECTED))
+
+ oven = DummyOven()
+
+ while True:
+ oven.step()
+ port.write(
+ parser.encode(
+ ADDR_CTRL, FUNC_O_TEMPS, b"".join(wu16(t) for t in oven.temps)
+ )
+ )
+ time.sleep(1)
+
+ for msg in parser.try_read(port.read(port.in_waiting)):
+ if msg[0] == FUNC_C_PWM_HEAT or msg[0] == FUNC_C_PWM_COOL:
+ pwm, state = msg[1]
+ if state == ARG_PWM_ON:
+ pwm = pwm / 255
+ elif state == ARG_PWM_OFF:
+ pwm = None
+ else:
+ print("invalid PWM mode")
+
+ if msg[0] == FUNC_C_PWM_HEAT:
+ oven.heat = pwm
+ oven.cool = None
+ else:
+ oven.cool = pwm
+ oven.print_state()
+
+ elif msg[0] == FUNC_C_STARTSTOP:
+ profile, mode = msg[1]
+ if mode == ARG_STARTSTOP_START:
+ oven.start(profile)
+ elif mode == ARG_STARTSTOP_STOP:
+ oven.stop(profile)
+ else:
+ print("invalid STARTSTOP mode")
+
+ elif msg[0] == FUNC_C_REQ_READ:
+ profile, mode = msg[1]
+ pass
+
+ elif msg[0] == FUNC_C_WRITE:
+ pass
+
+ else:
+ print("unexpected message", msg)
if __name__ == "__main__":
- port = sys.argv[-1]
- with serial.Serial(port, timeout=1) as ser:
- run(ser)
+ port = sys.argv[-1]
+ print(f"starting Dummy Oven on port {port}")
+ with serial.Serial(port, baudrate=38400, timeout=1) as ser:
+ run(ser)