diff options
| author | s-ol <s+removethis@s-ol.nu> | 2024-03-08 22:39:47 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2024-03-09 00:12:11 +0000 |
| commit | a445ad7a3a9981da0d150dc8638cbd18c5af3d38 (patch) | |
| tree | b685588776b3c25233ec5bd65a18ed6296f9ceaa /simulator | |
| parent | add READMEs for each component (diff) | |
| download | t937-serial-a445ad7a3a9981da0d150dc8638cbd18c5af3d38.tar.gz t937-serial-a445ad7a3a9981da0d150dc8638cbd18c5af3d38.zip | |
simulator: implement direct control commands/loop
Diffstat (limited to 'simulator')
| -rw-r--r-- | simulator/__init__.py | 215 |
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) |
