Asyncio refactor for wifi and rpi
This commit is contained in:
+141
-96
@@ -5,7 +5,7 @@ import ujson as json
|
||||
import uasyncio as asyncio
|
||||
from machine import Pin, I2C
|
||||
|
||||
# 1. Clean memory immediately
|
||||
# 1. Clean memory immediately before performing any operations
|
||||
gc.collect()
|
||||
|
||||
# --- READ DEVICE ID ---
|
||||
@@ -19,8 +19,13 @@ except Exception:
|
||||
orchestrator_id = None
|
||||
cooking_state = None
|
||||
mqtt_connected = False
|
||||
unsubscribed_hello = False
|
||||
|
||||
# --- MQTT SETUP (Initialized First!) ---
|
||||
# --- ASYNC SIGNALS & QUEUES ---
|
||||
# Event to signal when orchestrator requests sensor data (prevents MQTT lock deadlock)
|
||||
sensor_request_event = None
|
||||
|
||||
# --- MQTT SETUP ---
|
||||
from shared import get_mqtt_client, config, payloads
|
||||
|
||||
MQTT_CA_FILE = "/certs/ca.crt"
|
||||
@@ -34,7 +39,6 @@ mqtt_client = get_mqtt_client(
|
||||
)
|
||||
|
||||
# --- HARDWARE & MODULE DEFERRED IMPORTS ---
|
||||
# We declare variables here, but initialize them AFTER MQTT connects
|
||||
status_led = None
|
||||
uart_device = None
|
||||
mlx_temperature_sensor = None
|
||||
@@ -45,13 +49,12 @@ UARTCommandType = None
|
||||
|
||||
|
||||
def init_hardware():
|
||||
"""Initializes hardware peripherals AFTER MQTT TLS has reserved its memory."""
|
||||
"""Initializes hardware peripherals AFTER MQTT TLS has reserved its RAM."""
|
||||
global status_led, uart_device, mlx_temperature_sensor
|
||||
global cookingState, log, UARTCommand, UARTCommandType
|
||||
|
||||
|
||||
print("[Main] Initializing hardware peripherals...")
|
||||
|
||||
# Deferred module imports
|
||||
|
||||
from shared import get_uart, cookingState as cs, logging
|
||||
from shared.uart_comm import UARTCommand as UC, UARTCommandType as UCT
|
||||
from shared.sensors import RGBLED
|
||||
@@ -62,13 +65,9 @@ def init_hardware():
|
||||
UARTCommand = UC
|
||||
UARTCommandType = UCT
|
||||
|
||||
# Status LED
|
||||
status_led = RGBLED(red_pin=21, green_pin=19, blue_pin=18)
|
||||
|
||||
# Hardware UART 2
|
||||
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
|
||||
|
||||
# I2C Temperature Sensor
|
||||
temperature_sensor_i2c = I2C(
|
||||
0,
|
||||
scl=Pin(25, Pin.IN, Pin.PULL_UP),
|
||||
@@ -83,51 +82,46 @@ def init_hardware():
|
||||
mlx_temperature_sensor = temperature_sensor.MLX90614(temperature_sensor_i2c)
|
||||
|
||||
|
||||
# --- CALLBACKS ---
|
||||
def on_received_cooking_state_update(state, is_error=False, is_terminated=False):
|
||||
"""Callback executed when state changes are received from the LoRa board over UART."""
|
||||
if cooking_state:
|
||||
if is_error:
|
||||
cooking_state.set_state(cookingState.CookingStates.ERROR)
|
||||
elif is_terminated:
|
||||
cooking_state.set_state(cookingState.CookingStates.ABORTED)
|
||||
else:
|
||||
cooking_state.set_state(state)
|
||||
|
||||
|
||||
def on_cooking_state_change(state):
|
||||
if status_led is None:
|
||||
return
|
||||
print(f"[Main] Cooking state changed to: {state.state}")
|
||||
BLINK_INTERVAL_MS = 500
|
||||
|
||||
from shared.sensors import RGBLED
|
||||
if state.state == cookingState.CookingStates.IDLE:
|
||||
status_led.color = RGBLED.OFF
|
||||
status_led.blink_off()
|
||||
elif state.state == cookingState.CookingStates.COOKING:
|
||||
status_led.color = RGBLED.YELLOW
|
||||
status_led.blink_off()
|
||||
elif state.state == cookingState.CookingStates.STIRRING_REQUIRED:
|
||||
status_led.color = RGBLED.ORANGE
|
||||
status_led.blink_on(BLINK_INTERVAL_MS)
|
||||
elif state.state == cookingState.CookingStates.DONE:
|
||||
status_led.color = RGBLED.GREEN
|
||||
status_led.blink_off()
|
||||
elif state.state == cookingState.CookingStates.ALERT:
|
||||
status_led.color = RGBLED.RED
|
||||
status_led.blink_on(BLINK_INTERVAL_MS)
|
||||
|
||||
|
||||
def on_received_cooking_state_update(new_state):
|
||||
global cooking_state
|
||||
if cooking_state is None:
|
||||
print("[Main] No active cooking state to update.")
|
||||
return
|
||||
if log:
|
||||
log(f"[Main] Updating cooking state to: {new_state}")
|
||||
cooking_state.set_state(new_state)
|
||||
"""Callback executed whenever local cooking state transitions."""
|
||||
if status_led and cookingState:
|
||||
if state == cookingState.CookingStates.IDLE:
|
||||
status_led.set_color(0, 0, 0) # Off
|
||||
elif state == cookingState.CookingStates.PREHEATING:
|
||||
status_led.set_color(255, 165, 0) # Orange
|
||||
elif state == cookingState.CookingStates.COOKING:
|
||||
status_led.set_color(255, 0, 0) # Red
|
||||
elif state == cookingState.CookingStates.DONE:
|
||||
status_led.set_color(0, 255, 0) # Green
|
||||
elif state in (
|
||||
cookingState.CookingStates.ERROR,
|
||||
cookingState.CookingStates.ABORTED,
|
||||
):
|
||||
status_led.set_color(255, 0, 255) # Magenta/Purple
|
||||
|
||||
|
||||
def on_mqtt_message(message):
|
||||
global orchestrator_id, cooking_state
|
||||
"""Sync callback: Lightweight! Only updates variables or triggers async signals."""
|
||||
global orchestrator_id, cooking_state, unsubscribed_hello
|
||||
print("[MQTT] Received message on topic:", message.get("topic"))
|
||||
|
||||
|
||||
payload_data = None
|
||||
try:
|
||||
payload_data = json.loads(message["payload"])
|
||||
except Exception as e:
|
||||
print("[MQTT] Payload parsing warning:", e)
|
||||
|
||||
|
||||
topic = message.get("topic")
|
||||
|
||||
# 1. Orchestrator Hello Response
|
||||
@@ -138,12 +132,13 @@ def on_mqtt_message(message):
|
||||
):
|
||||
orchestrator_id = payload_data.get("id_orchestrator")
|
||||
print("[MQTT] Hello response received from orchestrator:", orchestrator_id)
|
||||
try:
|
||||
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
|
||||
print("[MQTT] Unsubscribed from topic:", config.MQTT_TOPIC_HELLO)
|
||||
except Exception as e:
|
||||
print("[MQTT] Unsubscribe error:", e)
|
||||
sys.print_exception(e)
|
||||
if not unsubscribed_hello:
|
||||
unsubscribed_hello = True
|
||||
try:
|
||||
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
|
||||
print("[MQTT] Successfully unsubscribed from topic:", config.MQTT_TOPIC_HELLO)
|
||||
except Exception as e:
|
||||
print("[MQTT] Unsubscribe error:", e)
|
||||
|
||||
# 2. Cooking Parameters / Sensor Request
|
||||
elif (
|
||||
@@ -152,14 +147,9 @@ def on_mqtt_message(message):
|
||||
and payload_data.get("id_microwave") == DEVICE_ID
|
||||
):
|
||||
if "cook_time" not in payload_data:
|
||||
print("[MQTT] Sensor data requested by orchestrator.")
|
||||
obj_temp = mlx_temperature_sensor.read_object_temp() if mlx_temperature_sensor else 0
|
||||
amb_temp = mlx_temperature_sensor.read_ambient_temp() if mlx_temperature_sensor else 0
|
||||
|
||||
sensor_payload = payloads.mqtt_sensor_data(DEVICE_ID, obj_temp, amb_temp)
|
||||
mqtt_client.publish(
|
||||
config.MQTT_TOPIC_SENSOR, sensor_payload, qos=config.MQTT_QOS
|
||||
)
|
||||
print("[MQTT] Sensor data requested! Triggering async publisher...")
|
||||
# Trigger async event instead of calling publish() directly inside lock context!
|
||||
sensor_request_event.set()
|
||||
else:
|
||||
print("[MQTT] Cooking parameters received:", payload_data)
|
||||
if cookingState:
|
||||
@@ -170,26 +160,89 @@ def on_mqtt_message(message):
|
||||
)
|
||||
cooking_state.set_state_change_callback(on_cooking_state_change)
|
||||
cooking_state.set_state(cookingState.CookingStates.IDLE)
|
||||
|
||||
|
||||
if uart_device and UARTCommand:
|
||||
uart_device.send_as_command(
|
||||
UARTCommand(UARTCommandType.COOKING_PARAMS, payload_data)
|
||||
)
|
||||
print("[MQTT] Cooking parameters sent to LoRa board.")
|
||||
print("[MQTT] Cooking parameters sent to LoRa board over UART.")
|
||||
|
||||
|
||||
mqtt_client.set_callback(on_mqtt_message)
|
||||
# --- DEDICATED ASYNC TASK FOR SENSOR PUBLISHING ---
|
||||
async def sensor_publisher_task():
|
||||
"""Waits for sensor_request_event, reads hardware, and publishes outside the MQTT lock."""
|
||||
while True:
|
||||
await sensor_request_event.wait()
|
||||
sensor_request_event.clear()
|
||||
|
||||
print("[Sensor Task] Reading temperature sensors...")
|
||||
obj_temp = (
|
||||
mlx_temperature_sensor.read_object_temp()
|
||||
if mlx_temperature_sensor
|
||||
else 0
|
||||
)
|
||||
amb_temp = (
|
||||
mlx_temperature_sensor.read_ambient_temp()
|
||||
if mlx_temperature_sensor
|
||||
else 0
|
||||
)
|
||||
|
||||
sensor_payload = payloads.mqtt_sensor_data(DEVICE_ID, obj_temp, amb_temp)
|
||||
|
||||
try:
|
||||
print("[Sensor Task] Publishing sensor data to MQTT...")
|
||||
mqtt_client.publish(
|
||||
config.MQTT_TOPIC_SENSOR, sensor_payload, qos=config.MQTT_QOS
|
||||
)
|
||||
print("[Sensor Task] Sensor data successfully published:", sensor_payload)
|
||||
except Exception as e:
|
||||
print("[Sensor Task] Failed to publish sensor data:", e)
|
||||
|
||||
|
||||
async def uart_task():
|
||||
"""Polls incoming UART messages from the LoRa board using dynamic method fallback."""
|
||||
while True:
|
||||
if uart_device:
|
||||
try:
|
||||
cmd = uart_device.read_as_command()
|
||||
|
||||
if cmd:
|
||||
print("[UART] Command received from LoRa board:", cmd)
|
||||
if (
|
||||
hasattr(cmd, "command_type")
|
||||
and cmd.command_type == UARTCommandType.STATE_UPDATE
|
||||
and on_received_cooking_state_update
|
||||
):
|
||||
on_received_cooking_state_update(
|
||||
cmd.payload.get("state"),
|
||||
cmd.payload.get("is_error", False),
|
||||
cmd.payload.get("is_terminated", False),
|
||||
)
|
||||
except Exception as e:
|
||||
print("[UART Task] Error reading command:", e)
|
||||
|
||||
await asyncio.sleep_ms(50)
|
||||
|
||||
|
||||
async def connect_mqtt_async():
|
||||
"""Connects to MQTT safely while memory is clean."""
|
||||
global mqtt_connected
|
||||
global mqtt_connected, mqtt_client
|
||||
mqtt_connected = False
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
print("[MQTT] Connecting to broker with TLS...")
|
||||
# Re-instantiate client to clear old socket buffers
|
||||
gc.collect()
|
||||
mqtt_client = get_mqtt_client(
|
||||
host="192.168.50.1", # TODO : Use config.MQTT_BROKER_HOST instead of hardcoding
|
||||
port=8884,
|
||||
client_id="smartwave-esp32-demo",
|
||||
use_tls=True,
|
||||
cafile=MQTT_CA_FILE,
|
||||
keepalive=30,
|
||||
)
|
||||
mqtt_client.set_callback(on_mqtt_message)
|
||||
|
||||
mqtt_client.connect()
|
||||
print("[MQTT] Connected! Subscribing to topics...")
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_COOKING, qos=config.MQTT_QOS)
|
||||
@@ -204,12 +257,14 @@ async def connect_mqtt_async():
|
||||
mqtt_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Force heap cleanup before sleeping
|
||||
del mqtt_client
|
||||
gc.collect()
|
||||
print(f"[MQTT] Free RAM after cleanup: {gc.mem_free()} bytes")
|
||||
print("[MQTT] Retrying connection in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
# --- CONCURRENT ASYNC TASKS ---
|
||||
|
||||
async def mqtt_poll_task():
|
||||
global mqtt_connected
|
||||
last_ping = time.time()
|
||||
@@ -234,9 +289,18 @@ async def mqtt_poll_task():
|
||||
async def orchestrator_hello_task():
|
||||
global mqtt_connected
|
||||
while True:
|
||||
if mqtt_connected and orchestrator_id is None:
|
||||
if orchestrator_id is not None:
|
||||
# Hello successfully acknowledged! Stop looping this task.
|
||||
print("[Hello Task] Orchestrator acknowledged. Stopping hello task.")
|
||||
break
|
||||
|
||||
if mqtt_connected:
|
||||
print("[Hello Task] Sending initial hello to orchestrator...")
|
||||
try:
|
||||
if mqtt_client is None:
|
||||
print("[Hello Task] MQTT client is None. Attempting to reconnect...")
|
||||
await connect_mqtt_async()
|
||||
|
||||
mqtt_client.publish(
|
||||
config.MQTT_TOPIC_HELLO,
|
||||
payloads.mqtt_hello(DEVICE_ID),
|
||||
@@ -244,32 +308,11 @@ async def orchestrator_hello_task():
|
||||
)
|
||||
except OSError as e:
|
||||
print("[Hello Task] Hello publish failed:", e)
|
||||
mqtt_connected = False
|
||||
await connect_mqtt_async()
|
||||
# mqtt_connected = False
|
||||
|
||||
await asyncio.sleep(config.MQTT_HELLO_INTERVAL)
|
||||
|
||||
|
||||
async def uart_task():
|
||||
while True:
|
||||
if uart_device is not None:
|
||||
while uart_device.any():
|
||||
command = uart_device.read_as_command()
|
||||
if command:
|
||||
print(f"[UART Task] Received command: {command.command_type}")
|
||||
if command.command_type == UARTCommandType.COOKING_STATE_UPDATE:
|
||||
new_state = command.payload.get("state", None)
|
||||
print(f"[UART Task] Cooking state update: {new_state}")
|
||||
on_received_cooking_state_update(new_state)
|
||||
else:
|
||||
print(f"[UART Task] Unknown command type: {command.command_type}")
|
||||
else:
|
||||
raw_command = uart_device.read()
|
||||
print(f"[UART Task] Received raw command: {raw_command}")
|
||||
|
||||
await asyncio.sleep_ms(20)
|
||||
|
||||
|
||||
async def memory_cleanup_task():
|
||||
while True:
|
||||
gc.collect()
|
||||
@@ -278,17 +321,19 @@ async def memory_cleanup_task():
|
||||
|
||||
# --- MAIN ENTRY POINT ---
|
||||
async def main():
|
||||
global sensor_request_event
|
||||
print("[Main] Starting application...")
|
||||
|
||||
# Initialize loop-bound events
|
||||
sensor_request_event = asyncio.Event()
|
||||
|
||||
# STEP 1: Connect MQTT FIRST (while RAM is unfragmented)
|
||||
await connect_mqtt_async()
|
||||
|
||||
# STEP 2: Initialize Hardware & Secondary Modules AFTER connection
|
||||
init_hardware()
|
||||
|
||||
# STEP 3: Launch tasks
|
||||
# Launch background tasks
|
||||
asyncio.create_task(mqtt_poll_task())
|
||||
asyncio.create_task(orchestrator_hello_task())
|
||||
asyncio.create_task(sensor_publisher_task())
|
||||
asyncio.create_task(uart_task())
|
||||
asyncio.create_task(memory_cleanup_task())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user