Files
Smartwave/micro_ondes/esp_lora/main.py
T

298 lines
11 KiB
Python

import gc
import sys
import time
import _thread
from lib.microwaveScreen import MicrowaveScreen
import uasyncio as asyncio
from machine import Pin, SoftI2C
import ssd1306
import ujson
# 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
microwave_screen = None
defrost_mode = False
current_temp = [None, None]
last_temp = [None, None]
temperature_asked = False
PING_PAYLOAD = {
"id": DEVICE_ID,
"type": deviceTypes.DEVICE_TYPES["MICROWAVE"]
}
def init_hardware():
"""Initializes all hardware components."""
global lora, uart_device, magnetron_led, microwave_screen
print("[Main] Initializing hardware peripherals...")
# Power up VEXT (for LoRa/Display)
vext = Pin(19, Pin.OUT)
vext.value(0)
time.sleep_ms(100)
# 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)
microwave_screen = MicrowaveScreen(display)
microwave_screen.bootScreen()
# 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 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():
global current_temp, last_temp, temperature_asked, uart_device
def temp_is_none(temp):
return temp is None or temp[0] is None or temp[1] is None
# Return the current temperature if available,
# otherwise return the last recorded temperature
# and request a new reading from the WiFi board.
if temp_is_none(current_temp):
if temp_is_none(last_temp):
temps = (0.0, 0.0)
print("[CookingState] No temperature data available. Returning default (0.0, 0.0).")
else:
temps = [last_temp[0], last_temp[1]]
print(f"[CookingState] Returning last known temperature: {temps}")
# Asks the esp-wifi the temperature over UART
if not temperature_asked:
temperature_asked = True
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
print("[CookingState] Requested new temperature reading from WiFi board.")
else:
print(f"[CookingState] Returning current temperature: {current_temp}")
temps = [current_temp[0], current_temp[1]]
last_temp[0] = current_temp[0]
last_temp[1] = current_temp[1]
current_temp[0] = None
current_temp[1] = None
return temps
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})
microwave_screen.update(cooking_state, defrost_mode)
def cooking_state_on_refresh(state):
# Update OLED display
if microwave_screen:
microwave_screen.update(state, defrost_mode)
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, temperature_asked
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}")
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
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(200)
cooking_state_on_state_change(cooking_state)
elif command.command_type == UARTCommandType.TEMPERATURE_RESPONSE:
payload = ujson.loads(command.payload)
current_temp[0] = payload["dish_temp"]
current_temp[1] = payload["ambient_temp"]
temperature_asked = False
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, defrost_mode, microwave_screen
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.")
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)
await asyncio.sleep_ms(50)
async def cooking_loop_task():
"""Ticks the cooking state and logs information periodically without flooding output."""
log_counter = 0
while True:
if cooking_state is not None:
cooking_state.update_tick()
log_counter += 1
# Print log output every 5 seconds (10 ticks x 500ms)
if log_counter % 10 == 0:
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")
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():
global cooking_state, defrost_mode, microwave_screen
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!")
microwave_screen.update(None, defrost_mode)
# 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)