diff --git a/micro_ondes/esp_lora/lib/microwaveScreen.py b/micro_ondes/esp_lora/lib/microwaveScreen.py index dde160d..1f5224b 100644 --- a/micro_ondes/esp_lora/lib/microwaveScreen.py +++ b/micro_ondes/esp_lora/lib/microwaveScreen.py @@ -126,7 +126,6 @@ class MicrowaveScreen: self.h_centered_text(text, 29, 0) def message(self, text, alert): - self.flick() + # self.flick() self._message(text, alert) self.show() - pass diff --git a/micro_ondes/esp_lora/main.py b/micro_ondes/esp_lora/main.py index cd12d71..9780a70 100644 --- a/micro_ondes/esp_lora/main.py +++ b/micro_ondes/esp_lora/main.py @@ -17,6 +17,7 @@ from shared.uart_comm import UARTCommand, UARTCommandType from shared.sensors import RGBLED from shared.logging import log from shared.lora_device import LoraCommands +from shared.alerts import Alert, AlertType, AlertManager # --- READ DEVICE ID --- try: @@ -37,6 +38,7 @@ current_temp = [None, None] last_temp = [None, None] temperature_asked = False last_temp_request_time = 0 +alert_manager = None PING_PAYLOAD = { @@ -44,6 +46,11 @@ PING_PAYLOAD = { "type": deviceTypes.DEVICE_TYPES["MICROWAVE"] } +def on_new_alert(alert: Alert): + global microwave_screen + print(f"[Alert Manager] New alert received: {alert.to_dict()}") + microwave_screen.message(alert.message, True) + def init_hardware(): """Initializes all hardware components.""" global lora, uart_device, magnetron_led, microwave_screen @@ -247,8 +254,13 @@ async def lora_process_task(): print("[LoRa Process] Toggling defrost mode via orchestrator command.") defrost_mode = data["defrost_state"] microwave_screen.update(cooking_state, defrost_mode) + elif data["action"] == LoraCommands.NEW_ALERT: + alert = Alert(data.get("alert_type"), data.get("message", "Unsafe area")) + alert.timestamp = data.get("timestamp", time.time()) + print(f"[LoRa Process] New alert received via orchestrator: {alert.to_dict()}") + alert_manager.add_alert(alert) - await asyncio.sleep_ms(50) + await asyncio.sleep_ms(100) async def cooking_loop_task(): @@ -276,11 +288,14 @@ async def memory_cleanup_task(): # --- BOOTSTRAP --- async def main(): - global cooking_state, defrost_mode, microwave_screen + global cooking_state, defrost_mode, microwave_screen, alert_manager print("[Main] Starting application...") init_hardware() + alert_manager = AlertManager() + alert_manager.set_on_alert_callback(on_new_alert) + # Launch dedicated hardware thread for LoRa RX try: _thread.stack_size(16 * 1024) diff --git a/orchestrateur/main.py b/orchestrateur/main.py index 4dd84cd..3b766fb 100644 --- a/orchestrateur/main.py +++ b/orchestrateur/main.py @@ -9,6 +9,7 @@ from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db from shared.logging import log from shared.cookingState import CookingStates from shared.lora_device import LoraCommands +from shared.alerts import AlertManager, Alert, AlertType from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer from lib.systemd_logs import get_systemd_logs @@ -76,7 +77,6 @@ async_event_queue = None ir_data_cache = {} # mw_id -> dict of IR readings ir_data_events = {} # mw_id -> asyncio.Event() - # --- HARDWARE & MQTT SETUP --- lora = get_lora() lora.configure() @@ -188,6 +188,28 @@ def button_callback(): button.set_callback(button_callback) button.start_button_monitoring_thread() +# --- Alert manager --- +alert_manager = None + +def on_new_alert(alert): + # Sends to cloud server + alert_dict = alert.to_dict() + alert_dict["orchestrator_id"] = DEVICE_ID + + response = requests.post( + "https://smartwave.matthiasg.dev/alert", + json=alert_dict, + ) + + response.raise_for_status() + res_json = response.json() + print(f"[Alert Handling] Alert sent to cloud: {alert_dict}, Response: {res_json}") + + # Send the alert to the lora device + lora.send_reliable(payloads.lora_new_alert(alert)) + + # TODO : envoi de l'alerte aux écrans externes + # --- HARDWARE CONTROLLERS --- def _stop_hardware(microwave_id: str): print(f"[{microwave_id}] /!\\ Emergency stop issued to hardware.") @@ -268,7 +290,7 @@ async def handle_new_dish(microwave_id, detected_height): async def request_cloud_cooking_plan(microwave_id, sensors_data): """Sends all data to the cloud with up to 3 retries (330s timeout for AI generation).""" - global cloud_alert + global cloud_alert, alert_manager microwave_states[microwave_id] = MicrowaveState.WAITING_FOR_CLOUD URL = "https://smartwave.matthiasg.dev/cooking-params" @@ -306,8 +328,22 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data): response.raise_for_status() - # Robust JSON extraction: handles direct dict or nested {"cook_plan": {...}} + # JSON extraction res_json = response.json() + + # Error handling + if "error" in res_json: + print(f"[{microwave_id}] Cloud API returned error: {res_json['error']}") + microwave_states[microwave_id] = MicrowaveState.DONE + + if not res_json['is_safe']: # The cooking area is not safe, raise an alert + alert = Alert(AlertType.COOKING_SAFETY, res_json['warning_message']) + alert_manager.add_alert(alert) + else: + alert = Alert(AlertType.ERROR, f"Cloud API error for microwave {microwave_id}: {res_json['error']}") + return + + plan = res_json.get("cook_plan", res_json) c_time = plan.get("cook_time_seconds") @@ -316,7 +352,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data): if c_time is None or c_power is None or c_temp is None: print(f"[{microwave_id}] ❌ Invalid plan received: {res_json}") - microwave_states[microwave_id] = MicrowaveState.DONE + microwave_states[microwave_id] = None return print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W") @@ -335,6 +371,9 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data): return except Exception as e: + # Print stacktrace + import traceback + traceback.print_exc() print(f"[{microwave_id}] Cloud API Error (Attempt {attempt}/{max_retries}): {e}") if attempt < max_retries: @@ -540,6 +579,11 @@ async def main(): global async_event_queue print("Orchestrateur prêt. Lancement des tâches...") + # Initialize the alert manager and set the callback + global alert_manager + alert_manager = AlertManager() + alert_manager.set_on_alert_callback(on_new_alert) + # Initialize SQLite database table init_db() diff --git a/shared/__init__.py b/shared/__init__.py index df1e3fd..7875fa0 100644 --- a/shared/__init__.py +++ b/shared/__init__.py @@ -6,6 +6,7 @@ import shared.config as config import shared.payloads as payloads import shared.cookingState as cookingState import shared.safeQueue as safeQueue +import shared.alerts as alerts try: import shared.lora_device as lora_device except ImportError: diff --git a/shared/alerts.py b/shared/alerts.py new file mode 100644 index 0000000..6c358fe --- /dev/null +++ b/shared/alerts.py @@ -0,0 +1,47 @@ +import time + +class AlertManager: + def __init__(self): + self.alerts = [] + self.on_alert_callback = None + + def set_on_alert_callback(self, callback): + self.on_alert_callback = callback + + def add_alert(self, alert): + self.alerts.append(alert) + if self.on_alert_callback: + self.on_alert_callback(alert) + + def get_alerts(self): + return self.alerts + + def clear_alerts(self): + self.alerts.clear() + +class Alert: + def __init__(self, alertType: "AlertType", message: str): + self.alertType = alertType + self.message = message + self.timestamp = time.time() + + def to_dict(self): + return { + "type": self.alertType, + "message": self.message, + "timestamp": self.timestamp + } + + def to_json(self): + import json + return json.dumps(self.to_dict()) + + def from_json(json_string: str) -> "Alert": + import json + data = json.loads(json_string) + alert = Alert(data["type"], data["message"]) + alert.timestamp = data["timestamp"] + return alert + +class AlertType: + COOKING_SAFETY = "COOKING_SAFETY" \ No newline at end of file diff --git a/shared/payloads.py b/shared/payloads.py index 8d98b02..10c906f 100644 --- a/shared/payloads.py +++ b/shared/payloads.py @@ -1,5 +1,11 @@ from time import time from shared.deviceTypes import DEVICE_TYPES +from shared.alerts import Alert, AlertType +import shared.config as config +try: + from shared.lora_device import LoraCommands +except ImportError: + pass # No need try: @@ -65,4 +71,10 @@ def telemetry_payload(device_id, microwave_states, button_state, cloud_alert, gp "ambient_humidity": ambient_humidity, "connected_components": connected_components, "logs": logs - }) \ No newline at end of file + }) + +def lora_new_alert(alert: Alert): + return { + "action": LoraCommands.NEW_ALERT, + "alert_type": alert.alertType + } \ No newline at end of file