#!/usr/bin/env python3 """ Test script for app_photomagnetic communication protocol. Frame format (SimpleProtocal, per DTS config): H0(0x7E) H1(0xE7) CMD(1B) LEN(1B) DATA[0..N] CRC_LO CRC_HI CRC = CRC-16 Modbus over CMD+LEN+DATA, LSB-MSB order Commands: 0x00 TEMP D→H (2 bytes: val1 val2) 0x01 GET_ID H→D / D→H (20 bytes HWID) 0x02 RUNNING_STATE H→D (1 byte: 0=Standby 1=Running) 0x03 W_HEATING_STATE H→D (1 byte: 0=stop, >0=target °C) 0x04 R_HEATING_STATE H→D / D→H (3 bytes: state val1 val2) 0x64 R_TEMP_POLE_NTC H→D (1 byte index) / D→H (2 bytes: val1 val2) 0x65 R_HEATING_PAD_NTC H→D (0 bytes) / D→H (2 bytes: val1 val2) Usage: python test.py /dev/ttyUSB0 --monitor python test.py /dev/ttyUSB0 --heating 42 python test.py /dev/ttyUSB0 --read-pole-ntc 3 python test.py /dev/ttyUSB0 --read-heating-ntc """ import argparse import sys import time try: import serial except ImportError: print("Please install pyserial: pip install pyserial", file=sys.stderr) sys.exit(1) # ── Protocol constants ──────────────────────────────────────── HEADER = b"\x7e\xe7" HEADER_SIZE = len(HEADER) CMD_TEMP = 0x00 CMD_GET_ID = 0x01 CMD_RUNNING_STATE = 0x02 CMD_W_HEATING = 0x03 CMD_R_HEATING = 0x04 CMD_READ_POLE_NTC = 0x64 CMD_READ_HEATING_NTC = 0x65 HEATING_STATE_NAMES = {0: "OFF", 1: "ON"} # ── CRC-16 Modbus ───────────────────────────────────────────── def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc & 0xFFFF # ── Frame helpers ───────────────────────────────────────────── def build_frame(cmd: int, payload: bytes = b"") -> bytes: body = bytes([cmd, len(payload)]) + payload crc = crc16_modbus(body) return HEADER + body + bytes([crc & 0xFF, (crc >> 8) & 0xFF]) def parse_frame(frame: bytes) -> tuple[int, bytes] | None: if len(frame) < HEADER_SIZE + 2 + 2: return None if frame[:HEADER_SIZE] != HEADER: return None cmd = frame[HEADER_SIZE] length = frame[HEADER_SIZE + 1] expected = HEADER_SIZE + 2 + length + 2 if len(frame) != expected: return None data = frame[HEADER_SIZE + 2 : HEADER_SIZE + 2 + length] crc_body = frame[HEADER_SIZE : HEADER_SIZE + 2 + length] actual_crc = crc16_modbus(crc_body) wire_crc = (frame[expected - 1] << 8) | frame[expected - 2] if actual_crc != wire_crc: return None return cmd, data # ── Serial reader ───────────────────────────────────────────── class ProtocolReader: def __init__(self, ser: serial.Serial): self._ser = ser self._buf = bytearray() def read_frame(self, timeout: float = 1.0) -> bytes | None: deadline = time.time() + timeout while time.time() < deadline: if len(self._buf) >= 2: idx = self._buf.find(HEADER) if idx > 0: del self._buf[:idx] elif idx < 0: self._buf.clear() if len(self._buf) >= HEADER_SIZE and self._buf[:HEADER_SIZE] == HEADER: if len(self._buf) >= HEADER_SIZE + 2 + 2: length = self._buf[HEADER_SIZE + 1] total = HEADER_SIZE + 2 + length + 2 if len(self._buf) >= total: frame = bytes(self._buf[:total]) del self._buf[:total] return frame elif len(self._buf) >= HEADER_SIZE: del self._buf[0] continue try: chunk = self._ser.read(self._ser.in_waiting or 1) if chunk: self._buf.extend(chunk) except (serial.SerialTimeoutException, serial.SerialException): pass return None def _read_reply(ser: serial.Serial, timeout: float = 2.0) -> tuple[int, bytes] | None: reader = ProtocolReader(ser) frame = reader.read_frame(timeout) if frame is None: print("✗ No response (timeout)") return None result = parse_frame(frame) if result is None: print(f"✗ Invalid frame: {frame.hex()}") return None return result def _temp_from_data(data: bytes) -> float: return data[0] + data[1] / 100.0 if len(data) >= 2 else 0.0 # ── Command handlers ────────────────────────────────────────── def cmd_get_id(ser: serial.Serial, timeout: float = 2.0): print("→ Sending GET_ID...") ser.write(build_frame(CMD_GET_ID)) r = _read_reply(ser, timeout) if r is None: return cmd, data = r if cmd != CMD_GET_ID: print(f"✗ Unexpected cmd=0x{cmd:02X}") return print(f"✓ Device ID ({len(data)} bytes): {data.hex(' ')}") def cmd_set_state(ser: serial.Serial, state: int): names = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"} name = names.get(state, "Unknown") print(f"→ Setting state: {name} (0x{state:02X})") ser.write(build_frame(CMD_RUNNING_STATE, bytes([state]))) print("✓ Sent") def cmd_write_heating(ser: serial.Serial, target: int): if target == 0: print("→ Stopping heating") else: print(f"→ Starting heating to {target}°C") ser.write(build_frame(CMD_W_HEATING, bytes([target]))) print("✓ Sent") def cmd_read_heating(ser: serial.Serial, timeout: float = 2.0): print("→ Querying heating state...") ser.write(build_frame(CMD_R_HEATING)) r = _read_reply(ser, timeout) if r is None: return cmd, data = r if cmd != CMD_R_HEATING: print(f"✗ Unexpected cmd=0x{cmd:02X}") return if len(data) < 3: print(f"✗ Short data: {data.hex()}") return state = HEATING_STATE_NAMES.get(data[0], f"Unknown({data[0]})") print(f"✓ Heating: {state}, temp={_temp_from_data(data[1:3]):.2f}°C") def cmd_read_pole_ntc(ser: serial.Serial, index: int, timeout: float = 2.0): print(f"→ Reading pole NTC #{index}...") ser.write(build_frame(CMD_READ_POLE_NTC, bytes([index]))) r = _read_reply(ser, timeout) if r is None: return cmd, data = r if cmd != CMD_READ_POLE_NTC: print(f"✗ Unexpected cmd=0x{cmd:02X}") return if len(data) < 2: print(f"✗ Short data: {data.hex()}") return print(f"✓ Pole NTC #{index}: {_temp_from_data(data):.2f}°C") def cmd_read_heating_ntc(ser: serial.Serial, timeout: float = 2.0): print("→ Reading heating pad NTC...") ser.write(build_frame(CMD_READ_HEATING_NTC)) r = _read_reply(ser, timeout) if r is None: return cmd, data = r if cmd != CMD_READ_HEATING_NTC: print(f"✗ Unexpected cmd=0x{cmd:02X}") return if len(data) < 2: print(f"✗ Short data: {data.hex()}") return print(f"✓ Heating pad NTC: {_temp_from_data(data):.2f}°C") def cmd_monitor(ser: serial.Serial): print("Monitoring... (Ctrl+C to stop)") reader = ProtocolReader(ser) try: while True: frame = reader.read_frame(timeout=0.5) if frame is None: continue result = parse_frame(frame) if result is None: print(f"⚠ Bad frame: {frame.hex()}") continue cmd, data = result if cmd == CMD_TEMP: t = _temp_from_data(data) print(f"🌡 Temp: {t:.2f}°C (raw: {data.hex(' ')})") elif cmd == CMD_GET_ID: print(f"🆔 Device ID: {data.hex(' ')}") elif cmd == CMD_RUNNING_STATE: print(f"🔁 Running state: 0x{data.hex()}") elif cmd == CMD_R_HEATING: if len(data) >= 3: state = HEATING_STATE_NAMES.get(data[0], f"?{data[0]}") t = _temp_from_data(data[1:3]) print(f"🔥 Heating: {state}, temp={t:.2f}°C") else: print(f"🔥 Heating: {data.hex()}") elif cmd == CMD_READ_POLE_NTC: t = _temp_from_data(data) print(f"📡 Pole NTC reply: {t:.2f}°C") elif cmd == CMD_READ_HEATING_NTC: t = _temp_from_data(data) print(f"🔥 Heating pad NTC reply: {t:.2f}°C") else: print(f"📦 cmd=0x{cmd:02X} data={data.hex()}") except KeyboardInterrupt: print("\nDone.") def cmd_loop(ser: serial.Serial, count: int = 0): i = 0 print("Looping GET_ID... (Ctrl+C to stop)") try: while count == 0 or i < count: cmd_get_id(ser, timeout=1.0) i += 1 time.sleep(0.5) except KeyboardInterrupt: print("\nDone.") # ── Main ────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="Test script for app_photomagnetic comm protocol" ) parser.add_argument("port", help="Serial port") parser.add_argument("--baud", type=int, default=115200) parser.add_argument("--get-id", action="store_true") parser.add_argument( "--state", type=lambda x: int(x, 0), metavar="N", help="Set running state") parser.add_argument( "--heating", type=int, metavar="TEMP", help="Write heating target (0=stop)") parser.add_argument( "--read-heating", action="store_true", help="Read heating state + temp") parser.add_argument( "--read-pole-ntc", type=int, metavar="IDX", help="Read pole NTC by index (0..7)") parser.add_argument( "--read-heating-ntc", action="store_true", help="Read heating pad NTC temperature") parser.add_argument("--monitor", action="store_true") parser.add_argument( "--loop", type=int, nargs="?", const=0, metavar="N", help="Loop GET_ID N times") parser.add_argument( "--raw", type=lambda x: bytes.fromhex(x), metavar="HEX", help="Send raw CMD+DATA") args = parser.parse_args() ser = serial.Serial(args.port, args.baud, timeout=0.1) print(f"Connected to {args.port} @ {args.baud} baud") try: if args.get_id: cmd_get_id(ser) elif args.state is not None: cmd_set_state(ser, args.state) elif args.heating is not None: cmd_write_heating(ser, args.heating) elif args.read_heating: cmd_read_heating(ser) elif args.read_pole_ntc is not None: cmd_read_pole_ntc(ser, args.read_pole_ntc) elif args.read_heating_ntc: cmd_read_heating_ntc(ser) elif args.monitor: cmd_monitor(ser) elif args.loop is not None: cmd_loop(ser, args.loop) elif args.raw is not None: payload = args.raw c = payload[0] d = payload[1:] if len(payload) > 1 else b"" print(f"→ Sending CMD=0x{c:02X} data={d.hex()}") ser.write(build_frame(c, d)) print("✓ Sent") else: print("Use --get-id, --state, --heating, --read-heating, " "--read-pole-ntc, --read-heating-ntc, --monitor, --loop, --raw") finally: ser.close() if __name__ == "__main__": main()