ESP-LORA refactor
This commit is contained in:
+189
-122
@@ -1,181 +1,248 @@
|
|||||||
|
import gc
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import _thread
|
import _thread
|
||||||
|
import uasyncio as asyncio
|
||||||
from machine import Pin, SoftI2C
|
from machine import Pin, SoftI2C
|
||||||
|
import framebuf
|
||||||
|
import ssd1306
|
||||||
|
|
||||||
|
# Clean memory immediately
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
from shared.safeQueue import SafeQueue
|
from shared.safeQueue import SafeQueue
|
||||||
from shared import get_lora, get_uart, deviceTypes, config, cookingState
|
from shared import get_lora, get_uart, deviceTypes, config, cookingState
|
||||||
from shared.uart_comm import UARTCommand, UARTCommandType
|
from shared.uart_comm import UARTCommand, UARTCommandType
|
||||||
from shared.sensors import RGBLED
|
from shared.sensors import RGBLED
|
||||||
from shared.logging import log
|
from shared.logging import log
|
||||||
from shared.lora_device import LoraCommands
|
from shared.lora_device import LoraCommands
|
||||||
import framebuf
|
|
||||||
import ssd1306
|
|
||||||
import time
|
|
||||||
|
|
||||||
# --- Configuration Matérielle ---
|
# --- READ DEVICE ID ---
|
||||||
vext = Pin(19, Pin.OUT)
|
|
||||||
vext.value(0)
|
|
||||||
time.sleep_ms(100)
|
|
||||||
|
|
||||||
# --- Lecture de l'ID unique de l'ESP ---
|
|
||||||
try:
|
try:
|
||||||
with open("device_id.txt", "r") as f:
|
with open("device_id.txt", "r") as f:
|
||||||
DEVICE_ID = f.read().strip()
|
DEVICE_ID = f.read().strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
DEVICE_ID = "ESP32_Inconnu"
|
DEVICE_ID = "ESP32_Inconnu"
|
||||||
|
|
||||||
# --- Initialisation LoRa ---
|
# --- GLOBAL VARIABLES ---
|
||||||
lora = get_lora()
|
cooking_state = None
|
||||||
lora.configure(freq=868.1, sf=7)
|
|
||||||
data_queue = SafeQueue()
|
data_queue = SafeQueue()
|
||||||
|
lora = None
|
||||||
# --- Création des lEDs RGB ---
|
uart_device = None
|
||||||
magnetron_led = RGBLED(red_pin=48, green_pin=47, blue_pin=33)
|
magnetron_led = None
|
||||||
magnetron_led.color = RGBLED.WHITE_YELLOW
|
display = None
|
||||||
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']})")
|
|
||||||
|
|
||||||
PING_PAYLOAD = {
|
PING_PAYLOAD = {
|
||||||
"id": DEVICE_ID,
|
"id": DEVICE_ID,
|
||||||
"type": deviceTypes.DEVICE_TYPES["MICROWAVE"]
|
"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
|
last_heartbeat_time = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# 1. Send periodic heartbeat
|
# 1. Send periodic heartbeat
|
||||||
if now - last_heartbeat_time >= config.LORA_HEARTBEAT_INTERVAL:
|
if now - last_heartbeat_time >= config.LORA_HEARTBEAT_INTERVAL:
|
||||||
last_heartbeat_time = now
|
last_heartbeat_time = now
|
||||||
print("\nESP32 : Envoi du Heartbeat...")
|
print("\n[LoRa Thread] Sending Heartbeat...")
|
||||||
lora.send(PING_PAYLOAD)
|
if lora:
|
||||||
|
lora.send(PING_PAYLOAD)
|
||||||
|
|
||||||
# 2. Increase listen window to 300ms so radio stays active in RX mode
|
# 2. Blocking 300ms RX listen window (keeps radio actively listening)
|
||||||
paquet = lora.receive_reliable(timeout_ms=300)
|
if lora:
|
||||||
|
paquet = lora.receive_reliable(timeout_ms=300)
|
||||||
if paquet is not None:
|
if paquet is not None:
|
||||||
log(f"[LoRa Thread] New Packet Received: {paquet}")
|
log(f"[LoRa Thread] New Packet Received: {paquet}")
|
||||||
data_queue.put(paquet)
|
data_queue.put(paquet)
|
||||||
|
|
||||||
time.sleep_ms(10)
|
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é
|
# --- COOKING STATE CALLBACKS ---
|
||||||
try:
|
|
||||||
_thread.stack_size(16 * 1024)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_thread.start_new_thread(heartbeat_loop, ())
|
|
||||||
|
|
||||||
# Cooking parameters
|
|
||||||
cooking_state = None
|
|
||||||
def cooking_state_temperature_provider():
|
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):
|
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:
|
if state.paused or state.state == cookingState.CookingStates.DONE or state.state == cookingState.CookingStates.IDLE:
|
||||||
magnetron_led.off()
|
magnetron_led.off()
|
||||||
else:
|
else:
|
||||||
magnetron_led.on()
|
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:
|
# Update OLED display
|
||||||
pass
|
if display:
|
||||||
if state.state == cookingState.CookingStates.STIRRING_REQUIRED:
|
display.fill(0)
|
||||||
pass
|
display.text(cookingState.CookingStates.get_state_name(state.state), 1, 2, 1)
|
||||||
if state.state == cookingState.CookingStates.DONE:
|
display.show()
|
||||||
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()
|
|
||||||
|
|
||||||
def cooking_state_on_refresh(state):
|
def cooking_state_on_refresh(state):
|
||||||
# TODO Show screen information
|
# TODO: Show screen information
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def cooking_state_on_pause(state):
|
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 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):
|
if not state.paused and (state.state == cookingState.CookingStates.STIRRING_REQUIRED or state.state == cookingState.CookingStates.ALERT):
|
||||||
state.set_state(cookingState.CookingStates.COOKING)
|
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
|
|
||||||
|
|
||||||
|
|
||||||
# --- MAIN APPLICATION THREAD ---
|
# --- ASYNC TASKS ---
|
||||||
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:
|
async def uart_polling_task():
|
||||||
print(f"[Main] Unknown command type received: {command.command_type}")
|
"""Polls UART for incoming messages from the WiFi board."""
|
||||||
# 2. Listen for incoming LoRa packets from the orchestrator
|
global cooking_state
|
||||||
while not data_queue.empty():
|
|
||||||
paquet = data_queue.get()
|
while True:
|
||||||
if paquet and not paquet["raw"]:
|
if uart_device and uart_device.any():
|
||||||
data = paquet["data"]
|
command = uart_device.read_as_command()
|
||||||
# Commands
|
if command:
|
||||||
if "action" in data:
|
print(f"[UART Task] Received command from WiFi Board: {command.command_type}")
|
||||||
if data["action"] == LoraCommands.TOGGLE_PAUSE:
|
if command.command_type == UARTCommandType.COOKING_PARAMS:
|
||||||
if cooking_state != None:
|
params = command.payload
|
||||||
if (cooking_state.state == cookingState.CookingStates.DONE):
|
print(f"[UART Task] Cooking parameters received: {params}")
|
||||||
print("[Main] Cooking is done. We reset the microwave for the next cooking session.")
|
|
||||||
cooking_state.set_state(cookingState.CookingStates.IDLE)
|
cooking_state = cookingState.CookingState(
|
||||||
time.sleep_ms(20) # Before sending back right away
|
cook_time=params["cook_time"],
|
||||||
cooking_state = None
|
power_level=params["power_level"],
|
||||||
else:
|
target_temp=params["target_temp"]
|
||||||
cooking_state.toggle_pause()
|
)
|
||||||
if cooking_state.paused:
|
cooking_state.set_temperature_provider(cooking_state_temperature_provider)
|
||||||
print("[Main] Cooking paused via orchestrator command.")
|
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:
|
else:
|
||||||
print("[Main] Cooking resumed via orchestrator command.")
|
cooking_state.toggle_pause()
|
||||||
else:
|
if cooking_state.paused:
|
||||||
log("[Main] No active cooking state to toggle pause/resume.")
|
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.")
|
||||||
|
|
||||||
# uart_device.send(f"Hello from esp-32 lora ID {DEVICE_ID}")
|
await asyncio.sleep_ms(50)
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
time.sleep_ms(500)
|
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)
|
||||||
Reference in New Issue
Block a user