Small things because fuck sd cards
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import grovepi
|
||||
import time
|
||||
import threading
|
||||
from sensors.lock import grove_lock
|
||||
from sensors.lock import safe_grove_access
|
||||
from shared.logging import log
|
||||
|
||||
button = 2
|
||||
@@ -10,28 +10,25 @@ grovepi.pinMode(button, "INPUT")
|
||||
button_callback = None
|
||||
|
||||
def read_button_state():
|
||||
# Increase timeout slightly so the button thread can wait for long I2C sensor reads to finish
|
||||
if not grove_lock.acquire(timeout=0.2):
|
||||
return None
|
||||
try:
|
||||
val = grovepi.digitalRead(button)
|
||||
# Force strict binary output (0 or 1)
|
||||
return 1 if val == 1 else 0
|
||||
except Exception as e:
|
||||
log(f"BTN Error: {e}")
|
||||
return None
|
||||
finally:
|
||||
grove_lock.release()
|
||||
# Attempt to acquire the lock with a 0.2s timeout
|
||||
with safe_grove_access(timeout=0.2) as acquired:
|
||||
if not acquired:
|
||||
return None
|
||||
|
||||
try:
|
||||
val = grovepi.digitalRead(button)
|
||||
# Force strict binary output (0 or 1)
|
||||
return 1 if val == 1 else 0
|
||||
except Exception as e:
|
||||
log(f"BTN Error: {e}")
|
||||
return None
|
||||
|
||||
def monitor_button():
|
||||
last_stable_state = 0
|
||||
candidate_state = 0
|
||||
consecutive_count = 0
|
||||
|
||||
# Require 3 consecutive identical reads (~60ms) to confirm a valid state change
|
||||
REQUIRED_CONSECUTIVE_READS = 3
|
||||
|
||||
# Minimum time gap (in seconds) between allowed button triggers (cooldown)
|
||||
DEBOUNCE_COOLDOWN = 0.3
|
||||
last_trigger_time = 0
|
||||
|
||||
@@ -45,11 +42,9 @@ def monitor_button():
|
||||
candidate_state = current_state
|
||||
consecutive_count = 1
|
||||
|
||||
# State is confirmed stable across multiple reads
|
||||
if consecutive_count >= REQUIRED_CONSECUTIVE_READS:
|
||||
now = time.time()
|
||||
|
||||
# Rising edge detection (0 -> 1 transition) with cooldown timer
|
||||
if candidate_state == 1 and last_stable_state == 0:
|
||||
if (now - last_trigger_time) > DEBOUNCE_COOLDOWN:
|
||||
last_trigger_time = now
|
||||
@@ -63,7 +58,6 @@ def monitor_button():
|
||||
|
||||
time.sleep(0.02) # 20ms poll interval
|
||||
else:
|
||||
# Lock was busy or read failed; reset candidate counter to reject noisy spikes
|
||||
consecutive_count = 0
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
@@ -1,42 +1,51 @@
|
||||
import time
|
||||
import threading
|
||||
import grovepi
|
||||
from sensors.lock import grove_lock
|
||||
from sensors.lock import safe_grove_access
|
||||
from shared import config
|
||||
|
||||
BUZZER_PIN = 8 # Must be a PWM pin on GrovePi (D3, D5, D6, D8 support analogWrite)
|
||||
|
||||
# Initialize pin mode
|
||||
# Initialize pin mode safely
|
||||
try:
|
||||
with grove_lock:
|
||||
grovepi.pinMode(BUZZER_PIN, "OUTPUT")
|
||||
with safe_grove_access(timeout=1.0) as acquired:
|
||||
if acquired:
|
||||
grovepi.pinMode(BUZZER_PIN, "OUTPUT")
|
||||
except Exception as e:
|
||||
print(f"[BUZZER] Init error: {e}")
|
||||
|
||||
def _siren_worker(beats_nb: int, delay: float):
|
||||
if not config.BUZZER_ACTIVATED:
|
||||
return
|
||||
|
||||
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
|
||||
def _stop_sound():
|
||||
"""Helper to ensure sound is silenced safely under lock."""
|
||||
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:
|
||||
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):
|
||||
if async_run:
|
||||
thread = threading.Thread(target=_siren_worker, args=(beats_nb, delay), daemon=True)
|
||||
|
||||
@@ -2,7 +2,7 @@ import serial
|
||||
import time
|
||||
import threading
|
||||
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:
|
||||
"""Validates standard NMEA 0183 sentence checksum ($...*HH)."""
|
||||
@@ -35,7 +35,12 @@ class GROVEGPS:
|
||||
|
||||
def read(self):
|
||||
"""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
|
||||
self.ser.reset_input_buffer()
|
||||
|
||||
@@ -115,5 +120,4 @@ def get_gps_data():
|
||||
else:
|
||||
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
|
||||
@@ -1,5 +1,31 @@
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
# Dedicated lock for I2C bus access (used by GrovePi sensors)
|
||||
grove_lock = threading.Lock()
|
||||
# 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 pigpio
|
||||
from typing import Optional
|
||||
from sensors.lock import serial_lock
|
||||
from sensors.lock import safe_serial_access
|
||||
|
||||
|
||||
class RFIDReader:
|
||||
@@ -19,7 +19,10 @@ class RFIDReader:
|
||||
if not self.pi.connected:
|
||||
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)
|
||||
|
||||
# Clean up lingering serial sessions on this pin
|
||||
@@ -34,7 +37,10 @@ class RFIDReader:
|
||||
"""
|
||||
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:
|
||||
return None
|
||||
|
||||
@@ -70,7 +76,7 @@ class RFIDReader:
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
with serial_lock:
|
||||
with safe_serial_access(timeout=1.0) as acquired:
|
||||
if self.pi and self.pi.connected:
|
||||
try:
|
||||
self.pi.bb_serial_read_close(self.rx_pin)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
import sys
|
||||
from sensors.lock import grove_lock
|
||||
from sensors.lock import safe_grove_access
|
||||
|
||||
if sys.platform == 'uwp':
|
||||
import winrt_smbus as smbus
|
||||
@@ -18,7 +18,6 @@ else:
|
||||
DISPLAY_RGB_ADDR = 0x62
|
||||
DISPLAY_TEXT_ADDR = 0x3e
|
||||
|
||||
|
||||
def setRGB(r, g, b, brightness=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)))
|
||||
@@ -27,7 +26,9 @@ def setRGB(r, g, b, brightness=1.0):
|
||||
g_scaled = int(max(0, min(255, g * 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:
|
||||
bus.write_byte_data(DISPLAY_RGB_ADDR, 0, 0)
|
||||
bus.write_byte_data(DISPLAY_RGB_ADDR, 1, 0)
|
||||
@@ -38,15 +39,15 @@ def setRGB(r, g, b, brightness=1.0):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def textCommand(cmd):
|
||||
"""Send command to display (internal use)."""
|
||||
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd)
|
||||
|
||||
|
||||
def setText(text):
|
||||
"""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:
|
||||
textCommand(0x01) # Clear display
|
||||
time.sleep(0.05)
|
||||
@@ -67,14 +68,15 @@ def setText(text):
|
||||
continue
|
||||
count += 1
|
||||
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:
|
||||
pass
|
||||
|
||||
|
||||
def setText_norefresh(text):
|
||||
"""Update display text without full screen erase."""
|
||||
with grove_lock:
|
||||
with safe_grove_access(timeout=1.5) as acquired:
|
||||
if not acquired:
|
||||
return
|
||||
try:
|
||||
textCommand(0x02) # Return home
|
||||
time.sleep(0.05)
|
||||
@@ -97,6 +99,6 @@ def setText_norefresh(text):
|
||||
continue
|
||||
count += 1
|
||||
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:
|
||||
pass
|
||||
@@ -1,33 +1,24 @@
|
||||
import grovepi
|
||||
from sensors.lock import grove_lock
|
||||
from sensors.lock import safe_grove_access
|
||||
from shared import config
|
||||
|
||||
# Connect the Grove Ultrasonic Ranger to digital port D4
|
||||
# SIG,NC,VCC,GND
|
||||
ULTRASONIC_RANGER_PORT = 4
|
||||
|
||||
def read_ultrasonic_ranger(ultrasonic_ranger=ULTRASONIC_RANGER_PORT):
|
||||
if not grove_lock.acquire(timeout=1.0):
|
||||
print("Ultrasonic: Lock acquisition timed out")
|
||||
return None
|
||||
|
||||
try:
|
||||
return grovepi.ultrasonicRead(ultrasonic_ranger)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
finally:
|
||||
grove_lock.release()
|
||||
|
||||
with safe_grove_access(timeout=1.5) as acquired:
|
||||
if not acquired:
|
||||
return None
|
||||
|
||||
try:
|
||||
return grovepi.ultrasonicRead(ultrasonic_ranger)
|
||||
except Exception as e:
|
||||
print(f"Ultrasonic read error: {e}")
|
||||
return None
|
||||
|
||||
def get_dish_height():
|
||||
"""Returns the height of the dish in centimeters."""
|
||||
distance = read_ultrasonic_ranger()
|
||||
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
|
||||
return max(dish_height, 0) # Ensure height is not negative
|
||||
else:
|
||||
return None
|
||||
return max(dish_height, 0)
|
||||
return None
|
||||
Reference in New Issue
Block a user