Compare commits
3 Commits
9e078490dd
...
7299a50198
| Author | SHA1 | Date | |
|---|---|---|---|
| 7299a50198 | |||
| 8ac5db22c1 | |||
| b12296bf0e |
@@ -1,6 +1,9 @@
|
|||||||
import _thread
|
import _thread
|
||||||
from machine import Pin
|
from machine import Pin
|
||||||
from shared import get_lora, get_uart, deviceTypes, config
|
from shared import get_lora, get_uart, deviceTypes, config, cookingState
|
||||||
|
from shared.uart_comm import UARTCommand, UARTCommandType
|
||||||
|
from shared.sensors import RGBLED
|
||||||
|
from shared.logging import log
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# --- Configuration Matérielle ---
|
# --- Configuration Matérielle ---
|
||||||
@@ -19,6 +22,11 @@ except Exception:
|
|||||||
lora = get_lora()
|
lora = get_lora()
|
||||||
lora.configure(freq=868.1, sf=7)
|
lora.configure(freq=868.1, sf=7)
|
||||||
|
|
||||||
|
# --- Création des lEDs RGB ---
|
||||||
|
magnetron_led = RGBLED(red_pin=48, green_pin=47, blue_pin=33)
|
||||||
|
magnetron_led.color = RGBLED.WHITE_YELLOW
|
||||||
|
magnetron_led.off()
|
||||||
|
|
||||||
print(f"ESP32 initialisé avec l'ID : '{DEVICE_ID}' (Type : {deviceTypes.DEVICE_TYPES['MICROWAVE']})")
|
print(f"ESP32 initialisé avec l'ID : '{DEVICE_ID}' (Type : {deviceTypes.DEVICE_TYPES['MICROWAVE']})")
|
||||||
|
|
||||||
def heartbeat_loop():
|
def heartbeat_loop():
|
||||||
@@ -53,17 +61,69 @@ uart_device = get_uart(uart_id=1, tx_pin=46, rx_pin=45)
|
|||||||
# Lancer la boucle de heartbeat dans un thread séparé
|
# Lancer la boucle de heartbeat dans un thread séparé
|
||||||
_thread.start_new_thread(heartbeat_loop, ())
|
_thread.start_new_thread(heartbeat_loop, ())
|
||||||
|
|
||||||
|
# Cooking parameters
|
||||||
|
cooking_state = None
|
||||||
|
def cooking_state_temperature_provider():
|
||||||
|
return 22.0, 29.0 # TODO Remplacer par la lecture réelle de la température du plat et de l'air ambiant
|
||||||
|
|
||||||
|
def cooking_state_on_state_change(state):
|
||||||
|
print(f"[Main] Cooking state changed to: {state.state}")
|
||||||
|
|
||||||
|
# Send to the Wifi board the current state
|
||||||
|
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state}))
|
||||||
|
|
||||||
|
if state.paused or state.state == cookingState.CookingStates.DONE:
|
||||||
|
magnetron_led.off()
|
||||||
|
else:
|
||||||
|
magnetron_led.on()
|
||||||
|
|
||||||
|
|
||||||
|
if state.state == cookingState.CookingStates.COOKING:
|
||||||
|
pass
|
||||||
|
if state.state == cookingState.CookingStates.STIRRING_REQUIRED:
|
||||||
|
pass
|
||||||
|
if state.state == cookingState.CookingStates.DONE:
|
||||||
|
global cooking_state
|
||||||
|
cooking_state = None
|
||||||
|
if state.state == cookingState.CookingStates.ALERT:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cooking_state_on_refresh(state):
|
||||||
|
# TODO Show screen information
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# --- MAIN APPLICATION THREAD ---
|
# --- MAIN APPLICATION THREAD ---
|
||||||
print("[Main] Main execution path active.")
|
print("[Main] Main execution path active.")
|
||||||
while True:
|
while True:
|
||||||
# 1. Listen for incoming UART serial packets from the WROOM board
|
# 1. Listen for incoming UART serial packets from the WROOM board
|
||||||
while uart_device.any():
|
while uart_device.any():
|
||||||
command = uart_device.read()
|
command = uart_device.read_as_command()
|
||||||
print(f"[Main] Received command from WiFi Board: {command}")
|
if command:
|
||||||
|
print(f"[Main] Received command from WiFi Board: {command.command_type}")
|
||||||
|
if command.command_type == UARTCommandType.COOKING_PARAMS:
|
||||||
|
# Handle cooking parameters command
|
||||||
|
params = command.payload
|
||||||
|
print(f"[Main] Cooking parameters received: {params}")
|
||||||
|
cooking_state = cookingState.CookingState(
|
||||||
|
cook_time=params["cook_time"],
|
||||||
|
power_level=params["power_level"],
|
||||||
|
target_temp=params["target_temp"]
|
||||||
|
)
|
||||||
|
cooking_state.set_temperature_provider(cooking_state_temperature_provider)
|
||||||
|
cooking_state.set_state_change_callback(cooking_state_on_state_change)
|
||||||
|
cooking_state.set_refresh_callback(cooking_state_on_refresh)
|
||||||
|
time.sleep_ms(20) # Before sending back right away
|
||||||
|
cooking_state_on_state_change(cooking_state)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[Main] Unknown command type received: {command.command_type}")
|
||||||
|
|
||||||
# uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
|
# uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
|
||||||
|
|
||||||
# 2. Send local metrics over the wire to the WiFi board every few seconds
|
# Cooking State Update
|
||||||
# uart_device.send("Data Pack: LoRa Link RSSI -72dBm")
|
if cooking_state:
|
||||||
|
cooking_state.update_tick()
|
||||||
|
print(f"[Main] Cooking state : State : {cooking_state.state}, Temperature: {cooking_state.current_dish_temp}, Paused: {cooking_state.paused}, Remaining Time: {cooking_state.get_remaining_time():.2f}s, Estimated Remaining Time: {cooking_state.get_remaining_time_estimation():.2f}s")
|
||||||
|
|
||||||
time.sleep_ms(200)
|
time.sleep_ms(200)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# This file is executed on every boot (including wake-boot from deepsleep)
|
# This file is executed on every boot (including wake-boot from deepsleep)
|
||||||
import esp
|
import esp
|
||||||
|
from machine import Pin
|
||||||
esp.osdebug(True)
|
esp.osdebug(True)
|
||||||
#import webrepl
|
#import webrepl
|
||||||
#webrepl.start()
|
#webrepl.start()
|
||||||
@@ -17,3 +18,7 @@ def do_connect(ssid, pwd):
|
|||||||
|
|
||||||
# Attempt to connect to WiFi network
|
# Attempt to connect to WiFi network
|
||||||
do_connect("Smartwave-1", 'Smartwave-prot-1')
|
do_connect("Smartwave-1", 'Smartwave-prot-1')
|
||||||
|
|
||||||
|
# Set PIN 27 as GND for the temperature sensor (MLX90614)
|
||||||
|
sensor_gnd = Pin(27, Pin.OUT)
|
||||||
|
sensor_gnd.value(0)
|
||||||
@@ -2,7 +2,10 @@ import _thread
|
|||||||
import select
|
import select
|
||||||
from machine import Pin, I2C
|
from machine import Pin, I2C
|
||||||
from sensors import temperature_sensor
|
from sensors import temperature_sensor
|
||||||
from shared import get_mqtt_client, get_uart, config, payloads
|
from shared import get_mqtt_client, get_uart, config, payloads, cookingState
|
||||||
|
from shared.uart_comm import UARTCommand, UARTCommandType
|
||||||
|
from shared.sensors import RGBLED
|
||||||
|
from shared.logging import log
|
||||||
import time
|
import time
|
||||||
import ujson as json
|
import ujson as json
|
||||||
import sys
|
import sys
|
||||||
@@ -23,6 +26,9 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
DEVICE_ID = "ESP32_Inconnu"
|
DEVICE_ID = "ESP32_Inconnu"
|
||||||
|
|
||||||
|
# --- Cooking State ---
|
||||||
|
cooking_state = None # This will hold the current cooking state if any
|
||||||
|
|
||||||
# --- MQTT SETUP ---
|
# --- MQTT SETUP ---
|
||||||
MQTT_CA_FILE = "/certs/ca.crt"
|
MQTT_CA_FILE = "/certs/ca.crt"
|
||||||
|
|
||||||
@@ -59,16 +65,25 @@ def on_mqtt_message(message):
|
|||||||
# Handle cooking messages
|
# Handle cooking messages
|
||||||
elif message['topic'] == config.MQTT_TOPIC_COOKING and payload_data and payload_data["id_microwave"] == DEVICE_ID:
|
elif message['topic'] == config.MQTT_TOPIC_COOKING and payload_data and payload_data["id_microwave"] == DEVICE_ID:
|
||||||
# Cooking sensors init request
|
# Cooking sensors init request
|
||||||
if not "cook_time_seconds" in payload_data:
|
if not "cook_time" in payload_data:
|
||||||
print("[MQTT Thread] Cooking sensors init received from the orchestrator")
|
print("[MQTT Thread] Cooking sensors init received from the orchestrator")
|
||||||
obj_temp = temperature_sensor.read_object_temp()
|
obj_temp = mlx_temperature_sensor.read_object_temp()
|
||||||
amb_temp = temperature_sensor.read_ambient_temp()
|
amb_temp = mlx_temperature_sensor.read_ambient_temp()
|
||||||
queue_publish(config.MQTT_TOPIC_SENSOR, payloads.mqtt_sensor_data(DEVICE_ID, obj_temp, amb_temp))
|
queue_publish(config.MQTT_TOPIC_SENSOR, payloads.mqtt_sensor_data(DEVICE_ID, obj_temp, amb_temp))
|
||||||
# Received cooking parameters from the orchestrator
|
# Received cooking parameters from the orchestrator
|
||||||
else:
|
else:
|
||||||
print("[MQTT Thread] Cooking parameters received from the orchestrator:", payload_data)
|
print("[MQTT Thread] Cooking parameters received from the orchestrator:", payload_data)
|
||||||
# Here you would handle the cooking parameters, e.g., start a cooking process
|
global cooking_state
|
||||||
# For now, we just print them
|
cooking_state = cookingState.CookingState(
|
||||||
|
cook_time=payload_data["cook_time"],
|
||||||
|
power_level=payload_data["power_level"],
|
||||||
|
target_temp=payload_data["target_temp"]
|
||||||
|
)
|
||||||
|
cooking_state.set_state_change_callback(on_cooking_state_change)
|
||||||
|
cooking_state.set_state(cookingState.CookingStates.IDLE) # Set initial state to IDLE
|
||||||
|
# Send to the LoRa board the cooking parameters
|
||||||
|
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_PARAMS, payload_data))
|
||||||
|
print("[MQTT Thread] Cooking parameters sent to LoRa board.")
|
||||||
|
|
||||||
print("[MQTT Thread] Message processing complete.")
|
print("[MQTT Thread] Message processing complete.")
|
||||||
|
|
||||||
@@ -135,39 +150,47 @@ def mqtt_background_thread():
|
|||||||
pass
|
pass
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
# --- UART BACKGROUND THREAD ---
|
# UART
|
||||||
def uart_background_thread():
|
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
||||||
"""Background UART worker handling all serial operations safely."""
|
|
||||||
print("[Thread] Background UART worker started.")
|
|
||||||
|
|
||||||
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
# Cooking Cycle
|
||||||
|
cooking_state = None
|
||||||
|
def on_received_cooking_state_update(new_state):
|
||||||
|
global cooking_state
|
||||||
|
if cooking_state is None:
|
||||||
|
print("[Main] No active cooking state to update.")
|
||||||
|
return
|
||||||
|
|
||||||
while True:
|
log(f"[Main] Updating cooking state to: {new_state}")
|
||||||
try:
|
cooking_state.set_state(new_state)
|
||||||
# 1. Check for incoming messages from the Heltec board
|
|
||||||
while uart_device.any():
|
|
||||||
incoming_msg = uart_device.read()
|
|
||||||
print(f"[Thread] Received from esp-lora over UART: {incoming_msg}")
|
|
||||||
|
|
||||||
# 2. Example: Send data to the Heltec board every 5 seconds
|
# Status LED
|
||||||
# uart_device.send("Status Check: WiFi Active")
|
status_led = RGBLED(red_pin=21, green_pin=19, blue_pin=18)
|
||||||
|
def on_cooking_state_change(state):
|
||||||
|
print(f"[Main] Cooking state changed to: {state.state}")
|
||||||
|
|
||||||
time.sleep(5) # Fast responsive polling loop for local UART
|
# === STATUS LED UPDATE ===
|
||||||
|
BLINK_INTERVAL_MS = 500 # Blink every 500ms
|
||||||
|
|
||||||
except Exception as e:
|
if state.state == cookingState.CookingStates.IDLE:
|
||||||
print("[Thread] UART error encountered:", e)
|
status_led.color = RGBLED.OFF
|
||||||
time.sleep(5)
|
status_led.blink_off()
|
||||||
|
if state.state == cookingState.CookingStates.COOKING:
|
||||||
# --- Launch background worker ---
|
status_led.color = RGBLED.YELLOW
|
||||||
_thread.start_new_thread(mqtt_background_thread, ())
|
status_led.blink_off()
|
||||||
_thread.start_new_thread(uart_background_thread, ())
|
if state.state == cookingState.CookingStates.STIRRING_REQUIRED:
|
||||||
|
status_led.color = RGBLED.ORANGE
|
||||||
|
status_led.blink_on(BLINK_INTERVAL_MS)
|
||||||
|
if state.state == cookingState.CookingStates.DONE:
|
||||||
|
status_led.color = RGBLED.GREEN
|
||||||
|
status_led.blink_off()
|
||||||
|
if state.state == cookingState.CookingStates.ALERT:
|
||||||
|
status_led.color = RGBLED.RED
|
||||||
|
status_led.blink_on(BLINK_INTERVAL_MS)
|
||||||
|
|
||||||
|
|
||||||
# --- MAIN APPLICATION THREAD (Core 0) ---
|
# --- MAIN APPLICATION THREAD (Core 0) ---
|
||||||
print("[Main] Main execution path active.")
|
print("[Main] Main execution path active.")
|
||||||
time.sleep(2) # Give the thread a moment to initial connect
|
|
||||||
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
|
|
||||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
|
||||||
|
|
||||||
# Temperature sensor setup
|
# Temperature sensor setup
|
||||||
temperature_sensor_i2c = I2C(0, scl=Pin(25, Pin.IN, Pin.PULL_UP), sda=Pin(26, Pin.IN, Pin.PULL_UP), freq=100000)
|
temperature_sensor_i2c = I2C(0, scl=Pin(25, Pin.IN, Pin.PULL_UP), sda=Pin(26, Pin.IN, Pin.PULL_UP), freq=100000)
|
||||||
@@ -178,7 +201,13 @@ if 0x5A in devices:
|
|||||||
print("MLX90614 found at address 0x5A!")
|
print("MLX90614 found at address 0x5A!")
|
||||||
else:
|
else:
|
||||||
print("MLX90614 not found. Please check your wiring.")
|
print("MLX90614 not found. Please check your wiring.")
|
||||||
temperature_sensor = temperature_sensor.MLX90614(temperature_sensor_i2c)
|
mlx_temperature_sensor = temperature_sensor.MLX90614(temperature_sensor_i2c)
|
||||||
|
|
||||||
|
# --- Launch background worker ---
|
||||||
|
_thread.start_new_thread(mqtt_background_thread, ())
|
||||||
|
time.sleep(2) # Give the thread a moment to initial connect
|
||||||
|
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
|
||||||
|
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
|
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
|
||||||
@@ -188,6 +217,23 @@ while True:
|
|||||||
mqtt_hello_sent_timestamp = time.time()
|
mqtt_hello_sent_timestamp = time.time()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 1. Listen for incoming UART serial packets from the WROOM board
|
||||||
|
while uart_device.any():
|
||||||
|
command = uart_device.read_as_command()
|
||||||
|
if command:
|
||||||
|
print(f"[Main] Received command from LoRa Board: {command.command_type}")
|
||||||
|
if command.command_type == UARTCommandType.COOKING_STATE_UPDATE:
|
||||||
|
# Handle cooking state update command
|
||||||
|
new_state = command.payload.get("state", None)
|
||||||
|
print(f"[Main] Cooking state update received: {new_state}")
|
||||||
|
on_received_cooking_state_update(new_state)
|
||||||
|
else:
|
||||||
|
print(f"[Main] Unknown command type received: {command.command_type}")
|
||||||
|
else:
|
||||||
|
# Fallback to reading as a raw string if parsing fails
|
||||||
|
raw_command = uart_device.read()
|
||||||
|
print(f"[Main] Received raw command from WiFi Board: {raw_command}")
|
||||||
|
|
||||||
# 2. Example: Send data to the Heltec board every 5 seconds
|
# 2. Example: Send data to the Heltec board every 5 seconds
|
||||||
# uart_device.send("Status Check: WiFi Active")
|
# uart_device.send("Status Check: WiFi Active")
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -142,7 +142,7 @@ def read_sensors_for_cooking(microwave_id):
|
|||||||
log(f"\nLecture du capteur Ultrason : {sensor_data['ultrasonic_distance']}")
|
log(f"\nLecture du capteur Ultrason : {sensor_data['ultrasonic_distance']}")
|
||||||
|
|
||||||
# Read Temperature and Humidity
|
# Read Temperature and Humidity
|
||||||
temperature, humidity = temp_hum.get_temperature_and_humidity()
|
temperature, humidity = temp_hum.get_temperature_and_humidity_with_retry()
|
||||||
if temperature is not None and humidity is not None:
|
if temperature is not None and humidity is not None:
|
||||||
log(f"\nLecture du capteur Temp/Hum : {temperature}, {humidity}")
|
log(f"\nLecture du capteur Temp/Hum : {temperature}, {humidity}")
|
||||||
sensor_data["temperature"] = temperature
|
sensor_data["temperature"] = temperature
|
||||||
@@ -170,22 +170,20 @@ def _stop_hardware(microwave_id: str):
|
|||||||
"""
|
"""
|
||||||
Hardware driver stop — halts magnetron/turntable immediately.
|
Hardware driver stop — halts magnetron/turntable immediately.
|
||||||
"""
|
"""
|
||||||
log(f"[{microwave_id}] 🛑 Emergency stop issued to hardware.")
|
print(f"[{microwave_id}] 🛑 Emergency stop issued to hardware.")
|
||||||
# TODO: Add physical hardware stop command here
|
# TODO: Add physical hardware stop command here
|
||||||
# e.g., gpio_controller.stop()
|
# e.g., gpio_controller.stop()
|
||||||
|
|
||||||
|
|
||||||
def _send_params_to_microwave(microwave_id: str, cook_time_seconds: int, power_level_pct: int, target_temp: float, cancel_event: threading.Event):
|
def _send_params_to_microwave(microwave_id: str, cook_time: int, power_level: int, target_temp: float, cancel_event: threading.Event):
|
||||||
"""
|
"""
|
||||||
Triggers physical microwave execution.
|
Triggers physical microwave execution.
|
||||||
"""
|
"""
|
||||||
if cancel_event.is_set():
|
if cancel_event.is_set():
|
||||||
return
|
return
|
||||||
|
|
||||||
log(f"[{microwave_id}] ⚡ Starting microwave cooking : {cook_time_seconds}s @ {power_level_pct}W power, target temp {target_temp}°C.")
|
print(f"[{microwave_id}] Sending microwave {microwave_id} cooking parameters : {cook_time}s @ {power_level}W power, target temp {target_temp}°C.")
|
||||||
# TODO: Connect to microwave hardware driver here
|
mqtt_client.publish(config.MQTT_TOPIC_COOKING, payloads.mqtt_cooking_config(microwave_id, cook_time, power_level, target_temp), qos=config.MQTT_QOS)
|
||||||
# e.g., gpio_controller.start(time=cook_time_seconds, power=power_level_pct)
|
|
||||||
|
|
||||||
|
|
||||||
def _cooking_worker(microwave_id: str, sensors_data: dict, cancel_event: threading.Event):
|
def _cooking_worker(microwave_id: str, sensors_data: dict, cancel_event: threading.Event):
|
||||||
"""Worker function executing cloud API calls and hardware triggers."""
|
"""Worker function executing cloud API calls and hardware triggers."""
|
||||||
@@ -211,9 +209,9 @@ def _cooking_worker(microwave_id: str, sensors_data: dict, cancel_event: threadi
|
|||||||
print(f"[{microwave_id}] Job was canceled while waiting for cloud response. Discarding result.")
|
print(f"[{microwave_id}] Job was canceled while waiting for cloud response. Discarding result.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if (response.status_code != 200):
|
log(f"[{microwave_id}] Cloud API responded with : {response.json()}")
|
||||||
|
if response.status_code != 200 and response.status_code != 201:
|
||||||
print(f"[{microwave_id}] Cloud API returned error {response.status_code}: {response.json()}")
|
print(f"[{microwave_id}] Cloud API returned error {response.status_code}: {response.json()}")
|
||||||
return
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# 2. Extract Response Parameters
|
# 2. Extract Response Parameters
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import grovepi
|
import grovepi
|
||||||
import math
|
import math
|
||||||
|
import time
|
||||||
from sensors.lock import grove_lock
|
from sensors.lock import grove_lock
|
||||||
|
|
||||||
# Connect the Grove Temperature & Humidity Sensor Pro to digital port D3
|
# Connect the Grove Temperature & Humidity Sensor Pro to digital port D3
|
||||||
@@ -20,3 +21,11 @@ def get_temperature_and_humidity():
|
|||||||
else:
|
else:
|
||||||
print("Error reading from DHT sensor")
|
print("Error reading from DHT sensor")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
def get_temperature_and_humidity_with_retry(max_retries=3):
|
||||||
|
for _ in range(max_retries): # Try up to max_retries times
|
||||||
|
temp, humidity = get_temperature_and_humidity()
|
||||||
|
if temp is not None and humidity is not None:
|
||||||
|
return temp, humidity
|
||||||
|
time.sleep(1) # Wait a bit before retrying
|
||||||
|
return None, None
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
import shared.deviceTypes as deviceTypes
|
import shared.deviceTypes as deviceTypes
|
||||||
import shared.config as config
|
import shared.config as config
|
||||||
import shared.payloads as payloads
|
import shared.payloads as payloads
|
||||||
|
import shared.cookingState as cookingState
|
||||||
|
try:
|
||||||
|
import shared.uart_comm as uart_comm
|
||||||
|
except ImportError:
|
||||||
|
pass # No need as we are on the RPI
|
||||||
|
import shared.sensors
|
||||||
|
|
||||||
def get_lora(*args, **kwargs):
|
def get_lora(*args, **kwargs):
|
||||||
from .lora_device import get_lora_device
|
from .lora_device import get_lora_device
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class CookingState:
|
||||||
|
TEMPERATURE_TOLERANCE = 1.0
|
||||||
|
|
||||||
|
def __init__(self, cook_time: int, power_level: int, target_temp: float, temperature_provider=None, on_state_change=None, on_refresh=None):
|
||||||
|
self.cook_time = cook_time
|
||||||
|
self.power_level = power_level
|
||||||
|
self.target_temp = target_temp
|
||||||
|
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.temperature_provider = temperature_provider
|
||||||
|
self.on_state_change = on_state_change
|
||||||
|
self.on_refresh = on_refresh
|
||||||
|
|
||||||
|
self.state = CookingStates.COOKING
|
||||||
|
self.paused = False
|
||||||
|
self._pause_started_at = None
|
||||||
|
self._paused_duration = 0.0
|
||||||
|
|
||||||
|
self.current_dish_temp = None
|
||||||
|
self.current_ambient_temp = None
|
||||||
|
self.estimated_remaining_time = float(cook_time)
|
||||||
|
self._last_temperature_sample = None
|
||||||
|
self._last_refresh_signature = None
|
||||||
|
|
||||||
|
def set_temperature_provider(self, temperature_provider):
|
||||||
|
self.temperature_provider = temperature_provider
|
||||||
|
|
||||||
|
def set_state_change_callback(self, callback):
|
||||||
|
self.on_state_change = callback
|
||||||
|
|
||||||
|
def set_refresh_callback(self, callback):
|
||||||
|
self.on_refresh = callback
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
if self.paused:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.paused = True
|
||||||
|
self._pause_started_at = time.time()
|
||||||
|
self._notify_refresh(force=True)
|
||||||
|
|
||||||
|
def unpause(self):
|
||||||
|
if not self.paused:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if self._pause_started_at is not None:
|
||||||
|
self._paused_duration += now - self._pause_started_at
|
||||||
|
|
||||||
|
self._pause_started_at = None
|
||||||
|
self.paused = False
|
||||||
|
self._notify_refresh(force=True)
|
||||||
|
|
||||||
|
def toggle_pause(self):
|
||||||
|
if self.paused:
|
||||||
|
self.unpause()
|
||||||
|
else:
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
if self.state == state:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.state = state
|
||||||
|
self._notify_state_change()
|
||||||
|
self._notify_refresh(force=True)
|
||||||
|
|
||||||
|
def get_elapsed_time(self) -> float:
|
||||||
|
now = time.time()
|
||||||
|
elapsed = now - self.start_time - self._paused_duration
|
||||||
|
|
||||||
|
if self.paused and self._pause_started_at is not None:
|
||||||
|
elapsed -= now - self._pause_started_at
|
||||||
|
|
||||||
|
return max(0.0, elapsed)
|
||||||
|
|
||||||
|
def get_remaining_time(self) -> int:
|
||||||
|
"""Returns the estimated remaining cooking time in seconds."""
|
||||||
|
return int(max(0.0, self.get_remaining_time_estimation()))
|
||||||
|
|
||||||
|
def get_remaining_time_estimation(self) -> float:
|
||||||
|
elapsed_time = self.get_elapsed_time()
|
||||||
|
timer_remaining = max(0.0, float(self.cook_time) - elapsed_time)
|
||||||
|
|
||||||
|
if self.current_dish_temp is None:
|
||||||
|
return timer_remaining
|
||||||
|
|
||||||
|
if self.current_dish_temp >= self.target_temp:
|
||||||
|
return timer_remaining
|
||||||
|
|
||||||
|
heating_rate = self._estimate_heating_rate()
|
||||||
|
if heating_rate <= 0:
|
||||||
|
return timer_remaining
|
||||||
|
|
||||||
|
target_remaining = (self.target_temp - self.current_dish_temp) / heating_rate
|
||||||
|
return max(timer_remaining, max(0.0, target_remaining))
|
||||||
|
|
||||||
|
def _read_temperatures(self):
|
||||||
|
if self.temperature_provider is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
temperatures = self.temperature_provider()
|
||||||
|
if temperatures is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if isinstance(temperatures, (list, tuple)) and len(temperatures) >= 2:
|
||||||
|
return temperatures[0], temperatures[1]
|
||||||
|
|
||||||
|
raise ValueError("temperature_provider must return a pair: (dish_temp, ambient_temp)")
|
||||||
|
|
||||||
|
def _estimate_heating_rate(self):
|
||||||
|
if self._last_temperature_sample is None:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
last_time, last_temp = self._last_temperature_sample
|
||||||
|
now = time.time()
|
||||||
|
current_temp = self.current_dish_temp
|
||||||
|
|
||||||
|
if current_temp is None:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
delta_time = now - last_time
|
||||||
|
if delta_time <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return (current_temp - last_temp) / delta_time
|
||||||
|
|
||||||
|
def _notify_state_change(self):
|
||||||
|
if self.on_state_change is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_state_change(self)
|
||||||
|
|
||||||
|
def _notify_refresh(self, force=False):
|
||||||
|
if self.on_refresh is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
signature = (
|
||||||
|
int(self.get_elapsed_time()),
|
||||||
|
int(self.get_remaining_time_estimation()),
|
||||||
|
self.current_dish_temp,
|
||||||
|
self.current_ambient_temp,
|
||||||
|
self.state,
|
||||||
|
self.paused,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not force and signature == self._last_refresh_signature:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._last_refresh_signature = signature
|
||||||
|
self.on_refresh(self)
|
||||||
|
|
||||||
|
def update_tick(self):
|
||||||
|
if self.paused:
|
||||||
|
self._notify_refresh()
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
previous_state = self.state
|
||||||
|
previous_temperature = self.current_dish_temp
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.current_dish_temp, self.current_ambient_temp = self._read_temperatures()
|
||||||
|
except Exception:
|
||||||
|
self.current_dish_temp = previous_temperature
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
elapsed_time = self.get_elapsed_time()
|
||||||
|
self.estimated_remaining_time = self.get_remaining_time_estimation()
|
||||||
|
|
||||||
|
if self.current_dish_temp is not None:
|
||||||
|
if elapsed_time < (self.cook_time / 2.0) and self.current_dish_temp >= self.target_temp:
|
||||||
|
self.state = CookingStates.STIRRING_REQUIRED
|
||||||
|
self.pause()
|
||||||
|
elif elapsed_time >= self.cook_time and self.current_dish_temp >= (self.target_temp - self.TEMPERATURE_TOLERANCE):
|
||||||
|
self.state = CookingStates.DONE
|
||||||
|
elif self.state == CookingStates.DONE and self.current_dish_temp < (self.target_temp - self.TEMPERATURE_TOLERANCE):
|
||||||
|
self.state = CookingStates.COOKING
|
||||||
|
elif elapsed_time >= self.cook_time * 1.25: # If the dish is not heating up
|
||||||
|
self.state = CookingStates.STIRRING_REQUIRED
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
self._last_temperature_sample = (now, self.current_dish_temp)
|
||||||
|
|
||||||
|
if self.state != previous_state:
|
||||||
|
self._notify_state_change()
|
||||||
|
|
||||||
|
self._notify_refresh()
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
|
||||||
|
class CookingStates:
|
||||||
|
COOKING = 0
|
||||||
|
STIRRING_REQUIRED = 1
|
||||||
|
DONE = 2
|
||||||
|
ALERT = 3 # Microwave is too hot internally or other alerts
|
||||||
|
IDLE = 4 # Waiting for cooking parameters to be set, or after cooking is done
|
||||||
@@ -36,3 +36,11 @@ def mqtt_sensor_data(id_microwave, dish_temp, ambient_temp):
|
|||||||
"dish_temp": dish_temp,
|
"dish_temp": dish_temp,
|
||||||
"ambient_temp": ambient_temp
|
"ambient_temp": ambient_temp
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def mqtt_cooking_config(id_microwave, cook_time, power_level, target_temp):
|
||||||
|
return as_json({
|
||||||
|
"id_microwave": id_microwave,
|
||||||
|
"cook_time": cook_time,
|
||||||
|
"power_level": power_level,
|
||||||
|
"target_temp": target_temp
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
try:
|
||||||
|
from shared.sensors.rgb_led import RGBLED
|
||||||
|
except ImportError:
|
||||||
|
pass # No need as we are on the RPI
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from machine import Pin, PWM, Timer
|
||||||
|
import time
|
||||||
|
|
||||||
|
class RGBLED:
|
||||||
|
"""
|
||||||
|
MicroPython driver for 4-pin RGB LEDs on ESP32 / Heltec boards.
|
||||||
|
Supports state tracking, color setting, brightness scaling,
|
||||||
|
state toggling, and non-blocking blinking via machine.Timer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RED = (255, 0, 0)
|
||||||
|
GREEN = (0, 255, 0)
|
||||||
|
BLUE = (0, 0, 255)
|
||||||
|
YELLOW = (255, 120, 0)
|
||||||
|
WHITE_YELLOW = (150, 30, 0)
|
||||||
|
ORANGE = (255, 50, 0)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
OFF = (0, 0, 0)
|
||||||
|
|
||||||
|
def __init__(self, red_pin, green_pin, blue_pin, common_anode=False, freq=1000, timer_id=1):
|
||||||
|
"""
|
||||||
|
:param red_pin: GPIO pin number for Red channel
|
||||||
|
:param green_pin: GPIO pin number for Green channel
|
||||||
|
:param blue_pin: GPIO pin number for Blue channel
|
||||||
|
:param common_anode: Set True if cathode is connected to 3.3V instead of GND
|
||||||
|
:param freq: PWM frequency in Hz (default 1000Hz)
|
||||||
|
:param timer_id: Hardware/software timer ID for non-blocking blinks (-1 uses soft timers on ESP32).
|
||||||
|
"""
|
||||||
|
self._r_pwm = PWM(Pin(red_pin, Pin.OUT), freq=freq)
|
||||||
|
self._g_pwm = PWM(Pin(green_pin, Pin.OUT), freq=freq)
|
||||||
|
self._b_pwm = PWM(Pin(blue_pin, Pin.OUT), freq=freq)
|
||||||
|
|
||||||
|
self._common_anode = common_anode
|
||||||
|
|
||||||
|
# State tracking variables
|
||||||
|
self._color = (0, 0, 0) # Current (R, G, B) tuple [0-255]
|
||||||
|
self._brightness = 1.0 # Brightness factor [0.0 to 1.0]
|
||||||
|
self._is_on = True # Master power state
|
||||||
|
|
||||||
|
# Blink state variables
|
||||||
|
self._timer = Timer(timer_id)
|
||||||
|
self._is_blinking = False
|
||||||
|
|
||||||
|
self._apply()
|
||||||
|
|
||||||
|
def _apply(self):
|
||||||
|
"""Recalculates and applies PWM duty cycles based on state."""
|
||||||
|
if not self._is_on:
|
||||||
|
r, g, b = 0, 0, 0
|
||||||
|
else:
|
||||||
|
r = int(self._color[0] * self._brightness)
|
||||||
|
g = int(self._color[1] * self._brightness)
|
||||||
|
b = int(self._color[2] * self._brightness)
|
||||||
|
|
||||||
|
for pwm, val in ((self._r_pwm, r), (self._g_pwm, g), (self._b_pwm, b)):
|
||||||
|
# Clamp value between 0 and 255
|
||||||
|
val = max(0, min(255, val))
|
||||||
|
# Convert 8-bit (0-255) to MicroPython's 16-bit PWM duty (0-65535)
|
||||||
|
duty = int((val / 255.0) * 65535)
|
||||||
|
|
||||||
|
if self._common_anode:
|
||||||
|
duty = 65535 - duty
|
||||||
|
|
||||||
|
pwm.duty_u16(duty)
|
||||||
|
|
||||||
|
# --- Properties and Setters ---
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color(self):
|
||||||
|
"""Returns the active RGB tuple (R, G, B)."""
|
||||||
|
return self._color
|
||||||
|
|
||||||
|
@color.setter
|
||||||
|
def color(self, rgb_tuple):
|
||||||
|
"""Sets the RGB color tuple (e.g., (255, 128, 0))."""
|
||||||
|
if isinstance(rgb_tuple, (tuple, list)) and len(rgb_tuple) == 3:
|
||||||
|
self._color = tuple(rgb_tuple)
|
||||||
|
self._apply()
|
||||||
|
else:
|
||||||
|
raise ValueError("Color must be a tuple of 3 integers: (R, G, B)")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Returns the current brightness level (0.0 to 1.0)."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@brightness.setter
|
||||||
|
def brightness(self, level):
|
||||||
|
"""Sets brightness level from 0.0 (0%) to 1.0 (100%)."""
|
||||||
|
self._brightness = max(0.0, min(1.0, float(level)))
|
||||||
|
self._apply()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Returns True if the LED is currently powered on."""
|
||||||
|
return self._is_on
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_blinking(self):
|
||||||
|
return self._is_blinking
|
||||||
|
|
||||||
|
# --- Basic Control Methods ---
|
||||||
|
|
||||||
|
def set_rgb(self, r, g, b):
|
||||||
|
"""Alternative setter for individual R, G, B integer values."""
|
||||||
|
self.color = (r, g, b)
|
||||||
|
|
||||||
|
def on(self):
|
||||||
|
"""Turns the LED on using its stored color and brightness."""
|
||||||
|
self._is_on = True
|
||||||
|
self._apply()
|
||||||
|
|
||||||
|
def off(self):
|
||||||
|
"""Turns the LED off without resetting the active color state."""
|
||||||
|
self._is_on = False
|
||||||
|
self._apply()
|
||||||
|
|
||||||
|
def toggle(self):
|
||||||
|
"""Toggles between ON and OFF states."""
|
||||||
|
self._is_on = not self._is_on
|
||||||
|
self._apply()
|
||||||
|
|
||||||
|
# --- Non-Blocking Blinking Methods ---
|
||||||
|
|
||||||
|
def _timer_callback(self, t):
|
||||||
|
"""Internal callback executed by machine.Timer."""
|
||||||
|
self.toggle()
|
||||||
|
|
||||||
|
def blink_on(self, interval_ms=500):
|
||||||
|
"""Starts background blinking at the specified interval in milliseconds."""
|
||||||
|
if self._is_blinking:
|
||||||
|
self._timer.deinit()
|
||||||
|
|
||||||
|
self._is_blinking = True
|
||||||
|
self.on() # Ensure initial state is on
|
||||||
|
self._timer.init(
|
||||||
|
period=interval_ms,
|
||||||
|
mode=Timer.PERIODIC,
|
||||||
|
callback=self._timer_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
def blink_off(self):
|
||||||
|
"""Stops blinking and returns control to steady state."""
|
||||||
|
if self._is_blinking:
|
||||||
|
self._timer.deinit()
|
||||||
|
self._is_blinking = False
|
||||||
|
|
||||||
|
def blink_toggle(self, interval_ms=500):
|
||||||
|
"""Toggles blinking state (starts if stopped, stops if active)."""
|
||||||
|
if self._is_blinking:
|
||||||
|
self.blink_off()
|
||||||
|
else:
|
||||||
|
self.blink_on(interval_ms)
|
||||||
|
|
||||||
|
def deinit(self):
|
||||||
|
"""Releases the hardware PWM pins and timer when finished."""
|
||||||
|
self._r_pwm.deinit()
|
||||||
|
self._g_pwm.deinit()
|
||||||
|
self._b_pwm.deinit()
|
||||||
+118
-21
@@ -1,12 +1,12 @@
|
|||||||
# shared/uart_comm.py
|
|
||||||
import _thread
|
import _thread
|
||||||
from machine import UART
|
from machine import UART
|
||||||
import time
|
import time
|
||||||
|
import ujson
|
||||||
|
|
||||||
class SafeUART:
|
class SafeUART:
|
||||||
def __init__(self, uart_id, tx_pin, rx_pin, baudrate=115200):
|
def __init__(self, uart_id, tx_pin, rx_pin, baudrate=115200):
|
||||||
# Initialize the hardware UART channel
|
# Initialize the hardware UART channel
|
||||||
self.uart = UART(uart_id, baudrate=baudrate, tx=tx_pin, rx=rx_pin, timeout=10)
|
self.uart = UART(uart_id, baudrate=baudrate, tx=tx_pin, rx=rx_pin, timeout=10, rxbuf=1024)
|
||||||
|
|
||||||
# Core thread-safety assets
|
# Core thread-safety assets
|
||||||
self.lock = _thread.allocate_lock()
|
self.lock = _thread.allocate_lock()
|
||||||
@@ -21,37 +21,87 @@ class SafeUART:
|
|||||||
print(f"[UART] Thread initialized on UART{uart_id} (TX:{tx_pin}, RX:{rx_pin})")
|
print(f"[UART] Thread initialized on UART{uart_id} (TX:{tx_pin}, RX:{rx_pin})")
|
||||||
|
|
||||||
def _listener_worker(self):
|
def _listener_worker(self):
|
||||||
"""Asynchronous internal loop parsing incoming stream lines into the queue."""
|
"""Worker loop that handles nested JSON structures by tracking brace depth."""
|
||||||
while True:
|
while True:
|
||||||
try:
|
messages_found = []
|
||||||
if self.uart.any():
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Pull all raw bytes waiting in the hardware ring buffer
|
if self.uart.any():
|
||||||
chunk = self.uart.read(self.uart.any())
|
chunk = self.uart.read()
|
||||||
if chunk:
|
if chunk is not None and isinstance(chunk, bytes):
|
||||||
self.buffer += chunk
|
self.buffer += chunk
|
||||||
|
|
||||||
# Process complete lines terminated by a newline character
|
# Extract complete JSON objects while accounting for nested braces
|
||||||
while b'\n' in self.buffer:
|
while True:
|
||||||
line, self.buffer = self.buffer.split(b'\n', 1)
|
start_idx = self.buffer.find(b'{')
|
||||||
try:
|
if start_idx == -1:
|
||||||
decoded_line = line.decode('utf-8').strip()
|
# No starting brace; clear any garbage bytes currently in buffer
|
||||||
if decoded_line:
|
self.buffer = b""
|
||||||
self.rx_queue.append(decoded_line)
|
break
|
||||||
except Exception:
|
|
||||||
pass # Discard corrupt data frames safely
|
|
||||||
except Exception as e:
|
|
||||||
print("[UART Thread Error]:", e)
|
|
||||||
|
|
||||||
time.sleep_ms(20) # Give other background threads breathing room
|
# 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):
|
def send(self, message):
|
||||||
"""Safely pushes strings across the serial wire from any thread context."""
|
"""Safely pushes strings across the serial wire from any thread context."""
|
||||||
if not message.endswith('\n'):
|
if not message.endswith('\n'):
|
||||||
message += '\n'
|
message += '\n'
|
||||||
|
|
||||||
|
data = message.encode('utf-8')
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.uart.write(message.encode('utf-8'))
|
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):
|
def any(self):
|
||||||
"""Checks if any complete messages are waiting to be read."""
|
"""Checks if any complete messages are waiting to be read."""
|
||||||
@@ -64,3 +114,50 @@ class SafeUART:
|
|||||||
if self.rx_queue:
|
if self.rx_queue:
|
||||||
return self.rx_queue.pop(0)
|
return self.rx_queue.pop(0)
|
||||||
return None
|
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"
|
||||||
Reference in New Issue
Block a user