Compare commits

...

34 Commits

Author SHA1 Message Date
Ninluc e68ffd9b8b Fix
Build, push image, and notify Watchtower / build-image (push) Successful in 43s
Build, push image, and notify Watchtower / notify (push) Successful in 10s
2026-08-25 20:07:38 +02:00
Ninluc 49fd566457 Idk
Build, push image, and notify Watchtower / build-image (push) Successful in 48s
Build, push image, and notify Watchtower / notify (push) Successful in 10s
2026-08-25 20:01:36 +02:00
Ninluc dd37465546 OOpsie
Build, push image, and notify Watchtower / build-image (push) Successful in 50s
Build, push image, and notify Watchtower / notify (push) Successful in 13s
2026-08-25 19:23:10 +02:00
Ninluc 3753f57041 Small things because fuck sd cards
Build, push image, and notify Watchtower / build-image (push) Successful in 3m22s
Build, push image, and notify Watchtower / notify (push) Successful in 1m24s
2026-08-25 19:16:50 +02:00
Ninluc 688fd26db8 Changed interface 2026-08-25 19:16:41 +02:00
Ninluc 4d7706f8e1 Adde command to setuot thread network 2026-08-25 19:16:13 +02:00
Ninluc efbc147f48 Default debug to false 2026-08-25 19:16:01 +02:00
Ninluc 4085305039 Save Analyzed image 2026-08-25 19:15:52 +02:00
Ninluc fed99772fc Don't override microwave ID
Build, push image, and notify Watchtower / build-image (push) Successful in 46s
Build, push image, and notify Watchtower / notify (push) Successful in 8s
2026-08-24 15:44:22 +02:00
Ninluc 204f271fc2 Fix global
Build, push image, and notify Watchtower / build-image (push) Successful in 42s
Build, push image, and notify Watchtower / notify (push) Successful in 14s
2026-08-23 20:04:26 +02:00
Ninluc 5c60de3f03 Fix buzzer 2026-08-23 20:04:20 +02:00
Ninluc 8a71d4eaa7 :(
Build, push image, and notify Watchtower / build-image (push) Successful in 37s
Build, push image, and notify Watchtower / notify (push) Successful in 12s
2026-08-23 18:52:05 +02:00
Ninluc eb80d13300 :(
Build, push image, and notify Watchtower / build-image (push) Successful in 39s
Build, push image, and notify Watchtower / notify (push) Successful in 12s
2026-08-23 18:43:16 +02:00
Ninluc 6e2f849ee2 :(
Build, push image, and notify Watchtower / build-image (push) Successful in 39s
Build, push image, and notify Watchtower / notify (push) Successful in 19s
2026-08-23 18:38:41 +02:00
Ninluc 68250e8069 IDC
Build, push image, and notify Watchtower / build-image (push) Successful in 51s
Build, push image, and notify Watchtower / notify (push) Successful in 12s
2026-08-23 18:30:00 +02:00
Ninluc fe31ac978a Dissociate DEBUG variables 2026-08-23 17:48:04 +02:00
Ninluc 3c50fc4d0e Follow the damn prompt CJ
Build, push image, and notify Watchtower / build-image (push) Successful in 39s
Build, push image, and notify Watchtower / notify (push) Successful in 10s
2026-08-23 17:45:13 +02:00
Ninluc 3aed0e5f00 Fuck the blur
Build, push image, and notify Watchtower / build-image (push) Successful in 42s
Build, push image, and notify Watchtower / notify (push) Successful in 16s
2026-08-23 17:36:57 +02:00
Ninluc f55336a636 Tried my own
Build, push image, and notify Watchtower / build-image (push) Successful in 47s
Build, push image, and notify Watchtower / notify (push) Successful in 15s
2026-08-23 17:33:24 +02:00
Ninluc c58ae4603e Skip sending implicit new state
Build, push image, and notify Watchtower / build-image (push) Successful in 39s
Build, push image, and notify Watchtower / notify (push) Successful in 11s
2026-08-23 17:07:18 +02:00
Ninluc e7f6374200 Skip food safety alert for webex and sms 2026-08-23 17:06:59 +02:00
Ninluc 9053351674 idk
Build, push image, and notify Watchtower / build-image (push) Successful in 37s
Build, push image, and notify Watchtower / notify (push) Successful in 9s
2026-08-23 16:51:45 +02:00
Ninluc 1f53e7e6c3 Better prompt again ?
Build, push image, and notify Watchtower / build-image (push) Successful in 34s
Build, push image, and notify Watchtower / notify (push) Successful in 15s
2026-08-23 16:51:27 +02:00
Ninluc 7c35b52dbd Precision in prompt + better camera
Build, push image, and notify Watchtower / build-image (push) Successful in 38s
Build, push image, and notify Watchtower / notify (push) Successful in 11s
2026-08-23 16:48:30 +02:00
Ninluc 0f576467e1 send_reliably if cooking state update
Build, push image, and notify Watchtower / build-image (push) Successful in 40s
Build, push image, and notify Watchtower / notify (push) Successful in 15s
2026-08-23 16:19:09 +02:00
Ninluc 9d1a1f4c78 Try with normal image 2026-08-23 16:18:53 +02:00
Ninluc 701e7f1491 Reapply "Fix move to new messages"
This reverts commit e652831ce5.
2026-08-23 16:14:04 +02:00
Ninluc e652831ce5 Revert "Fix move to new messages"
This reverts commit 3bead59b23.
2026-08-23 16:12:15 +02:00
Ninluc ebc28546bc Better camera settings 2026-08-23 16:10:11 +02:00
Ninluc a51bfc1388 Bigger gaps 2026-08-23 16:10:00 +02:00
Ninluc 7082ea3f8b Better camera settings + better rpompt
Build, push image, and notify Watchtower / build-image (push) Successful in 45s
Build, push image, and notify Watchtower / notify (push) Successful in 12s
2026-08-23 14:32:17 +02:00
Ninluc e802386adf Set to tidle when error requesting cooking plan 2026-08-23 14:22:59 +02:00
Ninluc 289124b7f8 Change State when microwave state is Idel 2026-08-23 14:22:39 +02:00
Ninluc 3bead59b23 Fix move to new messages 2026-08-23 13:04:16 +02:00
22 changed files with 365 additions and 240 deletions
+8 -8
View File
@@ -911,15 +911,15 @@ class EdamamAPI:
""") """)
url = "https://api.edamam.com/api/vision/v1/nutrients" url = "https://api.edamam.com/api/vision/v1/nutrients?app_id=09cb5bd7&app_key=8b2730f3c7f4f075cc939da3fa0c49fb&beta=true&servings=1"
# Query parameters as specified by the YAML schema # Query parameters as specified by the YAML schema
params = { # params = {
"app_id": EDAMAM_APP_ID__FOOD, # "app_id": EDAMAM_APP_ID__FOOD,
"app_key": EDAMAM_APP_KEY__FOOD, # "app_key": EDAMAM_APP_KEY__FOOD,
"beta": "true" # Recommended since vision is in Beta # "beta": "true" # Recommended since vision is in Beta
} # }
print("params:", params) # print("params:", params)
# Specify the media type in the header # Specify the media type in the header
headers = { headers = {
@@ -932,7 +932,7 @@ class EdamamAPI:
response = requests.post( response = requests.post(
url, url,
params=params, # params=params,
headers=headers, headers=headers,
data=raw_image_bytes data=raw_image_bytes
) )
+12 -8
View File
@@ -220,7 +220,7 @@ async def cooking_params():
if not data or not isinstance(data, dict): if not data or not isinstance(data, dict):
return jsonify({"@odata.error": {"code": "400", "message": "Invalid payload"}}), 400 return jsonify({"@odata.error": {"code": "400", "message": "Invalid payload"}}), 400
data["microwave_id"] = g.device_id # data["microwave_id"] = g.device_id
forwarded_for = request.headers.get('X-Forwarded-For') forwarded_for = request.headers.get('X-Forwarded-For')
client_ip = forwarded_for.split(',')[0].strip() if forwarded_for else request.remote_addr client_ip = forwarded_for.split(',')[0].strip() if forwarded_for else request.remote_addr
@@ -250,7 +250,7 @@ async def cooking_params():
enhanced_filepath = enhance_image_for_ai(filepath) enhanced_filepath = enhance_image_for_ai(filepath)
# Concurrent AI Execution # Concurrent AI Execution
safety_task = asyncio.to_thread(safety_checker.check_dish_safety, enhanced_filepath) safety_task = asyncio.to_thread(safety_checker.check_dish_safety, filepath)
planner_task = asyncio.to_thread( planner_task = asyncio.to_thread(
microwave_cook_planner.generate_plan, microwave_cook_planner.generate_plan,
image_path=enhanced_filepath, image_path=enhanced_filepath,
@@ -323,6 +323,14 @@ def alert():
alert_type = alert_info.get("type", "Unknown") alert_type = alert_info.get("type", "Unknown")
alert_message = alert_info.get("message", "No message provided") alert_message = alert_info.get("message", "No message provided")
webex_room_id = None
room_link = None
room_title = None
webex_status = "skipped"
sms_sent = False
if alert_type != "COOKING_SAFETY":
client_name = f"Unknown Client (Orchestrator {orchestrator_id})" if orchestrator_id else "Unknown Client" client_name = f"Unknown Client (Orchestrator {orchestrator_id})" if orchestrator_id else "Unknown Client"
client_email = None client_email = None
client_phone = None client_phone = None
@@ -343,11 +351,7 @@ def alert():
client_email = contact_info.get("email") client_email = contact_info.get("email")
client_phone = contact_info.get("phone") client_phone = contact_info.get("phone")
webex_room_id = None
room_title = f"Support - {client_name}" room_title = f"Support - {client_name}"
room_link = None
webex_status = "skipped"
sms_sent = False
three_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=3) three_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=3)
@@ -601,7 +605,7 @@ def debug_unsafe_dish():
def debug_safe_dish(): def debug_safe_dish():
"""Debug endpoint to simulate a safe dish scenario.""" """Debug endpoint to simulate a safe dish scenario."""
# Simulated safe dish data # Simulated safe dish data
unsafe_dish = "storage/dishPhotos/dish_def0047482e6430488dbf9a47d73271a.jpg" unsafe_dish = "storage/dishPhotos/dish_af60c26a08a74ce894d453394f417c22.jpg"
# Call the cooking_params endpoint logic directly # Call the cooking_params endpoint logic directly
return safety_checker.check_dish_safety(unsafe_dish) return safety_checker.check_dish_safety(unsafe_dish)
@@ -660,4 +664,4 @@ def odata_metadata():
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=getattr(config, "DEBUG", True)) app.run(debug=getattr(config, "DEBUG", False))
+17
View File
@@ -1,6 +1,12 @@
import cv2 import cv2
import numpy as np import numpy as np
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import sys
sys.path.insert(0, '..')
try:
from shared import config
except ImportError:
from ..shared import config
class MicrowaveDishAnalyzer: class MicrowaveDishAnalyzer:
@@ -42,6 +48,17 @@ class MicrowaveDishAnalyzer:
largest_contour = max(contours, key=cv2.contourArea) largest_contour = max(contours, key=cv2.contourArea)
area_pixels = cv2.contourArea(largest_contour) area_pixels = cv2.contourArea(largest_contour)
# DEBUG CONDITION: Draw and save contour image if DEBUG_DISHANALYZER is set
if getattr(config, "DEBUG_DISHANALYZER", False):
annotated_image = image.copy()
# Draw the contour in red (BGR: 0, 0, 255) with thickness of 2
cv2.drawContours(annotated_image, [largest_contour], -1, (0, 0, 255), 2)
# Handle file extensions gracefully
analyzed_path = image_path.replace(".jpg", "_analyzed.jpg")
cv2.imwrite(analyzed_path, annotated_image)
# 5. Convert pixels² to cm² using scale ratio squared # 5. Convert pixels² to cm² using scale ratio squared
area_cm2 = area_pixels * (self.cm_per_pixel ** 2) area_cm2 = area_pixels * (self.cm_per_pixel ** 2)
return float(area_cm2) return float(area_cm2)
+13 -10
View File
@@ -4,25 +4,26 @@ from APIs.aichat import generate
import shared.config as config import shared.config as config
class DishSafetyResult(BaseModel): class DishSafetyResult(BaseModel):
utensil_or_foreign_object_detected: bool = Field(
description="Set to True if ANY spoon, fork, knife, handle, or foil is inside or touching the bowl. Set to False ONLY if the bowl contains ONLY 100% food."
)
is_safe: bool = Field( is_safe: bool = Field(
description="Set to True if the dish contains ONLY normal food, rice, stew, and ceramic/glass/plastic bowls. Set to False ONLY if shiny metallic cutlery (spoon/fork/knife) or metallic foil is present." description="MUST be False if utensil_or_foreign_object_detected is True. Set to True ONLY if pure food/bowl with ZERO utensils."
) )
detected_hazards: list[str] = Field( detected_hazards: list[str] = Field(
description="List ONLY physical metal objects found (e.g., ['metal spoon']). If is_safe is True, this MUST be an empty list []." description="List any items found (e.g., ['spoon']). Empty list [] if safe."
) )
warning_message: str = Field( warning_message: str = Field(
description="Clear warning if is_safe is False, otherwise empty string ''." description="Warning statement if unsafe, otherwise empty string ''."
) )
def check_dish_safety(image_path: str) -> dict: def check_dish_safety(image_path: str) -> dict:
prompt = ( prompt = (
"You are a microwave safety vision inspector.\n\n" "Inspect this top-down photo of a food dish inside a microwave.\n\n"
"TASK: Check if this dish contains any REAL METAL CUTLERY (metal spoons, forks, knives) or ALUMINUM FOIL.\n\n" "STRICT MICROWAVE RULES:\n"
"CRITICAL RULES:\n" "1. Check if ANY utensil (spoon, fork, knife, or handle) is present in or on the bowl.\n"
"1. Rice, curry, stew, potatoes, herbs, dark sauce, and ceramic/plastic/glass bowls are SAFE food items.\n" "2. If ANY spoon or utensil is present (metal, plastic, or ceramic), you MUST set `utensil_or_foreign_object_detected = True` and `is_safe = False`.\n"
"2. Camera image noise, shadows, and food textures are NOT metal objects.\n" "3. `is_safe` can ONLY be True if the dish contains strictly food and nothing else."
"3. Unless a shiny, metallic silver/gold utensil or foil sheet is clearly visible, mark is_safe = True and detected_hazards = [].\n"
"4. Do NOT invent or guess utensils if none are clearly visible."
) )
response_raw = generate( response_raw = generate(
@@ -31,6 +32,8 @@ def check_dish_safety(image_path: str) -> dict:
output_format=DishSafetyResult, output_format=DishSafetyResult,
should_think=False, should_think=False,
) )
if config.DEBUG_MESSAGES:
print(f"[Debug] Raw response from AI generator: {response_raw}") print(f"[Debug] Raw response from AI generator: {response_raw}")
if isinstance(response_raw, str): if isinstance(response_raw, str):
+16
View File
@@ -1,3 +1,19 @@
`esphome run epaper_thread_node.yaml` `esphome run epaper_thread_node.yaml`
`esphome logs epaper_thread_node.yaml` `esphome logs epaper_thread_node.yaml`
# Config OTBR
## 1. Stop the active Thread interface
docker exec -it otbr ot-ctl thread stop
docker exec -it otbr ot-ctl ifconfig down
## 2. Inject your ESP32-H2 Active Dataset TLV
docker exec -it otbr ot-ctl dataset set active 00030000184a0300000b35060004001fffe00208f7754c0c4a4dea9a0708fd1adbbe7435e95405103f543281275a135e38982974aa71e987030f4f70656e5468726561642d3766343101027f410410a500c86abfe7ef17888c2232feac2dc60c0402a0f7f80e080000000000010000
## 3. Commit the dataset as active
docker exec -it otbr ot-ctl dataset commit active
## 4. Bring up the interface and start Thread
docker exec -it otbr ot-ctl ifconfig up
docker exec -it otbr ot-ctl thread start
+21 -4
View File
@@ -49,19 +49,26 @@ PING_PAYLOAD = {
"type": deviceTypes.DEVICE_TYPES["MICROWAVE"] "type": deviceTypes.DEVICE_TYPES["MICROWAVE"]
} }
last_cooking_update = {}
def send_cooking_update(state=None, start_time=None, estimated_remaining_time=None, paused=None): def send_cooking_update(state=None, start_time=None, estimated_remaining_time=None, paused=None):
"""Sends a flexible COOKING_UPDATE payload containing only populated fields.""" """Sends a flexible COOKING_UPDATE payload containing only populated fields."""
global last_cooking_update
if not lora: if not lora:
return return
send_reliably = False
payload = { payload = {
"id": DEVICE_ID, "id": DEVICE_ID,
"action": LoraCommands.COOKING_UPDATE "action": LoraCommands.COOKING_UPDATE
} }
if state is not None: if state is not None and state != last_cooking_update.get("cooking_state"):
payload["cooking_state"] = state payload["cooking_state"] = state
send_reliably = True
if estimated_remaining_time is not None: if estimated_remaining_time is not None:
payload["estimated_remaining_time"] = estimated_remaining_time payload["estimated_remaining_time"] = estimated_remaining_time
if paused is not None: if paused is not None:
@@ -70,7 +77,10 @@ def send_cooking_update(state=None, start_time=None, estimated_remaining_time=No
# Only transmit if at least one field beyond id and action was provided # Only transmit if at least one field beyond id and action was provided
if len(payload) > 2: if len(payload) > 2:
log(f"[LoRa] Sending COOKING_UPDATE payload: {payload}") log(f"[LoRa] Sending COOKING_UPDATE payload: {payload}")
last_cooking_update = payload.copy()
if send_reliably:
lora.send_reliable(payload)
else:
lora.send(payload) lora.send(payload)
@@ -195,7 +205,7 @@ def cooking_state_temperature_provider():
return (0.0, 0.0) return (0.0, 0.0)
def cooking_state_on_state_change(state): def cooking_state_on_state_change(state):
global cooking_start_time global cooking_start_time, uart_device
print(f"[CookingState] State changed to: {state.state}") print(f"[CookingState] State changed to: {state.state}")
if state.paused or state.state in (cookingState.CookingStates.DONE, cookingState.CookingStates.IDLE, cookingState.CookingStates.ALERT): if state.paused or state.state in (cookingState.CookingStates.DONE, cookingState.CookingStates.IDLE, cookingState.CookingStates.ALERT):
@@ -204,7 +214,7 @@ def cooking_state_on_state_change(state):
magnetron_led.on() magnetron_led.on()
# Send state updates to WiFi board and Orchestrator # Send state updates to WiFi board and Orchestrator
if uart_device: if uart_device != None:
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state})) uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state}))
# Send LoRa update with start time and remaining time if beginning cooking process # Send LoRa update with start time and remaining time if beginning cooking process
@@ -336,6 +346,9 @@ async def lora_process_task():
if alert: if alert:
alert.timestamp = data.get("timestamp", time.time()) alert.timestamp = data.get("timestamp", time.time())
alert_manager.add_alert(alert) alert_manager.add_alert(alert)
microwave_state = MicrowaveState.ALERT
if cooking_state is not None:
cooking_state.set_state(cookingState.CookingStates.ALERT)
elif action == LoraCommands.MICROVAVE_STATE_UPDATE: elif action == LoraCommands.MICROVAVE_STATE_UPDATE:
new_state = data.get("new_microwave_state") new_state = data.get("new_microwave_state")
@@ -344,6 +357,10 @@ async def lora_process_task():
microwave_state = new_state microwave_state = new_state
if new_state not in (MicrowaveState.ALERT): # Not alert so we can show the error if new_state not in (MicrowaveState.ALERT): # Not alert so we can show the error
update_screen() update_screen()
if new_state == MicrowaveState.IDLE:
if cooking_state is not None:
cooking_state.set_state(cookingState.CookingStates.IDLE)
await asyncio.sleep_ms(20)
await asyncio.sleep_ms(100) await asyncio.sleep_ms(100)
+1 -1
View File
@@ -9,7 +9,7 @@ services:
- NET_ADMIN - NET_ADMIN
environment: environment:
- OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800 - OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800
- OT_INFRA_IF=wlx6815790f3204 - OT_INFRA_IF=wlan1
- OT_THREAD_IF=wpan0 - OT_THREAD_IF=wpan0
- OT_LOG_LEVEL=6 - OT_LOG_LEVEL=6
- FIREWALL=0 - FIREWALL=0
+17 -12
View File
@@ -3,6 +3,7 @@ set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(dirname -- "$SCRIPT_DIR") REPO_ROOT=$(dirname -- "$SCRIPT_DIR")
VENV_DIR="$SCRIPT_DIR/venv"
PYTHON_BIN="${PYTHON_BIN:-python3}" PYTHON_BIN="${PYTHON_BIN:-python3}"
PYTHON_SCRIPT="${1:-$SCRIPT_DIR/main.py}" PYTHON_SCRIPT="${1:-$SCRIPT_DIR/main.py}"
REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt" REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt"
@@ -19,24 +20,28 @@ cd "$SCRIPT_DIR"
# 2. On lance Docker en arrière-plan # 2. On lance Docker en arrière-plan
echo "Démarrage des conteneurs Docker..." echo "Démarrage des conteneurs Docker..."
# docker compose pull
docker compose up -d --remove-orphans docker compose up -d --remove-orphans
# 3. Installation des dépendances (sans '--user' si on est déjà root sous systemd) # 3. Gestion propre de l'environnement virtuel Python (contourne PEP 668)
if [ -f "$REQUIREMENTS_FILE" ]; then if [ ! -d "$VENV_DIR" ]; then
echo "Vérification des dépendances Python..." echo "Création de l'environnement virtuel Python..."
if [ "$(id -u)" -eq 0 ]; then "$PYTHON_BIN" -m venv "$VENV_DIR"
"$PYTHON_BIN" -m pip install -r "$REQUIREMENTS_FILE"
else
"$PYTHON_BIN" -m pip install --user -r "$REQUIREMENTS_FILE"
fi fi
# Utiliser le pip/python du venv
VENV_PYTHON="$VENV_DIR/bin/python"
VENV_PIP="$VENV_DIR/bin/pip"
if [ -f "$REQUIREMENTS_FILE" ]; then
echo "Vérification et installation des dépendances dans le venv..."
"$VENV_PIP" install --upgrade pip >/dev/null 2>&1 || true
"$VENV_PIP" install -r "$REQUIREMENTS_FILE"
fi fi
# 4. Sécurité anti-conflit : On vérifie si Docker n'a pas verrouillé le port USB de la LA66 # 4. Sécurité anti-conflit : On vérifie si Docker n'a pas verrouillé le port USB de la LA66
PORT="/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0" PORT="/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"
if [ -e "$PORT" ]; then if [ -e "$PORT" ]; then
echo "Vérification de la disponibilité du port série..." echo "Vérification de la disponibilité du port série..."
# Si le port est occupé, on force sa libération en tuant le processus docker/screen persistant
if lsof "$PORT" >/dev/null 2>&1; then if lsof "$PORT" >/dev/null 2>&1; then
echo "Le port série est occupé ! Libération en cours..." echo "Le port série est occupé ! Libération en cours..."
fuser -k "$PORT" || true fuser -k "$PORT" || true
@@ -44,7 +49,7 @@ if [ -e "$PORT" ]; then
fi fi
fi fi
# 5. Lancement propre du script Python # 5. Lancement propre du script Python via le venv
echo "Launching Python script: $PYTHON_SCRIPT" echo "Launching Python script: $PYTHON_SCRIPT"
cd "$REPO_ROOT" cd "$SCRIPT_DIR"
exec "$PYTHON_BIN" "$PYTHON_SCRIPT" exec "$VENV_PYTHON" "$PYTHON_SCRIPT"
+26 -28
View File
@@ -3,6 +3,7 @@ import json
import time import time
import asyncio import asyncio
import requests import requests
import os
from shared.microwave_state import MicrowaveState, MicrowaveStateFields from shared.microwave_state import MicrowaveState, MicrowaveStateFields
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db
@@ -26,7 +27,8 @@ def is_technician_mode_active() -> bool:
return TECHNICIAN_MODE return TECHNICIAN_MODE
# --- DB SETUP --- # --- DB SETUP ---
DB_PATH = "orchestrateur/db.sqlite" BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "db.sqlite")
def init_db(): def init_db():
"""Ensures the connected_components table exists on startup.""" """Ensures the connected_components table exists on startup."""
@@ -104,10 +106,19 @@ 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.IDLE, 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):
# 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)
if field == MicrowaveStateFields.COOKING_STATE:
update_lcd_microwave_count()
if state.cooking_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT):
# Trigger buzzer for user attention
beats = 5 if state.cooking_state == CookingStates.ALERT else (1 if state.cooking_state == CookingStates.STIRRING_REQUIRED else 3)
buzzer.buzzer_siren(beats_nb=beats)
elif state.cooking_state == CookingStates.DONE and state.state == MicrowaveState.COOKING:
state.set_state(MicrowaveState.DONE)
print(f"[{state.microwave_id}] Cooking finished. Waiting for user to remove dish.")
def set_cloud_alert(state: bool): def set_cloud_alert(state: bool):
"""Setter for cloud_alert that triggers a display update when changed.""" """Setter for cloud_alert that triggers a display update when changed."""
@@ -266,17 +277,17 @@ async def rfid_listener_task():
TECHNICIAN_MODE = True TECHNICIAN_MODE = True
print(f"[RFID] Card {tag_id} VALIDATED! -> TECHNICIAN_MODE = TRUE") print(f"[RFID] Card {tag_id} VALIDATED! -> TECHNICIAN_MODE = TRUE")
# Optional: Sound positive feedback beep # Optional: Sound positive feedback beep
buzzer.buzzer_siren(beatsNb=1) buzzer.buzzer_siren(beats_nb=2)
else: else:
print(f"[RFID] Card {tag_id} REJECTED / Invalid.") print(f"[RFID] Card {tag_id} REJECTED / Invalid.")
# Optional: Sound error feedback beep # Optional: Sound error feedback beep
buzzer.buzzer_siren(beatsNb=2) buzzer.buzzer_siren(beats_nb=4)
except Exception as e: except Exception as e:
print(f"[RFID Task] Error reading RFID tag: {e}") print(f"[RFID Task] Error reading RFID tag: {e}")
# Sleep 100ms between checks to keep CPU usage near zero # Sleep 100ms between checks to keep CPU usage near zero
await asyncio.sleep(0.1) await asyncio.sleep(0.2)
async def display_broadcast_worker_task(): async def display_broadcast_worker_task():
""" """
@@ -322,20 +333,6 @@ async def process_messages_task():
data = msg["data"] data = msg["data"]
if source == "LoRa": if source == "LoRa":
if "new_cooking_state" in data.get("data", {}):
mw_id = data["data"].get("id")
n_state = data["data"].get("new_cooking_state")
print(f"[LoRa] Microwave {mw_id} state changed to: {n_state}")
microwave_states[mw_id].set_cooking_state(n_state)
update_lcd_microwave_count() # Update the LCD with new microwave count
if n_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT):
beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3)
buzzer.buzzer_siren(beatsNb=beats)
if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
microwave_states[mw_id].set_state(MicrowaveState.DONE)
print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.")
if "action" in data.get("data", {}): if "action" in data.get("data", {}):
if type(data["data"]) != dict: if type(data["data"]) != dict:
print(f"[LoRa] Invalid data format received: {data['data']}") print(f"[LoRa] Invalid data format received: {data['data']}")
@@ -477,15 +474,14 @@ async def on_new_alert(alert: Alert):
response.raise_for_status() response.raise_for_status()
res_json = response.json() res_json = response.json()
print(f"[Alert Handling] Alert sent to cloud: {alert_dict}, Response: {res_json}") log(f"[Alert Handling] Alert sent to cloud: {alert_dict}, Response: {res_json}")
if not alert.redistributed: # If not redistributed from microwaves if not alert.redistributed: # If not redistributed from microwaves
# Send the alert to the lora device # Send the alert to the lora device
payload = payloads.lora_new_alert(alert) payload = payloads.lora_new_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, max_retries=7):
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):
@@ -641,6 +637,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries + 1):
if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD: if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD:
print(f"[{microwave_id}] Dish removed or state changed. Aborting API request.") print(f"[{microwave_id}] Dish removed or state changed. Aborting API request.")
microwave_states[microwave_id].set_state(MicrowaveState.IDLE)
return return
try: try:
@@ -657,6 +654,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
# Abort if state changed while waiting for cloud AI response # Abort if state changed while waiting for cloud AI response
if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD: if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD:
print(f"[{microwave_id}] Dish removed during API request. Discarding API plan.") print(f"[{microwave_id}] Dish removed during API request. Discarding API plan.")
microwave_states[microwave_id].set_state(MicrowaveState.IDLE)
return return
response.raise_for_status() response.raise_for_status()
@@ -672,7 +670,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
microwave_states[microwave_id].set_state(MicrowaveState.DONE) microwave_states[microwave_id].set_state(MicrowaveState.DONE)
if not res_json['is_safe']: # The cooking area is not safe, raise an alert if not res_json['is_safe']: # The cooking area is not safe, raise an alert
alert = Alert(AlertType.COOKING_SAFETY, res_json['warning_message']) alert = Alert(AlertType.COOKING_SAFETY, res_json['warning_message'], redistributed=False)
alert_manager.add_alert(alert) alert_manager.add_alert(alert)
else: else:
alert = Alert(AlertType.ERROR, f"Cloud API error for microwave {microwave_id}: {res_json['error']}") alert = Alert(AlertType.ERROR, f"Cloud API error for microwave {microwave_id}: {res_json['error']}")
@@ -693,7 +691,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W") print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W")
microwave_states[microwave_id].set_state(MicrowaveState.COOKING) microwave_states[microwave_id].set_state(MicrowaveState.COOKING)
if config.DEBUG: if config.DEBUG_SHORTER_COOKING_PLAN:
c_time = 20 # Set to 20s for debug c_time = 20 # Set to 20s for debug
c_temp = 50 # Set to 50°C for debug c_temp = 50 # Set to 50°C for debug
@@ -818,16 +816,16 @@ async def monitor_dish_height_task():
if dist is not None: if dist is not None:
# Hysteresis Thresholds: # Hysteresis Thresholds:
# - Must be > 1.2 cm to detect dish insertion # - Must be > 1.5 cm to detect dish insertion
# - Must be < 0.8 cm to detect dish removal # - Must be < 0.8 cm to detect dish removal
if dist > 1.2: if dist > 2.0:
consecutive_present += 1 consecutive_present += 1
consecutive_absent = 0 consecutive_absent = 0
elif dist < 0.8: elif dist < 1.0:
consecutive_absent += 1 consecutive_absent += 1
consecutive_present = 0 consecutive_present = 0
else: else:
# Dead-zone (1.2cm to 0.8cm) -> Noise buffer # Dead-zone (2.0cm to 1.0cm) -> Noise buffer
consecutive_present = 0 consecutive_present = 0
consecutive_absent = 0 consecutive_absent = 0
+4
View File
@@ -4,9 +4,13 @@ zeroconf>=0.131.0
fastapi>=0.100.0,<1.0.0 fastapi>=0.100.0,<1.0.0
uvicorn>=0.20.0,<1.0.0 uvicorn>=0.20.0,<1.0.0
python-multipart>=0.0.6,<1.0.0 python-multipart>=0.0.6,<1.0.0
requests==2.34.2
numpy>=1.20.0
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2 # picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
# OpenCV # OpenCV
# sudo apt install -y python3-opencv # sudo apt install -y python3-opencv
# sudo apt install -y opencv-data # sudo apt install -y opencv-data
# sudo apt install -y swig python3-dev liblgpio-dev
# ... Replaces this ?
# sudo apt install pigpio python3-pigpio # sudo apt install pigpio python3-pigpio
# sudo systemctl enable pigpiod --now # sudo systemctl enable pigpiod --now
+5 -11
View File
@@ -1,7 +1,7 @@
import grovepi import grovepi
import time import time
import threading import threading
from sensors.lock import grove_lock from sensors.lock import safe_grove_access
from shared.logging import log from shared.logging import log
button = 2 button = 2
@@ -10,9 +10,11 @@ grovepi.pinMode(button, "INPUT")
button_callback = None button_callback = None
def read_button_state(): def read_button_state():
# Increase timeout slightly so the button thread can wait for long I2C sensor reads to finish # Attempt to acquire the lock with a 0.2s timeout
if not grove_lock.acquire(timeout=0.2): with safe_grove_access(timeout=0.2) as acquired:
if not acquired:
return None return None
try: try:
val = grovepi.digitalRead(button) val = grovepi.digitalRead(button)
# Force strict binary output (0 or 1) # Force strict binary output (0 or 1)
@@ -20,18 +22,13 @@ def read_button_state():
except Exception as e: except Exception as e:
log(f"BTN Error: {e}") log(f"BTN Error: {e}")
return None return None
finally:
grove_lock.release()
def monitor_button(): def monitor_button():
last_stable_state = 0 last_stable_state = 0
candidate_state = 0 candidate_state = 0
consecutive_count = 0 consecutive_count = 0
# Require 3 consecutive identical reads (~60ms) to confirm a valid state change
REQUIRED_CONSECUTIVE_READS = 3 REQUIRED_CONSECUTIVE_READS = 3
# Minimum time gap (in seconds) between allowed button triggers (cooldown)
DEBOUNCE_COOLDOWN = 0.3 DEBOUNCE_COOLDOWN = 0.3
last_trigger_time = 0 last_trigger_time = 0
@@ -45,11 +42,9 @@ def monitor_button():
candidate_state = current_state candidate_state = current_state
consecutive_count = 1 consecutive_count = 1
# State is confirmed stable across multiple reads
if consecutive_count >= REQUIRED_CONSECUTIVE_READS: if consecutive_count >= REQUIRED_CONSECUTIVE_READS:
now = time.time() now = time.time()
# Rising edge detection (0 -> 1 transition) with cooldown timer
if candidate_state == 1 and last_stable_state == 0: if candidate_state == 1 and last_stable_state == 0:
if (now - last_trigger_time) > DEBOUNCE_COOLDOWN: if (now - last_trigger_time) > DEBOUNCE_COOLDOWN:
last_trigger_time = now last_trigger_time = now
@@ -63,7 +58,6 @@ def monitor_button():
time.sleep(0.02) # 20ms poll interval time.sleep(0.02) # 20ms poll interval
else: else:
# Lock was busy or read failed; reset candidate counter to reject noisy spikes
consecutive_count = 0 consecutive_count = 0
time.sleep(0.01) time.sleep(0.01)
+42 -21
View File
@@ -1,33 +1,54 @@
import time import time
import threading
import grovepi import grovepi
from sensors.lock import grove_lock from sensors.lock import safe_grove_access
from shared import config from shared import config
# Pin definition BUZZER_PIN = 8 # Must be a PWM pin on GrovePi (D3, D5, D6, D8 support analogWrite)
BUZZER_PIN = 8
# Initialize pin mode safely
try:
with safe_grove_access(timeout=1.0) as acquired:
if acquired:
grovepi.pinMode(BUZZER_PIN, "OUTPUT") grovepi.pinMode(BUZZER_PIN, "OUTPUT")
buzzer_state = 0 except Exception as e:
print(f"[BUZZER] Init error: {e}")
def set_buzzer(state: int): def _stop_sound():
global buzzer_state """Helper to ensure sound is silenced safely under lock."""
buzzer_state = state with safe_grove_access(timeout=1.0) as acquired:
if acquired:
try:
grovepi.analogWrite(BUZZER_PIN, 0)
grovepi.digitalWrite(BUZZER_PIN, 0)
except Exception:
pass
def _siren_worker(beats_nb: int, delay: float):
if not config.BUZZER_ACTIVATED: if not config.BUZZER_ACTIVATED:
return return
try: try:
with grove_lock: for _ in range(beats_nb):
grovepi.digitalWrite(BUZZER_PIN, state) # 1. Turn sound ON (only acquire lock briefly for the I2C write)
with safe_grove_access(timeout=1.0) as acquired:
if acquired:
grovepi.analogWrite(BUZZER_PIN, 128)
time.sleep(delay) # Sleep WITHOUT holding the lock!
# 2. Turn sound OFF
with safe_grove_access(timeout=1.0) as acquired:
if acquired:
grovepi.analogWrite(BUZZER_PIN, 0)
time.sleep(delay) # Sleep WITHOUT holding the lock!
except Exception as e: except Exception as e:
print(f"[BUZZER] Error occurred while setting buzzer state: {e}") print(f"[BUZZER] Error during siren execution: {e}")
finally:
_stop_sound()
def buzzer_toggle(): def buzzer_siren(beats_nb: int = 3, delay: float = 0.2, async_run: bool = True):
global buzzer_state if async_run:
new_state = 1 - buzzer_state thread = threading.Thread(target=_siren_worker, args=(beats_nb, delay), daemon=True)
set_buzzer(new_state) thread.start()
else:
def buzzer_siren(beatsNb: int = 3, delay: float = 0.3): _siren_worker(beats_nb, delay)
for _ in range(beatsNb):
set_buzzer(1)
time.sleep(delay)
set_buzzer(0)
time.sleep(delay)
+17 -4
View File
@@ -1,12 +1,25 @@
import grovepi
import math
from sensors.lock import grove_lock
from picamera2 import Picamera2, Preview from picamera2 import Picamera2, Preview
import time import time
picam2 = Picamera2() picam2 = Picamera2()
camera_config = picam2.create_still_configuration() # Get maximum full-sensor dimensions
sensor_format = picam2.sensor_modes[0]
max_width = sensor_format['size'][0]
max_height = sensor_format['size'][1]
camera_config = picam2.create_still_configuration(
raw={"size": (1640, 1232)},
main={"size": (1640, 1232)}
)
picam2.set_controls({
"AeEnable": True,
"AwbEnable": True,
"ExposureTime": 100000,
"ExposureValue": 0.3,
"Brightness": 0.30,
"Contrast": 1.1
})
picam2.configure(camera_config) picam2.configure(camera_config)
picam2.start() picam2.start()
+7 -3
View File
@@ -2,7 +2,7 @@ import serial
import time import time
import threading import threading
from shared.logging import log from shared.logging import log
from sensors.lock import serial_lock from sensors.lock import safe_serial_access
def calculate_nmea_checksum(line: str) -> bool: def calculate_nmea_checksum(line: str) -> bool:
"""Validates standard NMEA 0183 sentence checksum ($...*HH).""" """Validates standard NMEA 0183 sentence checksum ($...*HH)."""
@@ -35,7 +35,12 @@ class GROVEGPS:
def read(self): def read(self):
"""Reads the freshest GGA sentence from serial, thread-safely.""" """Reads the freshest GGA sentence from serial, thread-safely."""
with serial_lock: # Allow sufficient time for the serial reads (readline can block up to timeout=1s per line)
with safe_serial_access(timeout=2.0) as acquired:
if not acquired:
log("GPS: Serial lock acquisition timed out")
return False
# 1. Flush accumulated stale buffer data # 1. Flush accumulated stale buffer data
self.ser.reset_input_buffer() self.ser.reset_input_buffer()
@@ -115,5 +120,4 @@ def get_gps_data():
else: else:
log(f"GPS: No valid fix or data available. Satellites: {gps.satellites}, Quality: {gps.quality}") log(f"GPS: No valid fix or data available. Satellites: {gps.satellites}, Quality: {gps.quality}")
# Return None so main.py doesn't process or log empty GPS data
return None return None
+26
View File
@@ -1,5 +1,31 @@
import threading import threading
from contextlib import contextmanager
# Dedicated lock for I2C bus access (used by GrovePi sensors) # Dedicated lock for I2C bus access (used by GrovePi sensors)
grove_lock = threading.Lock() grove_lock = threading.Lock()
# Dedicated lock for UART/Serial port access # Dedicated lock for UART/Serial port access
serial_lock = threading.Lock() serial_lock = threading.Lock()
@contextmanager
def safe_grove_access(timeout=1.0):
"""
Safely acquire the Grove I2C lock with a timeout.
Yields True if lock acquired, False otherwise.
Guarantees lock release only when successfully acquired.
"""
acquired = grove_lock.acquire(timeout=timeout)
try:
yield acquired
finally:
if acquired:
grove_lock.release()
@contextmanager
def safe_serial_access(timeout=1.0):
"""Safely acquire the UART/Serial lock with a timeout."""
acquired = serial_lock.acquire(timeout=timeout)
try:
yield acquired
finally:
if acquired:
serial_lock.release()
+10 -4
View File
@@ -1,7 +1,7 @@
import time import time
import pigpio import pigpio
from typing import Optional from typing import Optional
from sensors.lock import serial_lock from sensors.lock import safe_serial_access
class RFIDReader: class RFIDReader:
@@ -19,7 +19,10 @@ class RFIDReader:
if not self.pi.connected: if not self.pi.connected:
raise RuntimeError("pigpio daemon is not running. Run 'sudo systemctl start pigpiod'.") raise RuntimeError("pigpio daemon is not running. Run 'sudo systemctl start pigpiod'.")
with serial_lock: with safe_serial_access(timeout=1.0) as acquired:
if not acquired:
raise RuntimeError("RFID: Could not acquire serial lock during initialization")
self.pi.set_mode(self.rx_pin, pigpio.INPUT) self.pi.set_mode(self.rx_pin, pigpio.INPUT)
# Clean up lingering serial sessions on this pin # Clean up lingering serial sessions on this pin
@@ -34,7 +37,10 @@ class RFIDReader:
""" """
Reads and accumulates bytes, returning the 10-digit Tag ID. Reads and accumulates bytes, returning the 10-digit Tag ID.
""" """
with serial_lock: with safe_serial_access(timeout=0.5) as acquired:
if not acquired:
return None
if not self.pi or not self.pi.connected: if not self.pi or not self.pi.connected:
return None return None
@@ -70,7 +76,7 @@ class RFIDReader:
return None return None
def close(self): def close(self):
with serial_lock: with safe_serial_access(timeout=1.0) as acquired:
if self.pi and self.pi.connected: if self.pi and self.pi.connected:
try: try:
self.pi.bb_serial_read_close(self.rx_pin) self.pi.bb_serial_read_close(self.rx_pin)
+12 -10
View File
@@ -1,6 +1,6 @@
import time import time
import sys import sys
from sensors.lock import grove_lock from sensors.lock import safe_grove_access
if sys.platform == 'uwp': if sys.platform == 'uwp':
import winrt_smbus as smbus import winrt_smbus as smbus
@@ -18,7 +18,6 @@ else:
DISPLAY_RGB_ADDR = 0x62 DISPLAY_RGB_ADDR = 0x62
DISPLAY_TEXT_ADDR = 0x3e DISPLAY_TEXT_ADDR = 0x3e
def setRGB(r, g, b, brightness=1.0): def setRGB(r, g, b, brightness=1.0):
"""Set backlight to (R,G,B) with optional brightness level (0.0 to 1.0).""" """Set backlight to (R,G,B) with optional brightness level (0.0 to 1.0)."""
brightness = max(0.0, min(1.0, float(brightness))) brightness = max(0.0, min(1.0, float(brightness)))
@@ -27,7 +26,9 @@ def setRGB(r, g, b, brightness=1.0):
g_scaled = int(max(0, min(255, g * brightness))) g_scaled = int(max(0, min(255, g * brightness)))
b_scaled = int(max(0, min(255, b * brightness))) b_scaled = int(max(0, min(255, b * brightness)))
with grove_lock: with safe_grove_access(timeout=1.0) as acquired:
if not acquired:
return
try: try:
bus.write_byte_data(DISPLAY_RGB_ADDR, 0, 0) bus.write_byte_data(DISPLAY_RGB_ADDR, 0, 0)
bus.write_byte_data(DISPLAY_RGB_ADDR, 1, 0) bus.write_byte_data(DISPLAY_RGB_ADDR, 1, 0)
@@ -38,15 +39,15 @@ def setRGB(r, g, b, brightness=1.0):
except OSError: except OSError:
pass pass
def textCommand(cmd): def textCommand(cmd):
"""Send command to display (internal use).""" """Send command to display (internal use)."""
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd) bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd)
def setText(text): def setText(text):
"""Set display text (\n for second line or auto wrap).""" """Set display text (\n for second line or auto wrap)."""
with grove_lock: with safe_grove_access(timeout=1.5) as acquired:
if not acquired:
return
try: try:
textCommand(0x01) # Clear display textCommand(0x01) # Clear display
time.sleep(0.05) time.sleep(0.05)
@@ -67,14 +68,15 @@ def setText(text):
continue continue
count += 1 count += 1
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c)) bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c))
time.sleep(0.001) # Small pacing delay to prevent LCD buffer overflow time.sleep(0.001)
except OSError: except OSError:
pass pass
def setText_norefresh(text): def setText_norefresh(text):
"""Update display text without full screen erase.""" """Update display text without full screen erase."""
with grove_lock: with safe_grove_access(timeout=1.5) as acquired:
if not acquired:
return
try: try:
textCommand(0x02) # Return home textCommand(0x02) # Return home
time.sleep(0.05) time.sleep(0.05)
@@ -97,6 +99,6 @@ def setText_norefresh(text):
continue continue
count += 1 count += 1
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c)) bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c))
time.sleep(0.001) # Small pacing delay to prevent LCD buffer overflow time.sleep(0.001)
except OSError: except OSError:
pass pass
+5 -14
View File
@@ -1,33 +1,24 @@
import grovepi import grovepi
from sensors.lock import grove_lock from sensors.lock import safe_grove_access
from shared import config from shared import config
# Connect the Grove Ultrasonic Ranger to digital port D4
# SIG,NC,VCC,GND
ULTRASONIC_RANGER_PORT = 4 ULTRASONIC_RANGER_PORT = 4
def read_ultrasonic_ranger(ultrasonic_ranger=ULTRASONIC_RANGER_PORT): def read_ultrasonic_ranger(ultrasonic_ranger=ULTRASONIC_RANGER_PORT):
if not grove_lock.acquire(timeout=1.0): with safe_grove_access(timeout=1.5) as acquired:
print("Ultrasonic: Lock acquisition timed out") if not acquired:
return None return None
try: try:
return grovepi.ultrasonicRead(ultrasonic_ranger) return grovepi.ultrasonicRead(ultrasonic_ranger)
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Ultrasonic read error: {e}")
return None return None
finally:
grove_lock.release()
def get_dish_height(): def get_dish_height():
"""Returns the height of the dish in centimeters.""" """Returns the height of the dish in centimeters."""
distance = read_ultrasonic_ranger() distance = read_ultrasonic_ranger()
if distance is not None: if distance is not None:
# Assuming the ultrasonic sensor is mounted at a fixed height above the dish
# and pointing downwards, we can calculate the height of the dish.
# For example, if the sensor is 30 cm above the dish when it's empty:
# cm
dish_height = config.COOKING_COMPARTMENT_HEIGHT - distance dish_height = config.COOKING_COMPARTMENT_HEIGHT - distance
return max(dish_height, 0) # Ensure height is not negative return max(dish_height, 0)
else:
return None return None
+1
View File
@@ -9,6 +9,7 @@ User=pi
Group=pi Group=pi
WorkingDirectory=/home/pi/SmartWave WorkingDirectory=/home/pi/SmartWave
EnvironmentFile=-/etc/default/smartwave EnvironmentFile=-/etc/default/smartwave
Environment="PYTHONUNBUFFERED=1"
ExecStart=/bin/sh /home/pi/SmartWave/orchestrateur/launch.sh /home/pi/SmartWave/orchestrateur/main.py ExecStart=/bin/sh /home/pi/SmartWave/orchestrateur/launch.sh /home/pi/SmartWave/orchestrateur/main.py
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
+3
View File
@@ -1,7 +1,10 @@
# Debugging and others # Debugging and others
DEBUG=True DEBUG=True
DEBUG_MESSAGES=True
DEBUG_DANGEROUS_AREA=False DEBUG_DANGEROUS_AREA=False
DEBUG_TEMPERATURE_ALERT=False DEBUG_TEMPERATURE_ALERT=False
DEBUG_SHORTER_COOKING_PLAN=True
DEBUG_DISHANALYZER=True
# Technitian Web Server # Technitian Web Server
TECHNICIAN_WEB_SERVER_HOST = "192.168.50.1" TECHNICIAN_WEB_SERVER_HOST = "192.168.50.1"
+2 -2
View File
@@ -1,6 +1,6 @@
from shared.config import DEBUG from shared.config import DEBUG_MESSAGES
def log(message): def log(message):
"""Log a message to the console if DEBUG is enabled.""" """Log a message to the console if DEBUG is enabled."""
if DEBUG: if DEBUG_MESSAGES:
print(f"\n{message}") print(f"\n{message}")
+1 -1
View File
@@ -81,7 +81,7 @@ class BaseLoraDevice:
return packet return packet
def send_reliable(self, payload, max_retries=4, ack_timeout=3.0): def send_reliable(self, payload, max_retries=5, ack_timeout=3.0):
"""Sends a payload and listens in a single continuous RX window for the ACK.""" """Sends a payload and listens in a single continuous RX window for the ACK."""
with self.tx_lock: with self.tx_lock: