Working MQTT back !

This commit is contained in:
2026-08-03 16:41:48 +02:00
parent 9eac93c409
commit 43a1822547
4 changed files with 716 additions and 305 deletions
+268 -203
View File
@@ -1,23 +1,12 @@
import _thread
import select
from machine import Pin, I2C
from sensors import temperature_sensor
from shared import get_mqtt_client, get_uart, config, payloads, cookingState
from shared.uart_comm import UARTCommand, UARTCommandType
from shared.sensors import RGBLED
from shared.logging import log
import gc
import sys
import time
import ujson as json
import sys
import uasyncio as asyncio
from machine import Pin, I2C
# Simple thread-safe queue list
msg_queue = []
queue_lock = _thread.allocate_lock()
def queue_publish(topic, payload):
"""Safely queues a message from the main thread."""
with queue_lock:
msg_queue.append((topic, payload))
# 1. Clean memory immediately
gc.collect()
# --- READ DEVICE ID ---
try:
@@ -26,214 +15,290 @@ try:
except Exception:
DEVICE_ID = "ESP32_Inconnu"
# --- Cooking State ---
cooking_state = None # This will hold the current cooking state if any
# --- GLOBAL APP STATE ---
orchestrator_id = None
cooking_state = None
mqtt_connected = False
# --- MQTT SETUP (Initialized First!) ---
from shared import get_mqtt_client, config, payloads
# --- MQTT SETUP ---
MQTT_CA_FILE = "/certs/ca.crt"
mqtt_client = get_mqtt_client(
host=config.MQTT_BROKER_HOST,
client_id="smartwave-esp32-" + DEVICE_ID,
use_tls=config.USE_TLS,
host="192.168.50.1",
client_id="smartwave-esp32-demo",
use_tls=True,
cafile=MQTT_CA_FILE,
keepalive=config.MQTT_KEEPALIVE,
keepalive=30,
)
global orchestrator_id
orchestrator_id = None
def on_mqtt_message(message):
print("[MQTT Thread] Received message:", message)
# --- HARDWARE & MODULE DEFERRED IMPORTS ---
# We declare variables here, but initialize them AFTER MQTT connects
status_led = None
uart_device = None
mlx_temperature_sensor = None
cookingState = None
log = None
UARTCommand = None
UARTCommandType = None
def init_hardware():
"""Initializes hardware peripherals AFTER MQTT TLS has reserved its memory."""
global status_led, uart_device, mlx_temperature_sensor
global cookingState, log, UARTCommand, UARTCommandType
# Try and parse the payload as json, but if it fails, just print the raw payload
payload_data=None
try:
payload_data = json.loads(message['payload'])
except Exception as e:
print("[MQTT Thread] Error parsing JSON:", e)
sys.print_exception(e)
pass # Maybe it's not JSON
print("[Main] Initializing hardware peripherals...")
if message['topic'] == config.MQTT_TOPIC_HELLO and payload_data and "id_orchestrator" in payload_data and payload_data["id_microwave"] == DEVICE_ID:
print("[MQTT Thread] Hello response received from orchestrator:", payload_data["id_orchestrator"])
global orchestrator_id
orchestrator_id = payload_data["id_orchestrator"]
# Unsubscribe from the hello topic since we got a response
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
print("[MQTT Thread] Unsubscribed from topic:", config.MQTT_TOPIC_HELLO)
# Handle cooking messages
elif message['topic'] == config.MQTT_TOPIC_COOKING and payload_data and payload_data["id_microwave"] == DEVICE_ID:
# Cooking sensors init request
if not "cook_time" in payload_data:
print("[MQTT Thread] Cooking sensors init received from the orchestrator")
obj_temp = mlx_temperature_sensor.read_object_temp()
amb_temp = mlx_temperature_sensor.read_ambient_temp()
queue_publish(config.MQTT_TOPIC_SENSOR, payloads.mqtt_sensor_data(DEVICE_ID, obj_temp, amb_temp))
# Received cooking parameters from the orchestrator
else:
print("[MQTT Thread] Cooking parameters received from the orchestrator:", payload_data)
global cooking_state
cooking_state = cookingState.CookingState(
cook_time=payload_data["cook_time"],
power_level=payload_data["power_level"],
target_temp=payload_data["target_temp"]
)
cooking_state.set_state_change_callback(on_cooking_state_change)
cooking_state.set_state(cookingState.CookingStates.IDLE) # Set initial state to IDLE
# Send to the LoRa board the cooking parameters
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_PARAMS, payload_data))
print("[MQTT Thread] Cooking parameters sent to LoRa board.")
print("[MQTT Thread] Message processing complete.")
# 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
from sensors import temperature_sensor
mqtt_client.set_callback(on_mqtt_message)
cookingState = cs
log = logging.log
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),
sda=Pin(26, Pin.IN, Pin.PULL_UP),
freq=100000,
)
devices = temperature_sensor_i2c.scan()
if 0x5A in devices:
print("[Main] MLX90614 found at address 0x5A!")
else:
print("[Main] MLX90614 not found on I2C bus.")
mlx_temperature_sensor = temperature_sensor.MLX90614(temperature_sensor_i2c)
def mqtt_background_thread():
"""Background MQTT worker handling ALL socket operations safely."""
print("[Thread] Background MQTT worker started.")
# --- CALLBACKS ---
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
while True:
try:
print("[Thread] Attempting connection to MQTT broker...")
mqtt_client.connect()
print("[Thread] Connected! Subscribing to topic...")
mqtt_client.subscribe(config.MQTT_TOPIC_COOKING, qos=config.MQTT_QOS)
print("[Thread] Successfully subscribed. Setting up poller...")
poller = select.poll()
poller.register(mqtt_client._client.sock, select.POLLIN)
last_check = time.time()
while True:
# 1. Process outbound messages queued by the main thread
while len(msg_queue) > 0:
with queue_lock:
topic, payload = msg_queue.pop(0)
print(f"[Thread] Safely publishing queued message to {topic}...")
mqtt_client.publish(topic, payload, qos=config.MQTT_QOS)
# 2. Check for incoming messages (non-blocking poll)
events = poller.poll(200)
if events:
mqtt_client.wait()
# 3. Handle Keepalive tracking manually
if time.time() - last_check >= 15:
# print("[Thread] Sending keepalive ping...")
mqtt_client._client.ping()
last_check = time.time()
# Small breathe room for the CPU core
time.sleep_ms(50)
except Exception as e:
print("[Thread] Connection dropped or error encountered:", e)
sys.print_exception(e)
print("[Thread] Cleaning up socket context. Retrying in 5 seconds...")
# --- FIX FOR ERROR 23 (SOCKET LEAK) ---
# Manually force-kill the underlying socket file descriptor if it exists
try:
if mqtt_client._client and hasattr(mqtt_client._client, "sock"):
if mqtt_client._client.sock is not None:
mqtt_client._client.sock.close()
except Exception:
pass # Already dead or closed
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)
# Now we let the wrapper do its normal cleanup safely
try:
mqtt_client.close()
except Exception:
pass
time.sleep(5)
# UART
uart_device = get_uart(uart_id=2, tx_pin=17, rx_pin=16)
# Cooking Cycle
cooking_state = None
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
log(f"[Main] Updating cooking state to: {new_state}")
if log:
log(f"[Main] Updating cooking state to: {new_state}")
cooking_state.set_state(new_state)
# Status LED
status_led = RGBLED(red_pin=21, green_pin=19, blue_pin=18)
def on_cooking_state_change(state):
print(f"[Main] Cooking state changed to: {state.state}")
# === STATUS LED UPDATE ===
BLINK_INTERVAL_MS = 500 # Blink every 500ms
if state.state == cookingState.CookingStates.IDLE:
status_led.color = RGBLED.OFF
status_led.blink_off()
if state.state == cookingState.CookingStates.COOKING:
status_led.color = RGBLED.YELLOW
status_led.blink_off()
if state.state == cookingState.CookingStates.STIRRING_REQUIRED:
status_led.color = RGBLED.ORANGE
status_led.blink_on(BLINK_INTERVAL_MS)
if state.state == cookingState.CookingStates.DONE:
status_led.color = RGBLED.GREEN
status_led.blink_off()
if state.state == cookingState.CookingStates.ALERT:
status_led.color = RGBLED.RED
status_led.blink_on(BLINK_INTERVAL_MS)
# --- MAIN APPLICATION THREAD (Core 0) ---
print("[Main] Main execution path active.")
# Temperature sensor setup
temperature_sensor_i2c = I2C(0, scl=Pin(25, Pin.IN, Pin.PULL_UP), sda=Pin(26, Pin.IN, Pin.PULL_UP), freq=100000)
# Scan to verify the sensor is connected and detected
print("Scanning I2C bus...")
devices = temperature_sensor_i2c.scan()
if 0x5A in devices:
print("MLX90614 found at address 0x5A!")
else:
print("MLX90614 not found. Please check your wiring.")
mlx_temperature_sensor = temperature_sensor.MLX90614(temperature_sensor_i2c)
# --- Launch background worker ---
_thread.start_new_thread(mqtt_background_thread, ())
time.sleep(2) # Give the thread a moment to initial connect
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
while True:
# MQTT HELLO sent every x seconds until we get a response from the orchestrator
if (orchestrator_id == None and -(mqtt_hello_sent_timestamp - time.time()) > config.MQTT_HELLO_INTERVAL):
print("[Main] Attempting to send initial hello to orchestrator...")
queue_publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello(DEVICE_ID))
mqtt_hello_sent_timestamp = time.time()
pass
def on_mqtt_message(message):
global orchestrator_id, cooking_state
print("[MQTT] Received message on topic:", message.get("topic"))
# 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 LoRa Board: {command.command_type}")
if command.command_type == UARTCommandType.COOKING_STATE_UPDATE:
# Handle cooking state update command
new_state = command.payload.get("state", None)
print(f"[Main] Cooking state update received: {new_state}")
on_received_cooking_state_update(new_state)
else:
print(f"[Main] Unknown command type received: {command.command_type}")
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
if (
topic == config.MQTT_TOPIC_HELLO
and payload_data
and payload_data.get("id_microwave") == DEVICE_ID
):
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)
# 2. Cooking Parameters / Sensor Request
elif (
topic == config.MQTT_TOPIC_COOKING
and payload_data
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
)
else:
# Fallback to reading as a raw string if parsing fails
raw_command = uart_device.read()
print(f"[Main] Received raw command from WiFi Board: {raw_command}")
# 2. Example: Send data to the Heltec board every 5 seconds
# uart_device.send("Status Check: WiFi Active")
time.sleep(1)
print("[MQTT] Cooking parameters received:", payload_data)
if cookingState:
cooking_state = cookingState.CookingState(
cook_time=payload_data["cook_time"],
power_level=payload_data["power_level"],
target_temp=payload_data["target_temp"],
)
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.")
mqtt_client.set_callback(on_mqtt_message)
async def connect_mqtt_async():
"""Connects to MQTT safely while memory is clean."""
global mqtt_connected
mqtt_connected = False
while True:
try:
print("[MQTT] Connecting to broker with TLS...")
gc.collect()
mqtt_client.connect()
print("[MQTT] Connected! Subscribing to topics...")
mqtt_client.subscribe(config.MQTT_TOPIC_COOKING, qos=config.MQTT_QOS)
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
print("[MQTT] Subscribed successfully!")
mqtt_connected = True
return
except Exception as e:
print("[MQTT] Connection failed:", e)
sys.print_exception(e)
try:
mqtt_client.close()
except Exception:
pass
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()
while True:
if mqtt_connected:
try:
mqtt_client.poll()
now = time.time()
if now - last_ping >= 15:
if mqtt_client._client:
mqtt_client._client.ping()
last_ping = now
except OSError as e:
print("[MQTT Task] Socket error encountered during poll/ping:", e)
mqtt_connected = False
await connect_mqtt_async()
await asyncio.sleep_ms(30)
async def orchestrator_hello_task():
global mqtt_connected
while True:
if mqtt_connected and orchestrator_id is None:
print("[Hello Task] Sending initial hello to orchestrator...")
try:
mqtt_client.publish(
config.MQTT_TOPIC_HELLO,
payloads.mqtt_hello(DEVICE_ID),
qos=config.MQTT_QOS,
)
except OSError as e:
print("[Hello Task] Hello publish failed:", e)
mqtt_connected = False
await connect_mqtt_async()
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()
await asyncio.sleep(10)
# --- MAIN ENTRY POINT ---
async def main():
print("[Main] Starting application...")
# 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
asyncio.create_task(mqtt_poll_task())
asyncio.create_task(orchestrator_hello_task())
asyncio.create_task(uart_task())
asyncio.create_task(memory_cleanup_task())
print("[Main] All tasks running concurrently!")
while True:
await asyncio.sleep(3600)
try:
asyncio.run(main())
except KeyboardInterrupt:
print("[Main] Program stopped by user.")