Compare commits

...

2 Commits

Author SHA1 Message Date
Ninluc 3711928c7d Enhance image before ai processing
Build, push image, and notify Watchtower / build-image (push) Successful in 41s
Build, push image, and notify Watchtower / notify (push) Successful in 14s
2026-08-23 12:33:39 +02:00
Ninluc 6b8c1688a6 Send alerts and reset 2026-08-23 12:33:25 +02:00
5 changed files with 83 additions and 8 deletions
+24 -3
View File
@@ -17,6 +17,7 @@ from APIs.mqtt import send_command
from APIs.webex import WebexManager from APIs.webex import WebexManager
from APIs.shodan import ShodanAuditor from APIs.shodan import ShodanAuditor
from APIs.twilio import send_alert_sms from APIs.twilio import send_alert_sms
from cloud.image_enhancer import enhance_image_for_ai
from microwaveCookPlanner import MicrowaveCookPlanner from microwaveCookPlanner import MicrowaveCookPlanner
import safety_checker import safety_checker
from jobs import job_server_cve_audit, job_client_ip_audit, job_request_telemetry from jobs import job_server_cve_audit, job_client_ip_audit, job_request_telemetry
@@ -153,7 +154,7 @@ start_scheduler_once()
# --------------------------------------------------------- # ---------------------------------------------------------
# Authentication Middleware # Authentication Middleware
# --------------------------------------------------------- # ---------------------------------------------------------
EXEMPT_ROUTES = {'hello_world', 'oauth_callback', 'odata_metadata', 'debug_telemetryrequest', 'trigger_job_manually'} EXEMPT_ROUTES = {'hello_world', 'oauth_callback', 'odata_metadata', 'debug_telemetryrequest', 'trigger_job_manually', 'debug_unsafe_dish', 'debug_safe_dish'}
@app.before_request @app.before_request
def authenticate_request(): def authenticate_request():
@@ -246,11 +247,13 @@ async def cooking_params():
f.write(base64.b64decode(camera_image_b64)) f.write(base64.b64decode(camera_image_b64))
data["camera_image"] = filepath data["camera_image"] = filepath
enhanced_filepath = enhance_image_for_ai(filepath)
# Concurrent AI Execution # Concurrent AI Execution
safety_task = asyncio.to_thread(safety_checker.check_dish_safety, filepath) safety_task = asyncio.to_thread(safety_checker.check_dish_safety, enhanced_filepath)
planner_task = asyncio.to_thread( planner_task = asyncio.to_thread(
microwave_cook_planner.generate_plan, microwave_cook_planner.generate_plan,
image_path=filepath, image_path=enhanced_filepath,
height_cm=height_cm, height_cm=height_cm,
initial_temp_c=initial_temp_c, initial_temp_c=initial_temp_c,
microwave_wattage=microwave_wattage, microwave_wattage=microwave_wattage,
@@ -585,6 +588,24 @@ def trigger_job_manually(job_id):
"message": f"Job execution failed: {str(e)}" "message": f"Job execution failed: {str(e)}"
}), 500 }), 500
@app.route("/debug/dishsafety/not_safe", methods=["GET"])
def debug_unsafe_dish():
"""Debug endpoint to simulate an unsafe dish scenario."""
# Simulated unsafe dish data
unsafe_dish = "storage/dishPhotos/dish_78827473286c4f67855f3fbd0ccb9bb4.jpg"
# Call the cooking_params endpoint logic directly
return safety_checker.check_dish_safety(unsafe_dish)
@app.route("/debug/dishsafety/safe", methods=["GET"])
def debug_safe_dish():
"""Debug endpoint to simulate a safe dish scenario."""
# Simulated safe dish data
unsafe_dish = "storage/dishPhotos/dish_972251744d034b32bb5134ff9d0f071c.jpg"
# Call the cooking_params endpoint logic directly
return safety_checker.check_dish_safety(unsafe_dish)
# --------------------------------------------------------- # ---------------------------------------------------------
# OData Metadata Definition # OData Metadata Definition
# --------------------------------------------------------- # ---------------------------------------------------------
+23
View File
@@ -0,0 +1,23 @@
import cv2
def enhance_image_for_ai(input_path: str) -> str:
"""Enhances dark areas and contrast using CLAHE without blowing out bright areas."""
img = cv2.imread(input_path)
if img is None:
return input_path
# Convert to LAB color space to modify luminance channel only
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
# Apply CLAHE to Lightness channel
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
cl = clahe.apply(l)
# Merge channels and convert back to BGR
limg = cv2.merge((cl, a, b))
enhanced = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
enhanced_path = input_path.replace(".jpg", "_enhanced.jpg")
cv2.imwrite(enhanced_path, enhanced)
return enhanced_path
+16
View File
@@ -2,6 +2,7 @@ import json
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from APIs.aichat import generate from APIs.aichat import generate
import shared.config as config import shared.config as config
from PIL import Image, ImageEnhance
class UtensilMaterialCheck(BaseModel): class UtensilMaterialCheck(BaseModel):
object_name: str = Field( object_name: str = Field(
@@ -28,6 +29,21 @@ class DishSafetyResult(BaseModel):
description="If is_safe is False, set to 'REMOVE METAL UTENSIL OR FOIL BEFORE MICROWAVING'. Otherwise empty ''." description="If is_safe is False, set to 'REMOVE METAL UTENSIL OR FOIL BEFORE MICROWAVING'. Otherwise empty ''."
) )
def preprocess_dish_image(image_path: str) -> str:
"""Brightens dark areas and sharpens food texture for small vision models."""
img = Image.open(image_path)
# 1. Boost brightness slightly
enhancer = ImageEnhance.Brightness(img)
img = enhancer.enhance(1.4)
# 2. Boost contrast to separate food shapes from dark shadows
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(1.3)
processed_path = "/tmp/processed_dish.jpg"
img.save(processed_path, quality=85)
return processed_path
def check_dish_safety(image_path: str) -> dict: def check_dish_safety(image_path: str) -> dict:
prompt = ( prompt = (
+4 -3
View File
@@ -104,7 +104,7 @@ def on_microwave_change(state: MicrowaveState, field: MicrowaveStateFields):
if field != MicrowaveStateFields.STATE: # No external screen or lcd uses this field, so we can skip updates for it if field != MicrowaveStateFields.STATE: # No external screen or lcd uses this field, so we can skip updates for it
notify_display_update() notify_display_update()
if field == MicrowaveStateFields.STATE and state.state in (MicrowaveState.DONE, MicrowaveState.ANALYZING, MicrowaveState.WAITING_FOR_CLOUD, MicrowaveState.ALERT): if field == MicrowaveStateFields.STATE and state.state in (MicrowaveState.IDLE, MicrowaveState.DONE, MicrowaveState.ANALYZING, MicrowaveState.WAITING_FOR_CLOUD, MicrowaveState.ALERT):
# Send to LoRa device if state changed # Send to LoRa device if state changed
lora.send_reliable(payloads.lora_microwave_state(state.state), max_retries=5) lora.send_reliable(payloads.lora_microwave_state(state.state), max_retries=5)
@@ -457,11 +457,11 @@ button.start_button_monitoring_thread()
# --- Alert manager --- # --- Alert manager ---
alert_manager = None alert_manager = None
def on_new_alert(alert: Alert): async def on_new_alert(alert: Alert):
# Sends to cloud server # Sends to cloud server
alert_dict = alert.to_dict() alert_dict = alert.to_dict()
logs_list = asyncio.run(get_systemd_logs(lines=200, service_name="smartwave")) logs_list = await get_systemd_logs(lines=200, service_name="smartwave")
payload = { payload = {
"orchestrator_id": DEVICE_ID, "orchestrator_id": DEVICE_ID,
@@ -485,6 +485,7 @@ def on_new_alert(alert: Alert):
print(f"[Alert Handling] Sending alert to LoRa device: {payload}") print(f"[Alert Handling] Sending alert to LoRa device: {payload}")
if lora.send_reliable(payload): if lora.send_reliable(payload):
microwave_states["2"].set_cooking_state(CookingStates.ALERT) microwave_states["2"].set_cooking_state(CookingStates.ALERT)
buzzer.buzzer_siren(beatsNb=5)
# --- HARDWARE CONTROLLERS --- # --- HARDWARE CONTROLLERS ---
def _stop_hardware(microwave_id: str): def _stop_hardware(microwave_id: str):
+16 -2
View File
@@ -1,4 +1,6 @@
import time import time
import asyncio
import inspect
class AlertManager: class AlertManager:
def __init__(self): def __init__(self):
@@ -8,10 +10,22 @@ class AlertManager:
def set_on_alert_callback(self, callback): def set_on_alert_callback(self, callback):
self.on_alert_callback = callback self.on_alert_callback = callback
def call_on_alert_callback(self, alert):
if self.on_alert_callback:
if inspect.iscoroutinefunction(self.on_alert_callback):
# Schedule coroutine on the running loop without blocking
try:
loop = asyncio.get_running_loop()
loop.create_task(self.on_alert_callback(alert))
except RuntimeError:
# Fallback if called outside an active event loop
asyncio.run(self.on_alert_callback(alert))
else:
self.on_alert_callback(alert)
def add_alert(self, alert): def add_alert(self, alert):
self.alerts.append(alert) self.alerts.append(alert)
if self.on_alert_callback: self.call_on_alert_callback(alert)
self.on_alert_callback(alert)
def get_alerts(self): def get_alerts(self):
return self.alerts return self.alerts