diff --git a/micro_ondes/esp_lora/main.py b/micro_ondes/esp_lora/main.py index 76ba5b6..e126b9f 100644 --- a/micro_ondes/esp_lora/main.py +++ b/micro_ondes/esp_lora/main.py @@ -1,181 +1,248 @@ +import gc +import sys +import time import _thread +import uasyncio as asyncio from machine import Pin, SoftI2C +import framebuf +import ssd1306 + +# Clean memory immediately +gc.collect() + from shared.safeQueue import SafeQueue 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 from shared.lora_device import LoraCommands -import framebuf -import ssd1306 -import time -# --- Configuration Matérielle --- -vext = Pin(19, Pin.OUT) -vext.value(0) -time.sleep_ms(100) - -# --- Lecture de l'ID unique de l'ESP --- +# --- READ DEVICE ID --- try: with open("device_id.txt", "r") as f: DEVICE_ID = f.read().strip() except Exception: DEVICE_ID = "ESP32_Inconnu" -# --- Initialisation LoRa --- -lora = get_lora() -lora.configure(freq=868.1, sf=7) +# --- GLOBAL VARIABLES --- +cooking_state = None data_queue = SafeQueue() - -# --- 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() - -# --- Création de l'écran OLED --- -scl_pin = Pin(18, Pin.OUT, pull=Pin.PULL_UP) -sda_pin = Pin(17, Pin.OUT, pull=Pin.PULL_UP) -display_i2c = SoftI2C(scl=scl_pin, sda=sda_pin, freq=100000) -display = ssd1306.SSD1306_I2C(128, 64, display_i2c, addr=0x3C) -display.text("Booting...", 1, 2, 1) -display.show() - -print(f"ESP32 initialisé avec l'ID : '{DEVICE_ID}' (Type : {deviceTypes.DEVICE_TYPES['MICROWAVE']})") +lora = None +uart_device = None +magnetron_led = None +display = None PING_PAYLOAD = { "id": DEVICE_ID, "type": deviceTypes.DEVICE_TYPES["MICROWAVE"] } -def heartbeat_loop(): +def init_hardware(): + """Initializes all hardware components.""" + global lora, uart_device, magnetron_led, display + + print("[Main] Initializing hardware peripherals...") + + # Power up VEXT (for LoRa/Display) + vext = Pin(19, Pin.OUT) + vext.value(0) + time.sleep_ms(100) + + # Init LoRa + lora = get_lora() + lora.configure(freq=868.1, sf=7) + + # Init RGB LEDs + magnetron_led = RGBLED(red_pin=48, green_pin=47, blue_pin=33) + magnetron_led.color = RGBLED.WHITE_YELLOW + magnetron_led.off() + + # Init OLED Display + scl_pin = Pin(18, Pin.OUT, pull=Pin.PULL_UP) + sda_pin = Pin(17, Pin.OUT, pull=Pin.PULL_UP) + display_i2c = SoftI2C(scl=scl_pin, sda=sda_pin, freq=100000) + display = ssd1306.SSD1306_I2C(128, 64, display_i2c, addr=0x3C) + display.text("Booting...", 1, 2, 1) + display.show() + + # Init UART + uart_device = get_uart(uart_id=1, tx_pin=46, rx_pin=45) + + print(f"[Main] ESP32 initialized with ID: '{DEVICE_ID}' (Type: {deviceTypes.DEVICE_TYPES['MICROWAVE']})") + + +# --- DEDICATED LORA HARDWARE THREAD --- +def lora_hardware_thread(): + """Runs in a separate OS thread to keep the LoRa radio in continuous RX mode.""" last_heartbeat_time = 0 + while True: now = time.time() # 1. Send periodic heartbeat if now - last_heartbeat_time >= config.LORA_HEARTBEAT_INTERVAL: last_heartbeat_time = now - print("\nESP32 : Envoi du Heartbeat...") - lora.send(PING_PAYLOAD) + print("\n[LoRa Thread] Sending Heartbeat...") + if lora: + lora.send(PING_PAYLOAD) - # 2. Increase listen window to 300ms so radio stays active in RX mode - paquet = lora.receive_reliable(timeout_ms=300) + # 2. Blocking 300ms RX listen window (keeps radio actively listening) + if lora: + paquet = lora.receive_reliable(timeout_ms=300) + if paquet is not None: + log(f"[LoRa Thread] New Packet Received: {paquet}") + data_queue.put(paquet) - if paquet is not None: - log(f"[LoRa Thread] New Packet Received: {paquet}") - data_queue.put(paquet) - time.sleep_ms(10) - -# UART -uart_device = get_uart(uart_id=1, tx_pin=46, rx_pin=45) -# Lancer la boucle de heartbeat dans un thread séparé -try: - _thread.stack_size(16 * 1024) -except Exception: - pass -_thread.start_new_thread(heartbeat_loop, ()) -# Cooking parameters -cooking_state = None +# --- COOKING STATE CALLBACKS --- 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 + return 22.0, 29.0 # TODO: Replace with real temperature reading def cooking_state_on_state_change(state): - print(f"[Main] Cooking state changed to: {state.state}") - + print(f"[CookingState] State changed to: {state.state}") if state.paused or state.state == cookingState.CookingStates.DONE or state.state == cookingState.CookingStates.IDLE: magnetron_led.off() else: magnetron_led.on() + # Send state updates to WiFi board and Orchestrator + if uart_device: + uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state})) + if lora: + lora.send_reliable({"id": DEVICE_ID, "new_cooking_state": state.state}) - if state.state == cookingState.CookingStates.COOKING: - pass - if state.state == cookingState.CookingStates.STIRRING_REQUIRED: - pass - if state.state == cookingState.CookingStates.DONE: - pass - if state.state == cookingState.CookingStates.ALERT: - pass - - # Send to the Wifi board the current state - uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state})) - # Send to the orchestrator the current state - lora.send_reliable({"id": DEVICE_ID, "new_cooking_state": state.state}) - - display.fill(0) - display.text(cookingState.CookingStates.get_state_name(state.state), 1, 2, 1) - display.show() + # Update OLED display + if display: + display.fill(0) + display.text(cookingState.CookingStates.get_state_name(state.state), 1, 2, 1) + display.show() def cooking_state_on_refresh(state): - # TODO Show screen information + # TODO: Show screen information pass 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. if not state.paused and (state.state == cookingState.CookingStates.STIRRING_REQUIRED or state.state == cookingState.CookingStates.ALERT): state.set_state(cookingState.CookingStates.COOKING) - - # TODO send_reliable lora message to orchestrator about pause/resume state + # TODO: send_reliable lora message to orchestrator about pause/resume state + + +# --- ASYNC TASKS --- + +async def uart_polling_task(): + """Polls UART for incoming messages from the WiFi board.""" + global cooking_state + while True: + if uart_device and uart_device.any(): + command = uart_device.read_as_command() + if command: + print(f"[UART Task] Received command from WiFi Board: {command.command_type}") + if command.command_type == UARTCommandType.COOKING_PARAMS: + params = command.payload + print(f"[UART Task] 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) + cooking_state.set_pause_callback(cooking_state_on_pause) + + await asyncio.sleep_ms(20) + cooking_state_on_state_change(cooking_state) + else: + print(f"[UART Task] Unknown command type received: {command.command_type}") + + await asyncio.sleep_ms(50) -# --- MAIN APPLICATION THREAD --- -print("[Main] Main execution path active.") -while True: - # 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 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) - cooking_state.set_pause_callback(cooking_state_on_pause) - 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}") - # 2. Listen for incoming LoRa packets from the orchestrator - while not data_queue.empty(): - paquet = data_queue.get() - if paquet and not paquet["raw"]: - data = paquet["data"] - # Commands - if "action" in data: - if data["action"] == LoraCommands.TOGGLE_PAUSE: - if cooking_state != None: - if (cooking_state.state == cookingState.CookingStates.DONE): - print("[Main] Cooking is done. We reset the microwave for the next cooking session.") - cooking_state.set_state(cookingState.CookingStates.IDLE) - time.sleep_ms(20) # Before sending back right away - cooking_state = None - else: - cooking_state.toggle_pause() - if cooking_state.paused: - print("[Main] Cooking paused via orchestrator command.") +async def lora_process_task(): + """Consumes packets pushed to data_queue by the LoRa hardware thread.""" + global cooking_state + + while True: + while not data_queue.empty(): + paquet = data_queue.get() + if paquet and not paquet.get("raw"): + data = paquet.get("data", {}) + + if "action" in data: + if data["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 the next session.") + cooking_state.set_state(cookingState.CookingStates.IDLE) + await asyncio.sleep_ms(20) + cooking_state = None else: - print("[Main] Cooking resumed via orchestrator command.") - else: - log("[Main] No active cooking state to toggle pause/resume.") + cooking_state.toggle_pause() + if cooking_state.paused: + print("[LoRa Process] Cooking paused via orchestrator command.") + else: + print("[LoRa Process] Cooking resumed via orchestrator command.") + else: + log("[LoRa Process] No active cooking state to toggle pause/resume.") + + await asyncio.sleep_ms(50) - # uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}") - # Cooking State Update - if cooking_state != None: - 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") +async def cooking_loop_task(): + """Ticks the cooking state and logs information periodically.""" + while True: + if cooking_state is not None: + cooking_state.update_tick() + print(f"[Cooking Task] State: {cooking_state.state}, Temp: {cooking_state.current_dish_temp}, " + f"Paused: {cooking_state.paused}, Remaining: {cooking_state.get_remaining_time():.2f}s, " + f"Est. Remaining: {cooking_state.get_remaining_time_estimation():.2f}s") + + await asyncio.sleep_ms(500) - time.sleep_ms(500) \ No newline at end of file + +async def memory_cleanup_task(): + """Periodically cleans up memory to prevent heap fragmentation.""" + while True: + gc.collect() + await asyncio.sleep(10) + + +# --- BOOTSTRAP --- +async def main(): + print("[Main] Starting application...") + + init_hardware() + + # Launch dedicated hardware thread for LoRa RX + try: + _thread.stack_size(16 * 1024) + except Exception: + pass + _thread.start_new_thread(lora_hardware_thread, ()) + print("[Main] LoRa hardware background thread started.") + + # Launch background async tasks + asyncio.create_task(uart_polling_task()) + asyncio.create_task(lora_process_task()) + asyncio.create_task(cooking_loop_task()) + asyncio.create_task(memory_cleanup_task()) + + print("[Main] All async tasks running concurrently!") + + # Keep main task alive indefinitely + while True: + await asyncio.sleep(3600) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("[Main] Program stopped by user.") + except Exception as e: + sys.print_exception(e) \ No newline at end of file