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 # --- READ DEVICE ID --- try: with open("device_id.txt", "r") as f: DEVICE_ID = f.read().strip() except Exception: DEVICE_ID = "ESP32_Inconnu" # --- GLOBAL VARIABLES --- cooking_state = None data_queue = SafeQueue() lora = None uart_device = None magnetron_led = None display = None PING_PAYLOAD = { "id": DEVICE_ID, "type": deviceTypes.DEVICE_TYPES["MICROWAVE"] } 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("\n[LoRa Thread] Sending Heartbeat...") if lora: lora.send(PING_PAYLOAD) # 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) time.sleep_ms(10) # --- COOKING STATE CALLBACKS --- def cooking_state_temperature_provider(): return 22.0, 29.0 # TODO: Replace with real temperature reading def cooking_state_on_state_change(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}) # 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 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 # --- 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) 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: 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) 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) 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)