From 83b269cc8818aa8614e446f4d40346d014f8b698 Mon Sep 17 00:00:00 2001 From: Matthias Guillitte Date: Mon, 17 Aug 2026 13:04:35 +0200 Subject: [PATCH] Temperature Alert --- micro_ondes/esp_lora/lib/microwaveScreen.py | 10 +++-- micro_ondes/esp_lora/main.py | 47 +++++++++++++++------ orchestrateur/main.py | 38 ++++++++++++++--- shared/alerts.py | 7 ++- shared/config.py | 4 +- shared/microwave_state.py | 8 ++-- shared/payloads.py | 9 +++- 7 files changed, 93 insertions(+), 30 deletions(-) diff --git a/micro_ondes/esp_lora/lib/microwaveScreen.py b/micro_ondes/esp_lora/lib/microwaveScreen.py index 4587d4e..7e2238a 100644 --- a/micro_ondes/esp_lora/lib/microwaveScreen.py +++ b/micro_ondes/esp_lora/lib/microwaveScreen.py @@ -87,7 +87,8 @@ class MicrowaveScreen: if cooking_state and cooking_state.state == CookingStates.STIRRING_REQUIRED: self._message("Pls Stir", True) elif cooking_state and cooking_state.state == CookingStates.ALERT: - self._message("Alert !", True) + # nothing to do, the alert will be displayed by the alert manager + return elif cooking_state and cooking_state.state == CookingStates.DONE: self._message("Done !", False) elif microwave_state and microwave_state == MicrowaveState.ANALYZING: @@ -96,7 +97,10 @@ class MicrowaveScreen: elif microwave_state and microwave_state == MicrowaveState.WAITING_FOR_CLOUD: self.display.blit(fb_image_cloud_sync_bits, 55, 24) self._secondary_message("Receiving...", False) - else: # Progress bar + elif microwave_state and microwave_state == MicrowaveState.ALERT: + # nothing to do, the alert will be displayed by the alert manager + pass + elif cooking_state: # Progress bar elapsed_time = cooking_state.get_elapsed_time() estimated_progress = elapsed_time / (elapsed_time + cooking_state.estimated_remaining_time) self._progressBar(estimated_progress, 21, 25) @@ -132,7 +136,7 @@ class MicrowaveScreen: def _message_at(self, y, text, alert): self.display.fill_rect(0, y, self.width, CHARACTER_HEIGHT + 4, 1) if alert: - self.display.blit(fb_image_Alert_bits, 10, y+2) + self.display.blit(fb_image_Alert_bits, 1, y+2) self.display.blit(fb_image_Alert_bits, self.width - 10, y+2) self.h_centered_text(text, y+2, 0) diff --git a/micro_ondes/esp_lora/main.py b/micro_ondes/esp_lora/main.py index 3348ae7..10465b6 100644 --- a/micro_ondes/esp_lora/main.py +++ b/micro_ondes/esp_lora/main.py @@ -17,6 +17,7 @@ from shared.logging import log from shared.lora_device import LoraCommands from shared.alerts import Alert, AlertType, AlertManager from shared.microwave_state import MicrowaveState +from shared.payloads import lora_new_alert from lib.microwaveScreen import MicrowaveScreen # --- READ DEVICE ID --- @@ -76,7 +77,7 @@ def send_cooking_update(state=None, start_time=None, estimated_remaining_time=No 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) + microwave_screen.message(alert.small_message if alert.small_message else alert.message, True) def init_hardware(): """Initializes all hardware components.""" @@ -131,7 +132,7 @@ async def lora_rx_and_heartbeat_task(): # 2. Listen for incoming packets (Must be longer than LoRa Time-on-Air) if lora: - paquet = lora.receive_reliable(timeout_ms=350) + paquet = lora.receive_reliable(timeout_ms=500) if paquet is not None: log(f"[LoRa Task] New Packet Received: {paquet}") data_queue.put(paquet) @@ -169,6 +170,21 @@ def cooking_state_temperature_provider(): temps = [float(current_temp[0]), float(current_temp[1])] last_temp = [temps[0], temps[1]] current_temp = [None, None] + + if config.DEBUG_TEMPERATURE_ALERT or temps[0] > config.MAX_ALLOWED_TEMPERATURE_COOKING_COMPARTMENT or temps[1] > config.MAX_ALLOWED_TEMPERATURE_TECHNICAL_COMPARTMENT: + print(f"[CookingState] ALERT: Unsafe temperature detected! Dish: {temps[0]}°C, Ambient: {temps[1]}°C") + # Send alert to orchestrator via LoRa + alert = Alert( + AlertType.TEMPERATURE_ALERT, + f"Unsafe temperature detected! Dish: {temps[0]}/{config.MAX_ALLOWED_TEMPERATURE_COOKING_COMPARTMENT}°C, Ambient: {temps[1]}/{config.MAX_ALLOWED_TEMPERATURE_TECHNICAL_COMPARTMENT}°C", + small_message=f"Temp too high" + ) + alert_manager.add_alert(alert) + if lora: + lora.send_reliable(lora_new_alert(alert, with_message=True), max_retries=5) + cooking_state.set_state(cookingState.CookingStates.ALERT) + cooking_state.pause() + return temps # 4. Fallback: Use last valid reading @@ -182,7 +198,7 @@ def cooking_state_on_state_change(state): global cooking_start_time print(f"[CookingState] State changed to: {state.state}") - if state.paused or state.state == cookingState.CookingStates.DONE or state.state == cookingState.CookingStates.IDLE: + if state.paused or state.state in (cookingState.CookingStates.DONE, cookingState.CookingStates.IDLE, cookingState.CookingStates.ALERT): magnetron_led.off() else: magnetron_led.on() @@ -291,7 +307,8 @@ async def lora_process_task(): continue if "action" in data: - if data["action"] == LoraCommands.TOGGLE_PAUSE: + action = data["action"] + if action == LoraCommands.TOGGLE_PAUSE: if cooking_state is not None: if cooking_state.state == cookingState.CookingStates.DONE: print("[LoRa Process] Cooking is done. Resetting microwave for next session.") @@ -304,23 +321,29 @@ async def lora_process_task(): else: log("[LoRa Process] No active cooking state to toggle pause/resume.") - elif data["action"] == LoraCommands.TOGGLE_DEFROST: + elif action == LoraCommands.TOGGLE_DEFROST: print("[LoRa Process] Toggling defrost mode via orchestrator command.") defrost_mode = data.get("defrost_state", False) update_screen() - 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) + elif action == LoraCommands.NEW_ALERT: + alert: Alert | None = None + if data.get("alert_type") == AlertType.TEMPERATURE_ALERT: + alert = Alert(data.get("alert_type"), data.get("alert_message", "Temp too high"), redistributed=True) + elif data.get("alert_type") == AlertType.COOKING_SAFETY: + alert = Alert(data.get("alert_type"), data.get("alert_message", "Unsafe area"), redistributed=True) + + if alert: + alert.timestamp = data.get("timestamp", time.time()) + alert_manager.add_alert(alert) - elif data["action"] == LoraCommands.MICROVAVE_STATE_UPDATE: + elif action == LoraCommands.MICROVAVE_STATE_UPDATE: new_state = data.get("new_microwave_state") if new_state: print(f"[LoRa Process] Microwave state update received: {new_state}") microwave_state = new_state - update_screen() + if new_state not in (MicrowaveState.ALERT): # Not alert so we can show the error + update_screen() await asyncio.sleep_ms(100) diff --git a/orchestrateur/main.py b/orchestrateur/main.py index 33ef0a5..d241c6e 100644 --- a/orchestrateur/main.py +++ b/orchestrateur/main.py @@ -85,7 +85,7 @@ def on_microwave_change(state: MicrowaveState, field: MicrowaveStateFields): if field != MicrowaveStateFields.STATE: # No external screen or lcd uses this field, so we can skip updates for it notify_display_update() - if field == MicrowaveStateFields.STATE and state.state in (MicrowaveState.DONE, MicrowaveState.ANALYZING, MicrowaveState.WAITING_FOR_CLOUD): + if field == MicrowaveStateFields.STATE and state.state in (MicrowaveState.DONE, MicrowaveState.ANALYZING, MicrowaveState.WAITING_FOR_CLOUD, MicrowaveState.ALERT): # Send to LoRa device if state changed lora.send_reliable(payloads.lora_microwave_state(state.state), max_retries=5) @@ -266,7 +266,7 @@ async def display_broadcast_worker_task(): def button_callback(): """Button physical interrupt callback.""" global button_state - if microwave_states.get("2").cooking_state == CookingStates.COOKING or microwave_states.get("2").cooking_state == CookingStates.DONE or microwave_states.get("2").cooking_state == CookingStates.STIRRING_REQUIRED: + if microwave_states.get("2").cooking_state in (CookingStates.COOKING, CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT): print("[Button] Toggling pause/resume for microwave '2'.") lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_PAUSE}, max_retries=6) else: @@ -280,7 +280,7 @@ button.start_button_monitoring_thread() # --- Alert manager --- alert_manager = None -def on_new_alert(alert): +def on_new_alert(alert: Alert): # Sends to cloud server alert_dict = alert.to_dict() alert_dict["orchestrator_id"] = DEVICE_ID @@ -294,10 +294,12 @@ def on_new_alert(alert): 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)) - - microwave_states["2"].set_cooking_state(CookingStates.ALERT) + if not alert.redistributed: # If not redistributed from microwaves + # Send the alert to the lora device + payload = payloads.lora_new_alert(alert) + print(f"[Alert Handling] Sending alert to LoRa device: {payload}") + if lora.send_reliable(payload): + microwave_states["2"].set_cooking_state(CookingStates.ALERT) # --- HARDWARE CONTROLLERS --- def _stop_hardware(microwave_id: str): @@ -320,6 +322,14 @@ def read_local_sensors(microwave_id, initial_dish_height): if temp is not None: sensor_data["temperature"] = temp sensor_data["humidity"] = hum + if config.DEBUG_TEMPERATURE_ALERT or temp > config.MAX_ALLOWED_TEMPERATURE_TECHNICAL_COMPARTMENT: + print(f"[{microwave_id}] ALERT: Unsafe temperature in technical compartment detected! Temp: {temp}°C, Humidity: {hum}%") + alert = Alert( + AlertType.TEMPERATURE_ALERT, + f"Unsafe temperature in technical compartment detected! Temp: {temp}/{config.MAX_ALLOWED_TEMPERATURE_COOKING_COMPARTMENT}°C, Humidity: {hum}%" + ) + lora.send_reliable(payloads.lora_new_alert(alert), max_retries=5) + return None # Abort further processing if unsafe temperature detected except Exception as e: log(f"[{microwave_id}] DHT read warning: {e}") @@ -354,6 +364,11 @@ async def handle_new_dish(microwave_id, detected_height): # 4. Wait for local sensors to finish reading sensors_data = await sensor_task + + if sensors_data is None: + print(f"[{microwave_id}] Aborting cooking plan because of empty sensor data") + microwave_states[microwave_id].set_state(MicrowaveState.ALERT) + return # Check if dish was removed while reading sensors if microwave_states.get(microwave_id).state != MicrowaveState.ANALYZING: @@ -510,6 +525,9 @@ async def process_messages_task(): microwave_states[mw_id].set_state(MicrowaveState.DONE) print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.") if "action" in data.get("data", {}): + if type(data["data"]) != dict: + print(f"[LoRa] Invalid data format received: {data['data']}") + continue action = data["data"]["action"] if action == LoraCommands.COOKING_UPDATE: mw_id = data["data"].get("id") @@ -518,6 +536,12 @@ async def process_messages_task(): microwave_states[mw_id].set_cooking_state(state_info.get("cooking_state", microwave_states[mw_id].cooking_state)) microwave_states[mw_id].set_cooking_estimated_remaining_time(state_info.get("estimated_remaining_time", microwave_states[mw_id].cooking_estimated_remaining_time)) microwave_states[mw_id].set_paused(state_info.get("paused", microwave_states[mw_id].paused)) + elif action == LoraCommands.NEW_ALERT: + alert_type = data["data"].get("alert_type") + alert_message = data["data"].get("alert_message") + alert = Alert(alert_type, alert_message, redistributed=True) + alert_manager.add_alert(alert) + print(f"[LoRa] New alert received: {alert.to_dict()}") else: print(f"[LoRa] Unhandled action received: {action} with data: {data['data']}") elif source == "MQTT": diff --git a/shared/alerts.py b/shared/alerts.py index 6c358fe..84fe1c7 100644 --- a/shared/alerts.py +++ b/shared/alerts.py @@ -20,10 +20,12 @@ class AlertManager: self.alerts.clear() class Alert: - def __init__(self, alertType: "AlertType", message: str): + def __init__(self, alertType: "AlertType", message: str, redistributed: bool = False, small_message: str = None): self.alertType = alertType self.message = message + self.small_message = small_message self.timestamp = time.time() + self.redistributed = redistributed def to_dict(self): return { @@ -44,4 +46,5 @@ class Alert: return alert class AlertType: - COOKING_SAFETY = "COOKING_SAFETY" \ No newline at end of file + COOKING_SAFETY = "COOKING_SAFETY" + TEMPERATURE_ALERT = "TEMPERATURE_ALERT" \ No newline at end of file diff --git a/shared/config.py b/shared/config.py index de492d2..bdee0f9 100644 --- a/shared/config.py +++ b/shared/config.py @@ -1,6 +1,7 @@ # Debugging and others DEBUG=True DEBUG_DANGEROUS_AREA=False +DEBUG_TEMPERATURE_ALERT=True # LoRa LORA_HEARTBEAT_INTERVAL = 30 @@ -19,8 +20,9 @@ MQTT_HELLO_INTERVAL = 30 # Microwave Model COOKING_COMPARTMENT_HEIGHT = 30 # cm - BUZZER_ACTIVATED = False +MAX_ALLOWED_TEMPERATURE_TECHNICAL_COMPARTMENT = 70 +MAX_ALLOWED_TEMPERATURE_COOKING_COMPARTMENT = 200 # TELEMETRY TELEMETRY_SEND_INTERVAL = 3 \ No newline at end of file diff --git a/shared/microwave_state.py b/shared/microwave_state.py index c6f6bff..3a37c65 100644 --- a/shared/microwave_state.py +++ b/shared/microwave_state.py @@ -33,10 +33,11 @@ class MicrowaveState: if config.DEBUG: assert new_state in ( MicrowaveState.IDLE, - MicrowaveState.ANALYZING, + MicrowaveState.ANALYZING, MicrowaveState.WAITING_FOR_CLOUD, MicrowaveState.COOKING, - MicrowaveState.DONE + MicrowaveState.DONE, + MicrowaveState.ALERT, ), f"Invalid state: {new_state}" if self.state != new_state: @@ -90,4 +91,5 @@ class MicrowaveState: ANALYZING = "ANALYZING" # Reading sensors & waiting for IR WAITING_FOR_CLOUD = "WAITING_FOR_CLOUD" # Waiting for API parameters COOKING = "COOKING" # Microwave is active - DONE = "DONE" # Finished/Stopped, waiting for dish removal \ No newline at end of file + DONE = "DONE" # Finished/Stopped, waiting for dish removal + ALERT = "ALERT" # Alert state, waiting for user action \ No newline at end of file diff --git a/shared/payloads.py b/shared/payloads.py index f728c48..90225d0 100644 --- a/shared/payloads.py +++ b/shared/payloads.py @@ -69,17 +69,22 @@ def telemetry_payload(device_id, microwave_states, button_state, cloud_alert, gp "logs": logs }) -def lora_new_alert(alert: Alert): +def lora_new_alert(alert: Alert, with_message=False): try: from shared.lora_device import LoraCommands except ImportError: pass # No need - return { + payload = { "action": LoraCommands.NEW_ALERT, "alert_type": alert.alertType } + if with_message: + payload["alert_message"] = alert.message + + return payload + def lora_microwave_state(state: str): try: from shared.lora_device import LoraCommands