diff --git a/micro_ondes/esp_lora/lib/microwaveScreen.py b/micro_ondes/esp_lora/lib/microwaveScreen.py index 1f5224b..4587d4e 100644 --- a/micro_ondes/esp_lora/lib/microwaveScreen.py +++ b/micro_ondes/esp_lora/lib/microwaveScreen.py @@ -2,6 +2,7 @@ import time from shared.cookingState import CookingState, CookingStates from shared import config import framebuf +from shared.microwave_state import MicrowaveState # Icon and bitmaps image_weather_frost_bits = bytearray(b'\x01\x00\x13\x901\x18s\x9c\x09 \x05@S\x94\xfe\xfeS\x94\x05@\x09 s\x9c1\x18\x13\x90\x01\x00\x00\x00') @@ -10,12 +11,16 @@ image_CoolHiSmall_bits = bytearray(b'\x1f\xff\xc0?\xff\xe0\x7f\xff\xf0\xff\xdf\x image_CoolLoSmall_bits = bytearray(b'\x1f\xff\xc0 \x00 @\x00\x10\x80 \x08\x80\xa8\x08\x82r\x08\x86#\x08\x81$\x08\x88\xa8\x88\x84q\x08\x9f\xff\xc8\x84q\x08\x88\xa8\x88\x81$\x08\x86#\x08\x82r\x08\x80\xa8\x08\x80 \x08@\x00\x10 \x00 \x1f\xff\xc0') image_music_sound_wave_bits = bytearray(b'\x00\x00\x00\x02\x00\x00\x02 \x00\x02 \x00\x02 \x00\x0a(\x00\x0a\xa8\x00*\xa8\x00\xaa\xaa\x80\x0a\xaa\x00\x0a\xa8\x00\x0a \x00\x02 \x00\x02 \x00\x02\x00\x00\x00\x00\x00') image_weather_temperature_bits = bytearray(b'\x1c\x00\x22\x00+\x00*\x00+\x00*\x00+\x00*\x00*\x00I\x00\x9c\x80\xae\x80\xbe\x80\x9c\x80A\x00>\x00') +image_camera_bits = bytearray(b'\x00x\x000\x84\x00\x7f{\x00\xc0\x00\x80\x91\xc6\x80\x860\x80\x85\xd0\x80\x8a(\x80\x8a(\x80\x8a(\x80\x85\xd0\x80\x860\x80\x91\xc6\x80\xc0\x00\x80\x7f\xff\x00\x00\x00\x00') +image_cloud_sync_bits = bytearray(b'\x00\x00\x00\x07\xc0\x00\x08 \x00\x10\x10\x000\x08\x00@\x0e\x00\x80\x01\x00\x82 \x80G \x80/\xaf\x00\x02 \x00\x02\xf8\x00\x02p\x00\x02 \x00\x00\x00\x00\x00\x00\x00') fb_image_weather_frost_bits = framebuf.FrameBuffer(image_weather_frost_bits, 15, 16, framebuf.MONO_HLSB) fb_image_CoolHiSmall_bits = framebuf.FrameBuffer(image_CoolHiSmall_bits, 21, 21, framebuf.MONO_HLSB) fb_image_weather_temperature_bits = framebuf.FrameBuffer(image_weather_temperature_bits, 9, 16, framebuf.MONO_HLSB) fb_image_Alert_bits = framebuf.FrameBuffer(image_Alert_bits, 9, 8, framebuf.MONO_HLSB) fb_image_music_sound_wave_bits = framebuf.FrameBuffer(image_music_sound_wave_bits, 17, 16, framebuf.MONO_HLSB) fb_image_CoolLoSmall_bits = framebuf.FrameBuffer(image_CoolLoSmall_bits, 21, 21, framebuf.MONO_HLSB) +fb_image_camera_bits = framebuf.FrameBuffer(image_camera_bits, 17, 16, framebuf.MONO_HLSB) +fb_image_cloud_sync_bits = framebuf.FrameBuffer(image_cloud_sync_bits, 17, 16, framebuf.MONO_HLSB) # Constants CHARACTER_WIDTH = 6 @@ -70,44 +75,50 @@ class MicrowaveScreen: self.display.fill_rect(x, y, fill_width, height, color) - def update(self, cooking_state: CookingState | None, defrost_mode: bool = False): + def update(self, cooking_state: CookingState | None, defrost_mode: bool = False, microwave_state: str | None = None): self.flick() # self.lastUpdateTime = time.time() # Not Used right now - if not cooking_state or cooking_state.state == CookingStates.IDLE: # Is not currently cooking + if (not microwave_state or microwave_state == MicrowaveState.DONE) and (not cooking_state or cooking_state.state == CookingStates.IDLE): # Is not currently cooking self.h_centered_text("Ready !", 28, 1) else: # Screen center - if cooking_state.state == CookingStates.STIRRING_REQUIRED: + if cooking_state and cooking_state.state == CookingStates.STIRRING_REQUIRED: self._message("Pls Stir", True) - elif cooking_state.state == CookingStates.ALERT: + elif cooking_state and cooking_state.state == CookingStates.ALERT: self._message("Alert !", True) - elif cooking_state.state == CookingStates.DONE: + elif cooking_state and cooking_state.state == CookingStates.DONE: self._message("Done !", False) + elif microwave_state and microwave_state == MicrowaveState.ANALYZING: + self.display.blit(fb_image_camera_bits, 56, 24) + self._secondary_message("Analyzing...", False) + 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 elapsed_time = cooking_state.get_elapsed_time() estimated_progress = elapsed_time / (elapsed_time + cooking_state.estimated_remaining_time) self._progressBar(estimated_progress, 21, 25) # Estimated remaining time - if cooking_state.paused: + if cooking_state and cooking_state.paused: self.h_centered_text("Paused", 40, 1) - elif cooking_state.state != CookingStates.DONE: + elif cooking_state and cooking_state.state != CookingStates.DONE: remaining_time = int(cooking_state.estimated_remaining_time) minutes = remaining_time // 60 seconds = remaining_time % 60 self.h_centered_text(f"{minutes:02}:{seconds:02}", 40, 1) # Current dish temperature - if cooking_state.current_dish_temp is not None and cooking_state.current_dish_temp is not 0.0: + if cooking_state and cooking_state.current_dish_temp is not None and cooking_state.current_dish_temp is not 0.0: self.display.blit(fb_image_weather_temperature_bits, 27, 2) self.display.text(str(int(cooking_state.current_dish_temp)), 37, 6, 1) self.display.ellipse(54, 6, 1, 1, 1, False) self.display.text("C", 56, 6, 1) # Power level - if cooking_state.power_level is not None: + if cooking_state and cooking_state.power_level is not None: self.display.blit(fb_image_music_sound_wave_bits, 74, 2) self.display.text(f"{cooking_state.power_level}W", 92, 6, 1) if defrost_mode: @@ -118,12 +129,18 @@ class MicrowaveScreen: self.show() pass - def _message(self, text, alert): - self.display.fill_rect(0, 27, self.width, CHARACTER_HEIGHT + 4, 1) + 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, 29) - self.display.blit(fb_image_Alert_bits, self.width - 10, 29) - self.h_centered_text(text, 29, 0) + self.display.blit(fb_image_Alert_bits, 10, y+2) + self.display.blit(fb_image_Alert_bits, self.width - 10, y+2) + self.h_centered_text(text, y+2, 0) + + def _message(self, text, alert): + self._message_at(27, text, alert) + + def _secondary_message(self, text, alert): + self._message_at(40, text, alert) def message(self, text, alert): # self.flick() diff --git a/micro_ondes/esp_lora/main.py b/micro_ondes/esp_lora/main.py index 03205fe..6ee9cfe 100644 --- a/micro_ondes/esp_lora/main.py +++ b/micro_ondes/esp_lora/main.py @@ -18,6 +18,7 @@ from shared.sensors import RGBLED from shared.logging import log from shared.lora_device import LoraCommands from shared.alerts import Alert, AlertType, AlertManager +from shared.microwave_state import MicrowaveState # --- READ DEVICE ID --- try: @@ -29,6 +30,7 @@ except Exception: # --- GLOBAL VARIABLES --- cooking_state = None cooking_start_time = None # Track start timestamp +microwave_state: str = None data_queue = SafeQueue() lora = None uart_device = None @@ -196,12 +198,11 @@ def cooking_state_on_state_change(state): paused=state.paused ) - microwave_screen.update(cooking_state, defrost_mode) + update_screen() def cooking_state_on_refresh(state): # Update OLED display - if microwave_screen: - microwave_screen.update(state, defrost_mode) + update_screen() def cooking_state_on_pause(state): # If the cooking is unpaused and was in STIRRING_REQUIRED or ALERT state, we set the state back to COOKING. @@ -215,6 +216,13 @@ def cooking_state_on_pause(state): estimated_remaining_time=state.get_remaining_time() ) +# --- SCREEN UPDATE --- +def update_screen(): + global microwave_screen, cooking_state, defrost_mode, microwave_state + + if microwave_screen: + microwave_screen.update(cooking_state, defrost_mode, microwave_state) + # --- ASYNC TASKS --- @@ -266,7 +274,7 @@ async def uart_polling_task(): async def lora_process_task(): """Consumes packets pushed to data_queue by the LoRa hardware thread.""" - global cooking_state, defrost_mode, microwave_screen + global cooking_state, defrost_mode, microwave_screen, microwave_state while True: while not data_queue.empty(): @@ -293,12 +301,21 @@ async def lora_process_task(): elif data["action"] == LoraCommands.TOGGLE_DEFROST: print("[LoRa Process] Toggling defrost mode via orchestrator command.") defrost_mode = data["defrost_state"] - microwave_screen.update(cooking_state, defrost_mode) + 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 data["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() + else: + print("[LoRa Process] Received microwave state update with no state specified.") + await asyncio.sleep_ms(100) @@ -358,8 +375,7 @@ async def main(): print("[Main] All async tasks running concurrently!") - microwave_screen.update(None, defrost_mode) - + update_screen() # Keep main task alive indefinitely while True: await asyncio.sleep(3600) diff --git a/orchestrateur/main.py b/orchestrateur/main.py index 2b2bf6e..6b1702e 100644 --- a/orchestrateur/main.py +++ b/orchestrateur/main.py @@ -81,6 +81,15 @@ def notify_display_update(): except RuntimeError: pass +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): + # Send to LoRa device if state changed + lora.send_reliable(payloads.lora_microwave_state(state.state), max_retries=5) + + def set_cloud_alert(state: bool): """Setter for cloud_alert that triggers a display update when changed.""" global cloud_alert @@ -91,7 +100,7 @@ def set_cloud_alert(state: bool): update_lcd_cloud_alert() # Global state trackers -microwave_states = {"2": MicrowaveState("2", on_change_callback=notify_display_update)} +microwave_states = {"2": MicrowaveState("2", on_change_callback=on_microwave_change)} button_state = False cloud_alert = None # Global status flag for screen / UI display async_event_queue = None @@ -434,7 +443,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].set_state(MicrowaveState.IDLE) + microwave_states[microwave_id].set_state(MicrowaveState.DONE) return print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W") @@ -530,7 +539,7 @@ async def process_messages_task(): # Pass change callback to newly connected microwave microwave_states[component_id] = MicrowaveState( component_id, - on_change_callback=notify_display_update + on_change_callback=on_microwave_change ) notify_display_update() # Update the external screens update_lcd_microwave_count() # Update the LCD with new microwave count @@ -641,8 +650,11 @@ async def monitor_dish_height_task(): while True: dist = await get_filtered_dish_height() - current_state = microwave_states.get(mw_id, MicrowaveState.IDLE).state - + try: + current_state = microwave_states.get(mw_id).state + except AttributeError: + current_state = MicrowaveState.IDLE + if dist is not None: # Hysteresis Thresholds: # - Must be > 2.5 cm to detect dish insertion diff --git a/shared/__init__.py b/shared/__init__.py index 9432715..a1b0f44 100644 --- a/shared/__init__.py +++ b/shared/__init__.py @@ -7,6 +7,7 @@ import shared.payloads as payloads import shared.cookingState as cookingState import shared.safeQueue as safeQueue import shared.alerts as alerts +import shared.microwave_state as microwave_state try: import shared.lora_device as lora_device except (ImportError, SyntaxError): diff --git a/shared/payloads.py b/shared/payloads.py index f73ac65..f728c48 100644 --- a/shared/payloads.py +++ b/shared/payloads.py @@ -78,4 +78,15 @@ def lora_new_alert(alert: Alert): return { "action": LoraCommands.NEW_ALERT, "alert_type": alert.alertType + } + +def lora_microwave_state(state: str): + try: + from shared.lora_device import LoraCommands + except ImportError: + pass # No need + + return { + "action": LoraCommands.MICROVAVE_STATE_UPDATE, + "new_microwave_state": state } \ No newline at end of file