- apt dependencies (RPI.GPIO somehow needs to be installed from apt to work & re-establish uv venv with --system-site-packages - GPIO 20 triggers mode switching (can link to a photodiode or just switch)
142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
"""GPIO service for Pi Zero - edge-triggered monitoring.
|
|
|
|
Polls GPIO pins and exposes state changes for inclusion in other payloads.
|
|
Keeps UART separate (handled by arduino_service).
|
|
|
|
Tries RPi.GPIO first (apt install python3-rpi.gpio), falls back to gpiozero.
|
|
"""
|
|
|
|
import gevent
|
|
|
|
# Try RPi.GPIO first (commonly installed via apt on Pi)
|
|
_BACKEND = None
|
|
try:
|
|
import RPi.GPIO as GPIO
|
|
_BACKEND = "rpigpio"
|
|
print("[GPIO] Using RPi.GPIO backend")
|
|
except ImportError:
|
|
try:
|
|
from gpiozero import Button
|
|
_BACKEND = "gpiozero"
|
|
print("[GPIO] Using gpiozero backend")
|
|
except ImportError:
|
|
print("[GPIO] No GPIO library available - running in mock mode")
|
|
|
|
|
|
# Pin assignments
|
|
PIN_THEME_SWITCH = 20 # Physical switch for light/dark theme
|
|
|
|
|
|
class GPIOService:
|
|
"""Monitors GPIO pins and tracks state changes."""
|
|
|
|
def __init__(self):
|
|
self._running = False
|
|
self._greenlet = None
|
|
self._theme_button = None # gpiozero only
|
|
self._gpio_working = False
|
|
|
|
# Theme switch state
|
|
self._theme_switch_state = False # False = light, True = dark
|
|
self._pending_state = None # Candidate new state
|
|
self._pending_count = 0 # Consecutive readings of pending state
|
|
|
|
if _BACKEND == "rpigpio":
|
|
try:
|
|
GPIO.setmode(GPIO.BCM)
|
|
GPIO.setwarnings(False)
|
|
# No software pull - using external hardware pull-down
|
|
GPIO.setup(PIN_THEME_SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_OFF)
|
|
self._gpio_working = True
|
|
except Exception as e:
|
|
print(f"[GPIO] RPi.GPIO init failed: {e}")
|
|
elif _BACKEND == "gpiozero":
|
|
try:
|
|
self._theme_button = Button(PIN_THEME_SWITCH, pull_up=False)
|
|
self._gpio_working = True
|
|
except Exception as e:
|
|
print(f"[GPIO] gpiozero init failed: {e}")
|
|
|
|
def _read_pin(self):
|
|
"""Read current pin state. Returns True if HIGH (dark mode)."""
|
|
if _BACKEND == "rpigpio":
|
|
return GPIO.input(PIN_THEME_SWITCH) == GPIO.HIGH
|
|
elif _BACKEND == "gpiozero" and self._theme_button:
|
|
return self._theme_button.is_pressed
|
|
return self._theme_switch_state # Mock: return current
|
|
|
|
def start(self):
|
|
"""Start background polling."""
|
|
if self._running:
|
|
return
|
|
self._running = True
|
|
|
|
# Read initial state
|
|
if self._gpio_working:
|
|
self._theme_switch_state = self._read_pin()
|
|
else:
|
|
self._theme_switch_state = True # Mock: default dark
|
|
|
|
self._greenlet = gevent.spawn(self._poll_loop)
|
|
print(f"[GPIO] Started, theme_switch initial={self._theme_switch_state}")
|
|
|
|
def stop(self):
|
|
"""Stop background polling."""
|
|
self._running = False
|
|
if self._greenlet:
|
|
self._greenlet.kill()
|
|
self._greenlet = None
|
|
if _BACKEND == "rpigpio":
|
|
try:
|
|
GPIO.cleanup([PIN_THEME_SWITCH])
|
|
except Exception:
|
|
pass
|
|
elif self._theme_button:
|
|
self._theme_button.close()
|
|
self._theme_button = None
|
|
|
|
def _poll_loop(self):
|
|
"""Poll GPIO at ~20Hz, update state with consecutive-read debounce."""
|
|
poll_count = 0
|
|
# Require N consecutive same readings to accept state change
|
|
# At 20Hz: 10 readings = 500ms, 20 readings = 1s
|
|
required_consecutive = 11 # ~550ms of stable signal
|
|
|
|
while self._running:
|
|
gevent.sleep(0.05) # 20Hz
|
|
poll_count += 1
|
|
|
|
if self._gpio_working:
|
|
current = self._read_pin()
|
|
|
|
if current != self._theme_switch_state:
|
|
# Different from accepted state - count towards change
|
|
if current == self._pending_state:
|
|
self._pending_count += 1
|
|
else:
|
|
# New candidate state
|
|
self._pending_state = current
|
|
self._pending_count = 1
|
|
|
|
# Accept change after enough consecutive readings
|
|
if self._pending_count >= required_consecutive:
|
|
self._theme_switch_state = current
|
|
self._pending_state = None
|
|
self._pending_count = 0
|
|
print(f"[GPIO] Theme switch: {current} (dark={current})")
|
|
else:
|
|
# Matches current state - reset any pending change
|
|
self._pending_state = None
|
|
self._pending_count = 0
|
|
|
|
# Heartbeat log every ~5 seconds (100 polls at 20Hz)
|
|
if poll_count >= 100:
|
|
poll_count = 0
|
|
raw = 1 if self._theme_switch_state else 0
|
|
print(f"[GPIO] Pin {PIN_THEME_SWITCH}: {raw} (dark={self._theme_switch_state})")
|
|
|
|
@property
|
|
def theme_switch(self):
|
|
"""Current theme switch state (True = dark, False = light)."""
|
|
return self._theme_switch_state
|