Better cook parameter estimation + Defrost mode + Removed esp-wifi debugs + Orchestrator and microwave exchange
This commit is contained in:
@@ -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
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
# 2. Account for Phase Change (Ice -> Water) if food starts below 0°C
|
||||||
total_seconds = required_joules / effective_power_watts if effective_power_watts > 0 else 0
|
if state.initial_temp_c < 0:
|
||||||
|
water_g = state.macronutrients.get("water_g", state.estimated_mass_g * 0.7)
|
||||||
# Determine duty cycle / power level recommendations
|
latent_energy_joules = water_g * self.LATENT_HEAT_ICE_J_G
|
||||||
power_level = 100
|
required_joules += latent_energy_joules
|
||||||
if state.initial_temp_c < 0: # Frozen food requires defrost cycle to prevent edge-cooking
|
|
||||||
power_level = 50
|
# 3. Determine power level and duty cycle based on mode
|
||||||
total_seconds *= 1.4
|
if defrost_mode:
|
||||||
|
# Defrost mode strictly runs low power (30%) to allow heat conduction
|
||||||
|
power_level = 30 if "bread" in label or "baked" in label else 40
|
||||||
|
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,4 +1,5 @@
|
|||||||
Flask==3.0.2
|
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
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
+213
-13
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ujson as json
|
import ujson as json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -20,4 +23,16 @@ def mqtt_hello_ack(id_orchestrator, id_microwave):
|
|||||||
return as_json({
|
return as_json({
|
||||||
"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
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user