Files
Smartwave/micro_ondes/esp_lora/main.py
T
2026-08-06 15:44:03 +02:00

248 lines
8.6 KiB
Python

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)