initial switch to websocket

This commit is contained in:
Mikkeli Matlock
2026-01-26 16:50:52 +09:00
parent 62eaaff88e
commit d6ea28163e
11 changed files with 976 additions and 35 deletions

View File

@@ -25,6 +25,9 @@ class ArduinoService:
"gear": re.compile(r"GEAR:\s*(\d+)", re.IGNORECASE),
}
# ACK pattern: "ACK:CMD:STATUS" or "ACK:CMD:STATUS:extra"
ACK_PATTERN = re.compile(r"ACK:(\w+):(\w+)(?::(.*))?")
def __init__(
self,
port: str = "/dev/ttyUSB0",
@@ -42,6 +45,55 @@ class ArduinoService:
self._thread: threading.Thread | None = None
self._lock = threading.Lock()
# Callbacks for push-based updates
self._on_data_callback: callable | None = None
self._on_ack_callback: callable | None = None
# Serial port handle for sending commands
self._serial: Any = None
self._serial_lock = threading.Lock()
def set_on_data(self, callback: callable | None):
"""Set callback for new telemetry data. Called with data dict."""
self._on_data_callback = callback
def set_on_ack(self, callback: callable | None):
"""Set callback for ACK responses. Called with (cmd, status, extra)."""
self._on_ack_callback = callback
def send_command(self, cmd: str, params: dict | None = None) -> bool:
"""Send a command to Arduino via serial.
Format: "CMD:NAME:PARAM1:PARAM2..." followed by newline
Args:
cmd: Command name (e.g., "HORN", "LIGHT")
params: Optional parameters dict
Returns:
True if sent successfully, False if serial unavailable
"""
with self._serial_lock:
if self._serial is None or not self._connected:
print(f"[Arduino] Cannot send command, not connected")
return False
try:
# Build command string
parts = ["CMD", cmd.upper()]
if params:
for key, val in params.items():
parts.append(f"{key}={val}")
line = ":".join(parts) + "\n"
self._serial.write(line.encode("utf-8"))
self._serial.flush()
print(f"[Arduino] Sent: {line.strip()}")
return True
except Exception as e:
print(f"[Arduino] Failed to send command: {e}")
return False
@property
def connected(self) -> bool:
return self._connected
@@ -100,6 +152,9 @@ class ArduinoService:
return
try:
# Store serial handle for send_command()
with self._serial_lock:
self._serial = ser
self._connected = True
print(f"[Arduino] Connected to {self.port} @ {self.baudrate} baud")
@@ -109,6 +164,14 @@ class ArduinoService:
if not line:
continue
# Check for ACK responses first
ack_match = self.ACK_PATTERN.match(line)
if ack_match:
cmd, status, extra = ack_match.groups()
if self._on_ack_callback:
self._on_ack_callback(cmd, status, extra)
continue
data = self._parse_line(line)
if data:
data["time"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
@@ -120,12 +183,18 @@ class ArduinoService:
self._latest["time"] = data["time"]
self._buffer.append(self._latest.copy())
# Invoke callback with new data
if self._on_data_callback:
self._on_data_callback(self._latest.copy())
except serial.SerialException as e:
print(f"[Arduino] Serial error: {e}")
break
finally:
self._connected = False
with self._serial_lock:
self._serial = None
ser.close()
def _parse_line(self, line: str) -> dict[str, Any] | None:
@@ -172,4 +241,9 @@ class ArduinoService:
with self._lock:
self._latest = data
self._buffer.append(data)
# Invoke callback with new data
if self._on_data_callback:
self._on_data_callback(data)
time.sleep(0.5) # 2Hz stub updates