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 time
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
@@ -17,6 +18,7 @@ class EdamamAPI:
|
||||
|
||||
def analyze_dish_image(self, image_file_path: str):
|
||||
if SAVE_EDAMAM_API_TOKEN:
|
||||
time.sleep(3)
|
||||
return json.loads("""
|
||||
{
|
||||
"combined": {
|
||||
|
||||
+30
-17
@@ -52,44 +52,57 @@ def cooking_params():
|
||||
if not data:
|
||||
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
|
||||
camera_image_b64 = data.get("camera_image")
|
||||
filepath = None
|
||||
|
||||
if camera_image_b64:
|
||||
# Generate a unique filename using UUID to avoid overwriting
|
||||
filename = f"dish_{uuid.uuid4().hex}.jpg"
|
||||
filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
|
||||
|
||||
try:
|
||||
# Decode the base64 string and save it as a binary file
|
||||
with open(filepath, "wb") as f:
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
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:
|
||||
# Insert the dictionary directly into Mongo (it will retain your exact JSON keys)
|
||||
cooking_collection.insert_one(data)
|
||||
|
||||
# Remove the Mongo-injected '_id' object before returning the response
|
||||
data.pop("_id", None)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
||||
|
||||
# 3. Returns with the cooking parameters
|
||||
cooking_plan = microwave_cook_planner.generate_plan(
|
||||
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
|
||||
# 5. Return complete output
|
||||
return jsonify(cook_plan), 201
|
||||
|
||||
@app.route("/device-network", methods=["POST"])
|
||||
def device_network():
|
||||
|
||||
@@ -50,7 +50,8 @@ class MicrowaveCookPlanner:
|
||||
image_path: str,
|
||||
height_cm: float,
|
||||
initial_temp_c: float,
|
||||
microwave_wattage: int = 900
|
||||
microwave_wattage: int = 900,
|
||||
defrost_mode: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Main pipeline call to parse an image and return cooking parameters."""
|
||||
|
||||
@@ -85,7 +86,8 @@ class MicrowaveCookPlanner:
|
||||
# 5. Cook Plan Calculation
|
||||
cook_plan = self.engine.calculate_cook_plan(
|
||||
state=thermal_state,
|
||||
microwave_wattage=microwave_wattage
|
||||
microwave_wattage=microwave_wattage,
|
||||
defrost_mode=defrost_mode
|
||||
)
|
||||
|
||||
# Return consolidated output
|
||||
|
||||
@@ -4,23 +4,25 @@ from typing import Dict, Any, Optional
|
||||
@dataclass
|
||||
class DishThermalState:
|
||||
food_name: str
|
||||
macronutrients: Dict[str, float] # grams of water, fat, protein, carbs
|
||||
macronutrients: Dict[str, float]
|
||||
estimated_mass_g: float
|
||||
initial_temp_c: float
|
||||
volume_cm3: Optional[float] = None
|
||||
|
||||
|
||||
|
||||
class MicrowaveThermalEngine:
|
||||
"""Calculates cook parameters based on physical properties"""
|
||||
|
||||
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
|
||||
def estimate_specific_heat(macros: Dict[str, float], total_weight_g: float) -> float:
|
||||
"""Estimates Cp in J/(g*C) based on macro composition"""
|
||||
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_protein = macros.get("protein_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)
|
||||
|
||||
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)
|
||||
delta_t = max(0.0, self.TARGET_TEMP_C - state.initial_temp_c)
|
||||
|
||||
# Q = m * c_p * delta_t
|
||||
label = state.food_name.lower()
|
||||
|
||||
# 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
|
||||
effective_power_watts = microwave_wattage * self.DEFAULT_EFFICIENCY
|
||||
|
||||
total_seconds = required_joules / effective_power_watts if effective_power_watts > 0 else 0
|
||||
|
||||
# Determine duty cycle / power level recommendations
|
||||
power_level = 100
|
||||
if state.initial_temp_c < 0: # Frozen food requires defrost cycle to prevent edge-cooking
|
||||
power_level = 50
|
||||
total_seconds *= 1.4
|
||||
|
||||
# 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
|
||||
|
||||
# 3. Determine power level and duty cycle based on mode
|
||||
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 {
|
||||
"cook_time_seconds": round(total_seconds),
|
||||
"recommended_power_level_pct": power_level,
|
||||
"target_temp": target_temp,
|
||||
"estimated_specific_heat": round(cp, 2),
|
||||
"energy_joules": round(required_joules)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
Flask==3.0.2
|
||||
pymongo==4.6.1
|
||||
gunicorn==21.2.0
|
||||
opencv-python-headless
|
||||
opencv-python-headless
|
||||
requests==2.32.3
|
||||
Reference in New Issue
Block a user