Compare commits

...

9 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
18 changed files with 186 additions and 108 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
) )
+2 -2
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
@@ -664,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)
+17 -1
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
+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 [ ! -d "$VENV_DIR" ]; then
echo "Création de l'environnement virtuel Python..."
"$PYTHON_BIN" -m venv "$VENV_DIR"
fi
# Utiliser le pip/python du venv
VENV_PYTHON="$VENV_DIR/bin/python"
VENV_PIP="$VENV_DIR/bin/pip"
if [ -f "$REQUIREMENTS_FILE" ]; then if [ -f "$REQUIREMENTS_FILE" ]; then
echo "Vérification des dépendances Python..." echo "Vérification et installation des dépendances dans le venv..."
if [ "$(id -u)" -eq 0 ]; then "$VENV_PIP" install --upgrade pip >/dev/null 2>&1 || true
"$PYTHON_BIN" -m pip install -r "$REQUIREMENTS_FILE" "$VENV_PIP" install -r "$REQUIREMENTS_FILE"
else
"$PYTHON_BIN" -m pip install --user -r "$REQUIREMENTS_FILE"
fi
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"
+4 -2
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."""
@@ -478,7 +480,7 @@ async def on_new_alert(alert: Alert):
# 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)
# --- HARDWARE CONTROLLERS --- # --- HARDWARE CONTROLLERS ---
+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
+13 -19
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,28 +10,25 @@ 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:
return None if not acquired:
try: return None
val = grovepi.digitalRead(button)
# Force strict binary output (0 or 1) try:
return 1 if val == 1 else 0 val = grovepi.digitalRead(button)
except Exception as e: # Force strict binary output (0 or 1)
log(f"BTN Error: {e}") return 1 if val == 1 else 0
return None except Exception as e:
finally: log(f"BTN Error: {e}")
grove_lock.release() return None
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)
+31 -22
View File
@@ -1,42 +1,51 @@
import time import time
import threading 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
BUZZER_PIN = 8 # Must be a PWM pin on GrovePi (D3, D5, D6, D8 support analogWrite) BUZZER_PIN = 8 # Must be a PWM pin on GrovePi (D3, D5, D6, D8 support analogWrite)
# Initialize pin mode # Initialize pin mode safely
try: try:
with grove_lock: with safe_grove_access(timeout=1.0) as acquired:
grovepi.pinMode(BUZZER_PIN, "OUTPUT") if acquired:
grovepi.pinMode(BUZZER_PIN, "OUTPUT")
except Exception as e: except Exception as e:
print(f"[BUZZER] Init error: {e}") print(f"[BUZZER] Init error: {e}")
def _siren_worker(beats_nb: int, delay: float): def _stop_sound():
if not config.BUZZER_ACTIVATED: """Helper to ensure sound is silenced safely under lock."""
return with safe_grove_access(timeout=1.0) as acquired:
if acquired:
with grove_lock:
try:
for _ in range(beats_nb):
# 1. Send single PWM write to start sound (Duty cycle ~128/255)
grovepi.analogWrite(BUZZER_PIN, 128)
time.sleep(delay)
# 2. Send single PWM write to kill sound
grovepi.analogWrite(BUZZER_PIN, 0)
time.sleep(delay)
except Exception as e:
print(f"[BUZZER] Error during siren execution: {e}")
finally:
# Absolute safety reset
try: try:
grovepi.analogWrite(BUZZER_PIN, 0) grovepi.analogWrite(BUZZER_PIN, 0)
grovepi.digitalWrite(BUZZER_PIN, 0) grovepi.digitalWrite(BUZZER_PIN, 0)
except Exception: except Exception:
pass pass
def _siren_worker(beats_nb: int, delay: float):
if not config.BUZZER_ACTIVATED:
return
try:
for _ in range(beats_nb):
# 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:
print(f"[BUZZER] Error during siren execution: {e}")
finally:
_stop_sound()
def buzzer_siren(beats_nb: int = 3, delay: float = 0.2, async_run: bool = True): def buzzer_siren(beats_nb: int = 3, delay: float = 0.2, async_run: bool = True):
if async_run: if async_run:
thread = threading.Thread(target=_siren_worker, args=(beats_nb, delay), daemon=True) thread = threading.Thread(target=_siren_worker, args=(beats_nb, delay), daemon=True)
+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
+27 -1
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
+13 -22
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
+1
View File
@@ -4,6 +4,7 @@ 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_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"
+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: