Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3753f57041 | |||
| 688fd26db8 | |||
| 4d7706f8e1 | |||
| efbc147f48 | |||
| 4085305039 |
+1
-1
@@ -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))
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
sys.path.insert(0, '..')
|
||||||
|
try:
|
||||||
|
from shared import config
|
||||||
|
except ImportError:
|
||||||
|
from ..shared import config
|
||||||
|
|
||||||
|
|
||||||
class MicrowaveDishAnalyzer:
|
class MicrowaveDishAnalyzer:
|
||||||
@@ -42,6 +47,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)
|
||||||
@@ -64,13 +80,13 @@ class MicrowaveDishAnalyzer:
|
|||||||
return 0.85 # Default factor for plated meals
|
return 0.85 # Default factor for plated meals
|
||||||
|
|
||||||
def estimate_volume(
|
def estimate_volume(
|
||||||
self, image_path: str, height_cm: float, food_label: str = ""
|
self, image_path: str, height_cm: float, food_label: str = "", config: Optional[Any] = None
|
||||||
) -> Dict[str, float]:
|
) -> Dict[str, float]:
|
||||||
"""
|
"""
|
||||||
Computes total physical volume in cm³ (mL).
|
Computes total physical volume in cm³ (mL).
|
||||||
Volume = Area (cm²) * Height (cm) * Shape Factor
|
Volume = Area (cm²) * Height (cm) * Shape Factor
|
||||||
"""
|
"""
|
||||||
area_cm2 = self.calculate_surface_area_cm2(image_path)
|
area_cm2 = self.calculate_surface_area_cm2(image_path, config=config)
|
||||||
k_shape = self._get_shape_factor(food_label)
|
k_shape = self._get_shape_factor(food_label)
|
||||||
volume_cm3 = area_cm2 * height_cm * k_shape
|
volume_cm3 = area_cm2 * height_cm * k_shape
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
if acquired:
|
||||||
grovepi.pinMode(BUZZER_PIN, "OUTPUT")
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user