Better cook parameter estimation + Defrost mode + Removed esp-wifi debugs + Orchestrator and microwave exchange
This commit is contained in:
+213
-13
@@ -1,8 +1,11 @@
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
from orchestrateur.sensors import gps
|
||||
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads
|
||||
from shared.logging import log
|
||||
@@ -21,6 +24,9 @@ except Exception:
|
||||
|
||||
# Thread-safe queue for application messages
|
||||
data_queue = queue.Queue()
|
||||
cooking_queue = {}
|
||||
active_cooks = {}
|
||||
active_cooks_lock = threading.Lock()
|
||||
|
||||
lora = get_lora()
|
||||
lora.configure()
|
||||
@@ -54,8 +60,9 @@ mqtt_client = get_mqtt_client(
|
||||
)
|
||||
mqtt_client.connect()
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||
print(f"Subscribed to topic: {config.MQTT_TOPIC_SENSOR}")
|
||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||
print(f"Subscribed to topic: {config.MQTT_TOPIC_HELLO}")
|
||||
|
||||
# --- THE CRUCIAL PAHO FIX ---
|
||||
# Start Paho's internal background thread. This handles all network packets,
|
||||
@@ -95,13 +102,179 @@ def button_callback():
|
||||
button.set_callback(button_callback)
|
||||
|
||||
# Launch background monitoring workers
|
||||
# threading.Thread(target=lora_listener, daemon=True).start()
|
||||
# threading.Thread(target=mqtt_listener, daemon=True).start()
|
||||
threading.Thread(target=lora_listener, daemon=True).start()
|
||||
threading.Thread(target=mqtt_listener, daemon=True).start()
|
||||
# Launch button monitoring thread
|
||||
button.start_button_monitoring_thread()
|
||||
|
||||
print("Orchestrateur prêt. Le main loop est libre.")
|
||||
|
||||
def _tryReadSensorsWithRetries(func, exception=True, max_retries=3, delay=1):
|
||||
"""
|
||||
Tries to read the sensor max_retries times until the return value of func is not None.
|
||||
It will then return the value of func. If it fails max_retries times, it will fail if exception is True, otherwise it will return None.
|
||||
"""
|
||||
|
||||
for attempt in range(max_retries):
|
||||
result = func()
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
log(f"Attempt {attempt + 1} failed. Retrying in {delay} seconds...")
|
||||
time.sleep(delay)
|
||||
|
||||
if exception:
|
||||
raise Exception(f"Failed to read sensor after {max_retries} attempts.")
|
||||
else:
|
||||
return None
|
||||
|
||||
def read_sensors_for_cooking(microwave_id):
|
||||
"""Read all sensors and return a dictionary of their values, including the microwave ID."""
|
||||
log("\nLecture des capteurs...")
|
||||
sensor_data = {}
|
||||
sensor_data["microwave_id"] = microwave_id
|
||||
|
||||
# === Notify the microwave of needed sensor readings ===
|
||||
mqtt_client.publish(config.MQTT_TOPIC_COOKING, payloads.mqtt_cooking_init(microwave_id), qos=config.MQTT_QOS)
|
||||
|
||||
# Read Ultrasonic Ranger
|
||||
sensor_data["ultrasonic_distance"] = _tryReadSensorsWithRetries(ultrasonicRanger.get_dish_height)
|
||||
log(f"\nLecture du capteur Ultrason : {sensor_data['ultrasonic_distance']}")
|
||||
|
||||
# Read Temperature and Humidity
|
||||
temperature, humidity = temp_hum.get_temperature_and_humidity()
|
||||
if temperature is not None and humidity is not None:
|
||||
log(f"\nLecture du capteur Temp/Hum : {temperature}, {humidity}")
|
||||
sensor_data["temperature"] = temperature
|
||||
sensor_data["humidity"] = humidity
|
||||
|
||||
# Camera
|
||||
def _getPicture():
|
||||
picture_bytes = None
|
||||
try:
|
||||
picture_bytes = camera.get_picture()
|
||||
return picture_bytes
|
||||
except Exception as e:
|
||||
log(f"Error reading camera data: {e}")
|
||||
return None
|
||||
sensor_data["camera_image"] = _tryReadSensorsWithRetries(_getPicture, exception=True)
|
||||
log(f"\nPhoto de la Caméra : {len(sensor_data['camera_image'])} bytes")
|
||||
|
||||
# Read Button State (last because he can still change state while reading other sensors)
|
||||
sensor_data["defrost_mode"] = button_state
|
||||
|
||||
cooking_queue[microwave_id] = sensor_data
|
||||
|
||||
|
||||
def _stop_hardware(microwave_id: str):
|
||||
"""
|
||||
Hardware driver stop — halts magnetron/turntable immediately.
|
||||
"""
|
||||
log(f"[{microwave_id}] 🛑 Emergency stop issued to hardware.")
|
||||
# TODO: Add physical hardware stop command here
|
||||
# e.g., gpio_controller.stop()
|
||||
|
||||
|
||||
def _send_params_to_microwave(microwave_id: str, cook_time_seconds: int, power_level_pct: int, target_temp: float, cancel_event: threading.Event):
|
||||
"""
|
||||
Triggers physical microwave execution.
|
||||
"""
|
||||
if cancel_event.is_set():
|
||||
return
|
||||
|
||||
log(f"[{microwave_id}] ⚡ Starting microwave cooking : {cook_time_seconds}s @ {power_level_pct}% power, target temp {target_temp}°C.")
|
||||
# TODO: Connect to microwave hardware driver here
|
||||
# e.g., gpio_controller.start(time=cook_time_seconds, power=power_level_pct)
|
||||
|
||||
|
||||
def _cooking_worker(microwave_id: str, sensors_data: dict, cancel_event: threading.Event):
|
||||
"""Worker function executing cloud API calls and hardware triggers."""
|
||||
URL = "https://smartwave.matthiasg.dev/cooking-params"
|
||||
|
||||
try:
|
||||
# Check cancellation before network call
|
||||
if cancel_event.is_set():
|
||||
print(f"[{microwave_id}] Job canceled before starting API call.")
|
||||
return
|
||||
|
||||
log(f"[{microwave_id}] Sending sensor data to cloud API...")
|
||||
|
||||
# Ensure camera_image is encoded to Base64 string if it's currently raw bytes
|
||||
if isinstance(sensors_data.get("camera_image"), bytes):
|
||||
sensors_data["camera_image"] = base64.b64encode(sensors_data["camera_image"]).decode("utf-8")
|
||||
|
||||
# 1. HTTP Request (15-second timeout)
|
||||
response = requests.post(URL, json=sensors_data, timeout=15)
|
||||
|
||||
# Check cancellation right after network call returns
|
||||
if cancel_event.is_set():
|
||||
print(f"[{microwave_id}] Job was canceled while waiting for cloud response. Discarding result.")
|
||||
return
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 2. Extract Response Parameters
|
||||
response_json = response.json()
|
||||
cook_plan = response_json.get("cook_plan", {})
|
||||
|
||||
cook_time = cook_plan.get("cook_time_seconds")
|
||||
power_level = cook_plan.get("recommended_power_level_pct")
|
||||
target_temp = cook_plan.get("target_temp")
|
||||
dish_name = response_json.get("dish_name", "Unknown Dish")
|
||||
|
||||
if cook_time is None or power_level is None or target_temp is None:
|
||||
print(f"[{microwave_id}] Cloud returned incomplete plan: {response_json}")
|
||||
return
|
||||
|
||||
# Check cancellation before starting physical microwave
|
||||
if cancel_event.is_set():
|
||||
print(f"[{microwave_id}] Job was canceled before starting hardware execution.")
|
||||
return
|
||||
|
||||
print(f"[{microwave_id}] Received plan for '{dish_name}': {cook_time}s @ {power_level}% power, target temp {target_temp}°C.")
|
||||
|
||||
# 3. Start Hardware Execution
|
||||
_send_params_to_microwave(microwave_id, cook_time, power_level, target_temp, cancel_event)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"[{microwave_id}] Request timed out waiting for cloud response.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"[{microwave_id}] HTTP error reaching cloud API: {e}")
|
||||
except Exception as e:
|
||||
print(f"[{microwave_id}] Unexpected error in worker thread: {e}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# Clean up registry entry if this worker was the active one
|
||||
with active_cooks_lock:
|
||||
if active_cooks.get(microwave_id) == cancel_event:
|
||||
del active_cooks[microwave_id]
|
||||
|
||||
|
||||
def start_cooking_for_microwave(microwave_id: str, sensors_data: dict):
|
||||
"""
|
||||
Sends sensors data to the cloud and starts cooking in a separate thread.
|
||||
If a worker is already running for the given microwave_id, it cancels
|
||||
the previous process and stops the hardware before starting the new one.
|
||||
"""
|
||||
with active_cooks_lock:
|
||||
# 1. If an active job exists for this microwave, cancel it
|
||||
if microwave_id in active_cooks:
|
||||
print(f"[{microwave_id}] Existing cooking job detected! Canceling old worker...")
|
||||
active_cooks[microwave_id].set() # Signal existing thread to abort
|
||||
_stop_hardware(microwave_id) # Stop hardware immediately
|
||||
|
||||
# 2. Register a new cancellation event for this microwave
|
||||
cancel_event = threading.Event()
|
||||
active_cooks[microwave_id] = cancel_event
|
||||
|
||||
# 3. Start the new background worker thread
|
||||
thread = threading.Thread(
|
||||
target=_cooking_worker,
|
||||
args=(microwave_id, sensors_data, cancel_event),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
# Sensor reading
|
||||
def read_sensors():
|
||||
"""Read all sensors and return a dictionary of their values."""
|
||||
@@ -138,14 +311,14 @@ def read_sensors():
|
||||
log(f"Error reading camera data: {e}")
|
||||
|
||||
# Read Button State (last because he can still change state while reading other sensors)
|
||||
sensor_data["button_state"] = button_state
|
||||
sensor_data["defrost_state"] = button_state
|
||||
|
||||
return sensor_data
|
||||
|
||||
# --- MAIN EXECUTION LOOP ---
|
||||
while True:
|
||||
try:
|
||||
# Check for non-heartbeat data
|
||||
# === TREAT MESSAGE QUEUE ===
|
||||
try:
|
||||
msg = data_queue.get(block=False)
|
||||
|
||||
@@ -154,6 +327,7 @@ while True:
|
||||
if msg["source"] == "LoRa":
|
||||
print(f"\n[Main Loop] LoRa : Données traitées : {msg['data']}")
|
||||
elif msg["source"] == "MQTT":
|
||||
# MQTT HELLO
|
||||
if (msg["topic"] == config.MQTT_TOPIC_HELLO.decode('utf-8')):
|
||||
if ("id_orchestrator" in msg["data"] and msg["data"]["id_orchestrator"] == DEVICE_ID):
|
||||
# Do not answer to messages coming from me
|
||||
@@ -164,19 +338,45 @@ while True:
|
||||
mqtt_client.publish(config.MQTT_TOPIC_HELLO, payloads.mqtt_hello_ack(DEVICE_ID, microwave_id), qos=config.MQTT_QOS)
|
||||
print(f"[Main Loop] MQTT : Réponse Hello envoyée à {microwave_id}.")
|
||||
# TODO : Save in database
|
||||
|
||||
|
||||
# MQTT SENSOR DATA
|
||||
elif (msg["topic"] == config.MQTT_TOPIC_SENSOR.decode('utf-8')):
|
||||
print(f"\n[Main Loop] MQTT : Données capteurs reçues du micro-ondes : {msg['data']}")
|
||||
microwave_id = msg["data"].get("id_microwave")
|
||||
if not microwave_id:
|
||||
print("[Main Loop] MQTT : Données capteurs reçues sans ID micro-ondes. Ignoré.")
|
||||
continue
|
||||
# Get the already existing cooking data for this microwave
|
||||
sensors_data = cooking_queue.get(microwave_id)
|
||||
if sensors_data is None:
|
||||
print(f"[Main Loop] MQTT : Données capteurs reçues pour {microwave_id} mais aucune donnée de cuisson en cours. Ignoré.")
|
||||
continue
|
||||
# Merge the received sensor data into the existing cooking data
|
||||
sensors_data["ir_initial_temp"] = msg["data"].get("dish_temp")
|
||||
sensors_data["ir_ambient_temp"] = msg["data"].get("ambient_temp")
|
||||
start_cooking_for_microwave(microwave_id, sensors_data)
|
||||
# Remove the cooking data from the queue since it's now being processed
|
||||
del cooking_queue[microwave_id]
|
||||
|
||||
print(f"\n[Main Loop] MQTT : Données traitées : {msg['data']}")
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
# === CHECK FOR DISH INSERTED ===
|
||||
# Read the dish height from the ultrasonic sensor. If it's below a certain threshold, we assume a dish has been inserted.
|
||||
dish_height = ultrasonicRanger.get_dish_height()
|
||||
if dish_height is not None and dish_height > 2.0: # Threshold in cm for detecting a dish
|
||||
print(f"\n[Main Loop] Dish detected at height: {dish_height} cm. Initiating sensor read...")
|
||||
# Read all sensors and store the data in the cooking queue for this microwave
|
||||
read_sensors_for_cooking("2")
|
||||
print(f"[Main Loop] Sensor data collected and queued for cooking.")
|
||||
|
||||
# DEBUG : Read sensors
|
||||
sensor_values = read_sensors()
|
||||
if sensor_values:
|
||||
sensor_values_print = sensor_values.copy()
|
||||
if "camera_image" in sensor_values_print:
|
||||
sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>"
|
||||
print(f"\nCapteurs Données lues : {sensor_values_print}")
|
||||
# sensor_values = read_sensors()
|
||||
# if sensor_values:
|
||||
# sensor_values_print = sensor_values.copy()
|
||||
# if "camera_image" in sensor_values_print:
|
||||
# sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>"
|
||||
# print(f"\nCapteurs Données lues : {sensor_values_print}")
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user