Better cook parameter estimation + Defrost mode + Removed esp-wifi debugs + Orchestrator and microwave exchange
Build, push image, and notify Watchtower / build-image (push) Successful in 3m19s
Build, push image, and notify Watchtower / notify (push) Successful in 7s

This commit is contained in:
2026-07-27 17:14:05 +02:00
parent 81de985580
commit 0f17e9dce6
9 changed files with 335 additions and 64 deletions
+2
View File
@@ -1,4 +1,5 @@
import os import os
import time
import requests import requests
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import json import json
@@ -17,6 +18,7 @@ class EdamamAPI:
def analyze_dish_image(self, image_file_path: str): def analyze_dish_image(self, image_file_path: str):
if SAVE_EDAMAM_API_TOKEN: if SAVE_EDAMAM_API_TOKEN:
time.sleep(3)
return json.loads(""" return json.loads("""
{ {
"combined": { "combined": {
+30 -17
View File
@@ -52,44 +52,57 @@ def cooking_params():
if not data: if not data:
return jsonify({"error": "Invalid or missing JSON payload"}), 400 return jsonify({"error": "Invalid or missing JSON payload"}), 400
# Extract user or device parameters (with fallback defaults)
height_cm = float(data.get("dish_height", 4.0))
initial_temp_c = float(data.get("ir_initial_temp", 20.0)) # e.g., 4.0 for fridge, -18.0 for freezer
microwave_wattage = int(data.get("microwave_wattage", 900)) # e.g., 900W
defrost_mode = bool(data.get("defrost_mode", False)) # True for defrost, False for cook/reheat
print("Received cooking parameters request:", data)
print("Parsed parameters - Height (cm):", height_cm, "Initial Temp (C):", initial_temp_c, "Microwave Wattage:", microwave_wattage, "Defrost Mode:", defrost_mode)
# 1. Handle the Camera Image # 1. Handle the Camera Image
camera_image_b64 = data.get("camera_image") camera_image_b64 = data.get("camera_image")
filepath = None
if camera_image_b64: if camera_image_b64:
# Generate a unique filename using UUID to avoid overwriting
filename = f"dish_{uuid.uuid4().hex}.jpg" filename = f"dish_{uuid.uuid4().hex}.jpg"
filepath = os.path.join(CAMERA_IMAGE_DIR, filename) filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
try: try:
# Decode the base64 string and save it as a binary file
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(base64.b64decode(camera_image_b64)) f.write(base64.b64decode(camera_image_b64))
# Replace the giant base64 string in the dictionary with the local file path
# so we don't bloat the MongoDB document
data["camera_image"] = filepath data["camera_image"] = filepath
except Exception as e: except Exception as e:
return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500 return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500
else:
return jsonify({"error": "Missing required field 'camera_image'"}), 400
# 2. Save to MongoDB # 2. Run the Cook Planning Engine
try:
cook_plan = microwave_cook_planner.generate_plan(
image_path=filepath,
height_cm=height_cm,
initial_temp_c=initial_temp_c,
microwave_wattage=microwave_wattage,
defrost_mode=defrost_mode
)
except Exception as e:
return jsonify({"error": f"Failed to compute cooking plan: {str(e)}"}), 500
# 3. Attach cooking parameters to database record
data["analysis_results"] = cook_plan
# 4. Save to MongoDB
try: try:
# Insert the dictionary directly into Mongo (it will retain your exact JSON keys)
cooking_collection.insert_one(data) cooking_collection.insert_one(data)
# Remove the Mongo-injected '_id' object before returning the response
data.pop("_id", None) data.pop("_id", None)
except Exception as e: except Exception as e:
return jsonify({"error": f"Database error: {str(e)}"}), 500 return jsonify({"error": f"Database error: {str(e)}"}), 500
# 3. Returns with the cooking parameters # 5. Return complete output
cooking_plan = microwave_cook_planner.generate_plan( return jsonify(cook_plan), 201
image_path=data.get("camera_image"),
height_cm=data.get("height_cm", 4.0),
initial_temp_c=data.get("initial_temp_c", 20.0),
microwave_wattage=data.get("microwave_wattage", 900)
)
return jsonify(cooking_plan), 201
@app.route("/device-network", methods=["POST"]) @app.route("/device-network", methods=["POST"])
def device_network(): def device_network():
+4 -2
View File
@@ -50,7 +50,8 @@ class MicrowaveCookPlanner:
image_path: str, image_path: str,
height_cm: float, height_cm: float,
initial_temp_c: float, initial_temp_c: float,
microwave_wattage: int = 900 microwave_wattage: int = 900,
defrost_mode: bool = False
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Main pipeline call to parse an image and return cooking parameters.""" """Main pipeline call to parse an image and return cooking parameters."""
@@ -85,7 +86,8 @@ class MicrowaveCookPlanner:
# 5. Cook Plan Calculation # 5. Cook Plan Calculation
cook_plan = self.engine.calculate_cook_plan( cook_plan = self.engine.calculate_cook_plan(
state=thermal_state, state=thermal_state,
microwave_wattage=microwave_wattage microwave_wattage=microwave_wattage,
defrost_mode=defrost_mode
) )
# Return consolidated output # Return consolidated output
+45 -14
View File
@@ -4,23 +4,25 @@ from typing import Dict, Any, Optional
@dataclass @dataclass
class DishThermalState: class DishThermalState:
food_name: str food_name: str
macronutrients: Dict[str, float] # grams of water, fat, protein, carbs macronutrients: Dict[str, float]
estimated_mass_g: float estimated_mass_g: float
initial_temp_c: float initial_temp_c: float
volume_cm3: Optional[float] = None volume_cm3: Optional[float] = None
class MicrowaveThermalEngine: class MicrowaveThermalEngine:
"""Calculates cook parameters based on physical properties""" """Calculates cook parameters based on physical properties"""
DEFAULT_EFFICIENCY = 0.70 # ~70% magnetron efficiency DEFAULT_EFFICIENCY = 0.70 # ~70% magnetron efficiency
TARGET_TEMP_C = 74.0 # Safe food temperature COOK_TARGET_TEMP_C = 74.0 # Safe food temp for cooking/reheating
DEFROST_TARGET_TEMP_C = 4.0 # Chilled state target for defrosting
LATENT_HEAT_ICE_J_G = 334.0 # Joules required to melt 1g of ice to water
@staticmethod @staticmethod
def estimate_specific_heat(macros: Dict[str, float], total_weight_g: float) -> float: def estimate_specific_heat(macros: Dict[str, float], total_weight_g: float) -> float:
"""Estimates Cp in J/(g*C) based on macro composition""" """Estimates Cp in J/(g*C) based on macro composition"""
if total_weight_g <= 0: if total_weight_g <= 0:
return 3.5 # Fallback average for mixed meals return 3.5
w_water = macros.get("water_g", total_weight_g * 0.7) / total_weight_g w_water = macros.get("water_g", total_weight_g * 0.7) / total_weight_g
w_protein = macros.get("protein_g", 0.0) / total_weight_g w_protein = macros.get("protein_g", 0.0) / total_weight_g
w_fat = macros.get("fat_g", 0.0) / total_weight_g w_fat = macros.get("fat_g", 0.0) / total_weight_g
@@ -28,25 +30,54 @@ class MicrowaveThermalEngine:
return (4.184 * w_water) + (1.71 * w_protein) + (1.67 * w_fat) + (1.42 * w_carbs) return (4.184 * w_water) + (1.71 * w_protein) + (1.67 * w_fat) + (1.42 * w_carbs)
def calculate_cook_plan(self, state: DishThermalState, microwave_wattage: int) -> Dict[str, Any]: def calculate_cook_plan(
self, state: DishThermalState, microwave_wattage: int, defrost_mode: bool
) -> Dict[str, Any]:
cp = self.estimate_specific_heat(state.macronutrients, state.estimated_mass_g) cp = self.estimate_specific_heat(state.macronutrients, state.estimated_mass_g)
delta_t = max(0.0, self.TARGET_TEMP_C - state.initial_temp_c) label = state.food_name.lower()
# Q = m * c_p * delta_t # Set target temperature based on selected mode
target_temp = self.DEFROST_TARGET_TEMP_C if defrost_mode else self.COOK_TARGET_TEMP_C
delta_t = max(0.0, target_temp - state.initial_temp_c)
# 1. Base thermal energy: Q_sensible = m * c_p * delta_t
required_joules = state.estimated_mass_g * cp * delta_t required_joules = state.estimated_mass_g * cp * delta_t
effective_power_watts = microwave_wattage * self.DEFAULT_EFFICIENCY
total_seconds = required_joules / effective_power_watts if effective_power_watts > 0 else 0 # 2. Account for Phase Change (Ice -> Water) if food starts below 0°C
if state.initial_temp_c < 0:
water_g = state.macronutrients.get("water_g", state.estimated_mass_g * 0.7)
latent_energy_joules = water_g * self.LATENT_HEAT_ICE_J_G
required_joules += latent_energy_joules
# Determine duty cycle / power level recommendations # 3. Determine power level and duty cycle based on mode
power_level = 100 if defrost_mode:
if state.initial_temp_c < 0: # Frozen food requires defrost cycle to prevent edge-cooking # Defrost mode strictly runs low power (30%) to allow heat conduction
power_level = 50 power_level = 30 if "bread" in label or "baked" in label else 40
total_seconds *= 1.4 time_factor = 1.1 # Slight padding for thermal conductivity losses
else:
# Cook / Reheat Mode logic
if state.initial_temp_c < 0:
# Cooking from frozen needs lower power to defrost first, then cook
power_level = 50
time_factor = 1.35
elif state.estimated_mass_g > 350 and not any(w in label for w in ["soup", "beverage", "water", "tea"]):
power_level = 70
time_factor = 1.2
elif any(w in label for w in ["cheese", "cream", "sauce", "butter", "egg"]):
power_level = 60
time_factor = 1.25
else:
power_level = 100
time_factor = 1.0
# Effective power delivered to food
effective_power_watts = microwave_wattage * self.DEFAULT_EFFICIENCY * (power_level / 100.0)
total_seconds = (required_joules / effective_power_watts * time_factor) if effective_power_watts > 0 else 0
return { return {
"cook_time_seconds": round(total_seconds), "cook_time_seconds": round(total_seconds),
"recommended_power_level_pct": power_level, "recommended_power_level_pct": power_level,
"target_temp": target_temp,
"estimated_specific_heat": round(cp, 2), "estimated_specific_heat": round(cp, 2),
"energy_joules": round(required_joules) "energy_joules": round(required_joules)
} }
+1
View File
@@ -2,3 +2,4 @@ Flask==3.0.2
pymongo==4.6.1 pymongo==4.6.1
gunicorn==21.2.0 gunicorn==21.2.0
opencv-python-headless opencv-python-headless
requests==2.32.3
+1 -1
View File
@@ -16,4 +16,4 @@ def do_connect(ssid, pwd):
print('network config:', sta_if.ifconfig()) print('network config:', sta_if.ifconfig())
# Attempt to connect to WiFi network # Attempt to connect to WiFi network
# do_connect("Smartwave-1", 'Smartwave-prot-1') do_connect("Smartwave-1", 'Smartwave-prot-1')
+19 -12
View File
@@ -55,6 +55,21 @@ def on_mqtt_message(message):
# Unsubscribe from the hello topic since we got a response # Unsubscribe from the hello topic since we got a response
mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO) mqtt_client.unsubscribe(config.MQTT_TOPIC_HELLO)
print("[MQTT Thread] Unsubscribed from topic:", 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_seconds" in payload_data:
print("[MQTT Thread] Cooking sensors init received from the orchestrator")
obj_temp = temperature_sensor.read_object_temp()
amb_temp = 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)
# Here you would handle the cooking parameters, e.g., start a cooking process
# For now, we just print them
print("[MQTT Thread] Message processing complete.") print("[MQTT Thread] Message processing complete.")
mqtt_client.set_callback(on_mqtt_message) mqtt_client.set_callback(on_mqtt_message)
@@ -86,7 +101,6 @@ def mqtt_background_thread():
mqtt_client.publish(topic, payload, qos=config.MQTT_QOS) mqtt_client.publish(topic, payload, qos=config.MQTT_QOS)
# 2. Check for incoming messages (non-blocking poll) # 2. Check for incoming messages (non-blocking poll)
# Shortened timeout to keep the queue responsive
events = poller.poll(200) events = poller.poll(200)
if events: if events:
mqtt_client.wait() mqtt_client.wait()
@@ -145,18 +159,18 @@ def uart_background_thread():
time.sleep(5) time.sleep(5)
# --- Launch background worker --- # --- Launch background worker ---
# _thread.start_new_thread(mqtt_background_thread, ()) _thread.start_new_thread(mqtt_background_thread, ())
# _thread.start_new_thread(uart_background_thread, ()) _thread.start_new_thread(uart_background_thread, ())
# --- MAIN APPLICATION THREAD (Core 0) --- # --- MAIN APPLICATION THREAD (Core 0) ---
print("[Main] Main execution path active.") print("[Main] Main execution path active.")
time.sleep(2) # Give the thread a moment to initial connect time.sleep(2) # Give the thread a moment to initial connect
mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL mqtt_hello_sent_timestamp = -config.MQTT_HELLO_INTERVAL
# mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS) mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
# Temperature sensor setup # Temperature sensor setup
temperature_sensor_i2c = I2C(scl=Pin(25, Pin.IN, Pin.PULL_UP), sda=Pin(26, Pin.IN, Pin.PULL_UP), freq=100000) 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 # Scan to verify the sensor is connected and detected
print("Scanning I2C bus...") print("Scanning I2C bus...")
devices = temperature_sensor_i2c.scan() devices = temperature_sensor_i2c.scan()
@@ -174,13 +188,6 @@ while True:
mqtt_hello_sent_timestamp = time.time() mqtt_hello_sent_timestamp = time.time()
pass pass
# Sensors
print(f"[Main] Reading temperature from sensor...")
obj_temp = temperature_sensor.read_object_temp()
amb_temp = temperature_sensor.read_ambient_temp()
if obj_temp is not None and amb_temp is not None:
print(f"Object: {obj_temp:.2f}°C | Ambient: {amb_temp:.2f}°C")
# 2. Example: Send data to the Heltec board every 5 seconds # 2. Example: Send data to the Heltec board every 5 seconds
# uart_device.send("Status Check: WiFi Active") # uart_device.send("Status Check: WiFi Active")
time.sleep(1) time.sleep(1)
+212 -12
View File
@@ -1,8 +1,11 @@
import base64
import json import json
import threading import threading
import queue import queue
import time import time
import traceback import traceback
import requests
from orchestrateur.sensors import gps from orchestrateur.sensors import gps
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads
from shared.logging import log from shared.logging import log
@@ -21,6 +24,9 @@ except Exception:
# Thread-safe queue for application messages # Thread-safe queue for application messages
data_queue = queue.Queue() data_queue = queue.Queue()
cooking_queue = {}
active_cooks = {}
active_cooks_lock = threading.Lock()
lora = get_lora() lora = get_lora()
lora.configure() lora.configure()
@@ -54,8 +60,9 @@ mqtt_client = get_mqtt_client(
) )
mqtt_client.connect() mqtt_client.connect()
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS) 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}") 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 --- # --- THE CRUCIAL PAHO FIX ---
# Start Paho's internal background thread. This handles all network packets, # Start Paho's internal background thread. This handles all network packets,
@@ -95,13 +102,179 @@ def button_callback():
button.set_callback(button_callback) button.set_callback(button_callback)
# Launch background monitoring workers # Launch background monitoring workers
# threading.Thread(target=lora_listener, daemon=True).start() threading.Thread(target=lora_listener, daemon=True).start()
# threading.Thread(target=mqtt_listener, daemon=True).start() threading.Thread(target=mqtt_listener, daemon=True).start()
# Launch button monitoring thread # Launch button monitoring thread
button.start_button_monitoring_thread() button.start_button_monitoring_thread()
print("Orchestrateur prêt. Le main loop est libre.") 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 # Sensor reading
def read_sensors(): def read_sensors():
"""Read all sensors and return a dictionary of their values.""" """Read all sensors and return a dictionary of their values."""
@@ -138,14 +311,14 @@ def read_sensors():
log(f"Error reading camera data: {e}") log(f"Error reading camera data: {e}")
# Read Button State (last because he can still change state while reading other sensors) # 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 return sensor_data
# --- MAIN EXECUTION LOOP --- # --- MAIN EXECUTION LOOP ---
while True: while True:
try: try:
# Check for non-heartbeat data # === TREAT MESSAGE QUEUE ===
try: try:
msg = data_queue.get(block=False) msg = data_queue.get(block=False)
@@ -154,6 +327,7 @@ while True:
if msg["source"] == "LoRa": if msg["source"] == "LoRa":
print(f"\n[Main Loop] LoRa : Données traitées : {msg['data']}") print(f"\n[Main Loop] LoRa : Données traitées : {msg['data']}")
elif msg["source"] == "MQTT": elif msg["source"] == "MQTT":
# MQTT HELLO
if (msg["topic"] == config.MQTT_TOPIC_HELLO.decode('utf-8')): if (msg["topic"] == config.MQTT_TOPIC_HELLO.decode('utf-8')):
if ("id_orchestrator" in msg["data"] and msg["data"]["id_orchestrator"] == DEVICE_ID): if ("id_orchestrator" in msg["data"] and msg["data"]["id_orchestrator"] == DEVICE_ID):
# Do not answer to messages coming from me # 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) 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}.") print(f"[Main Loop] MQTT : Réponse Hello envoyée à {microwave_id}.")
# TODO : Save in database # 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']}") print(f"\n[Main Loop] MQTT : Données traitées : {msg['data']}")
except queue.Empty: except queue.Empty:
pass 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 # DEBUG : Read sensors
sensor_values = read_sensors() # sensor_values = read_sensors()
if sensor_values: # if sensor_values:
sensor_values_print = sensor_values.copy() # sensor_values_print = sensor_values.copy()
if "camera_image" in sensor_values_print: # if "camera_image" in sensor_values_print:
sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>" # sensor_values_print["camera_image"] = f"<{len(sensor_values_print['camera_image'])} bytes>"
print(f"\nCapteurs Données lues : {sensor_values_print}") # print(f"\nCapteurs Données lues : {sensor_values_print}")
time.sleep(3) time.sleep(3)
+15
View File
@@ -1,3 +1,6 @@
from time import time
try: try:
import ujson as json import ujson as json
except ImportError: except ImportError:
@@ -21,3 +24,15 @@ def mqtt_hello_ack(id_orchestrator, id_microwave):
"id_microwave": id_microwave, "id_microwave": id_microwave,
"id_orchestrator": id_orchestrator "id_orchestrator": id_orchestrator
}) })
def mqtt_cooking_init(id_microwave):
return as_json({
"id_microwave": id_microwave
})
def mqtt_sensor_data(id_microwave, dish_temp, ambient_temp):
return as_json({
"id_microwave": id_microwave,
"dish_temp": dish_temp,
"ambient_temp": ambient_temp
})