Files
Smartwave/shared/uart_comm.py
T
Ninluc 7299a50198
Build, push image, and notify Watchtower / build-image (push) Successful in 1m21s
Build, push image, and notify Watchtower / notify (push) Successful in 17s
Beginning of cooking cycle
2026-07-31 21:50:42 +02:00

163 lines
6.3 KiB
Python

import _thread
from machine import UART
import time
import ujson
class SafeUART:
def __init__(self, uart_id, tx_pin, rx_pin, baudrate=115200):
# Initialize the hardware UART channel
self.uart = UART(uart_id, baudrate=baudrate, tx=tx_pin, rx=rx_pin, timeout=10, rxbuf=1024)
# Core thread-safety assets
self.lock = _thread.allocate_lock()
self.rx_queue = []
self.buffer = b""
# Start the background data worker thread
_thread.stack_size(4096) # Cap the stack size for the UART listener
_thread.start_new_thread(self._listener_worker, ())
_thread.stack_size(0)
print(f"[UART] Thread initialized on UART{uart_id} (TX:{tx_pin}, RX:{rx_pin})")
def _listener_worker(self):
"""Worker loop that handles nested JSON structures by tracking brace depth."""
while True:
messages_found = []
with self.lock:
if self.uart.any():
chunk = self.uart.read()
if chunk is not None and isinstance(chunk, bytes):
self.buffer += chunk
# Extract complete JSON objects while accounting for nested braces
while True:
start_idx = self.buffer.find(b'{')
if start_idx == -1:
# No starting brace; clear any garbage bytes currently in buffer
self.buffer = b""
break
# Trim any leading noise before the first '{'
if start_idx > 0:
self.buffer = self.buffer[start_idx:]
# Track depth to find the matching OUTER '}'
depth = 0
in_string = False
escape = False
end_idx = -1
for i in range(len(self.buffer)):
b = self.buffer[i]
# Ignore braces inside string literals ("...")
if b == 34 and not escape: # 34 is ASCII for '"'
in_string = not in_string
elif b == 92 and in_string: # 92 is ASCII for '\'
escape = not escape
continue
elif not in_string:
if b == 123: # '{'
depth += 1
elif b == 125: # '}'
depth -= 1
if depth == 0:
end_idx = i
break
escape = False
if end_idx != -1:
# Full nested JSON object extracted safely
json_bytes = self.buffer[:end_idx + 1]
self.buffer = self.buffer[end_idx + 1:]
messages_found.append(json_bytes)
else:
# The complete outer JSON hasn't fully arrived yet; wait for next UART chunk
break
# Process valid complete frames outside the lock
for json_bytes in messages_found:
try:
decoded_str = json_bytes.decode('utf-8')
with self.lock:
self.rx_queue.append(decoded_str)
except Exception as e:
print(f"[UART Parse Error]: {e}")
time.sleep_ms(10)
def send(self, message):
"""Safely pushes strings across the serial wire from any thread context."""
if not message.endswith('\n'):
message += '\n'
data = message.encode('utf-8')
with self.lock:
self.uart.write(data)
def send_as_command(self, command: 'UARTCommand'):
"""Safely sends a structured command over UART."""
json_message = command.to_json()
self.send(json_message)
def any(self):
"""Checks if any complete messages are waiting to be read."""
with self.lock:
return len(self.rx_queue) > 0
def read(self):
"""Pulls the oldest unread string from the queue. Returns None if empty."""
with self.lock:
if self.rx_queue:
return self.rx_queue.pop(0)
return None
def read_as_command(self) -> 'UARTCommand | None':
"""Attempts to read the oldest unread string and parse it as a UARTCommand. Returns None if empty or invalid."""
raw_message = self.read()
if raw_message is not None:
cmd = UARTCommand.from_json(raw_message)
if cmd is None:
print("[UART] Impossible de traiter le message brut :", raw_message)
return cmd
return None
class UARTCommand:
"""A simple wrapper for commands sent over UART, allowing for structured data."""
def __init__(self, command_type: str, payload):
self.command_type = command_type
self.payload = payload
def to_json(self):
"""Serializes the command to a JSON string."""
return ujson.dumps({
"command_type": self.command_type,
"payload": self.payload
})
@staticmethod
def from_json(json_string: str) -> 'UARTCommand | None':
"""Deserializes a JSON string into a UARTCommand object."""
try:
# Remplacement préventif si des guillemets simples sont reçus
clean_str = json_string.replace("'", '"') if "'" in json_string else json_string
data = ujson.loads(clean_str)
if not isinstance(data, dict):
return None
return UARTCommand(data.get("command_type"), data.get("payload"))
except Exception as err:
# Affiche l'erreur exacte rencontrée par ujson (ex: syntax error)
print(f"[UARTCommand Parsing Error]: {err} -> Contenu: {json_string}")
return None
class UARTCommandType:
"""Enumeration of known UART command types."""
COOKING_PARAMS = "COOKING_PARAMS"
COOKING_STATE_UPDATE = "COOKING_STATE_UPDATE"