pi: GPIO-controlled theme switch
- 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)
This commit is contained in:
@@ -2,16 +2,25 @@
|
||||
|
||||
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
|
||||
_GPIO_AVAILABLE = True
|
||||
_BACKEND = "rpigpio"
|
||||
print("[GPIO] Using RPi.GPIO backend")
|
||||
except ImportError:
|
||||
_GPIO_AVAILABLE = False
|
||||
print("[GPIO] RPi.GPIO not available - running in mock mode")
|
||||
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
|
||||
@@ -24,16 +33,37 @@ class GPIOService:
|
||||
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._theme_switch_pending = None # None = no change, bool = new value
|
||||
self._pending_state = None # Candidate new state
|
||||
self._pending_count = 0 # Consecutive readings of pending state
|
||||
|
||||
if _GPIO_AVAILABLE:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
# Input with software pull-down (belt + suspenders with hardware pulldown)
|
||||
GPIO.setup(PIN_THEME_SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
||||
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."""
|
||||
@@ -42,8 +72,8 @@ class GPIOService:
|
||||
self._running = True
|
||||
|
||||
# Read initial state
|
||||
if _GPIO_AVAILABLE:
|
||||
self._theme_switch_state = GPIO.input(PIN_THEME_SWITCH) == GPIO.HIGH
|
||||
if self._gpio_working:
|
||||
self._theme_switch_state = self._read_pin()
|
||||
else:
|
||||
self._theme_switch_state = True # Mock: default dark
|
||||
|
||||
@@ -56,35 +86,54 @@ class GPIOService:
|
||||
if self._greenlet:
|
||||
self._greenlet.kill()
|
||||
self._greenlet = None
|
||||
if _GPIO_AVAILABLE:
|
||||
GPIO.cleanup([PIN_THEME_SWITCH])
|
||||
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, detect edges."""
|
||||
"""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 _GPIO_AVAILABLE:
|
||||
current = GPIO.input(PIN_THEME_SWITCH) == GPIO.HIGH
|
||||
else:
|
||||
current = self._theme_switch_state # Mock: no change
|
||||
if self._gpio_working:
|
||||
current = self._read_pin()
|
||||
|
||||
if current != self._theme_switch_state:
|
||||
self._theme_switch_state = current
|
||||
self._theme_switch_pending = current
|
||||
print(f"[GPIO] Theme switch changed to {current}")
|
||||
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
|
||||
|
||||
def get_theme_switch_change(self):
|
||||
"""Get pending theme switch change, if any.
|
||||
# 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
|
||||
|
||||
Returns:
|
||||
bool or None: New state if changed since last call, None otherwise.
|
||||
"""
|
||||
if self._theme_switch_pending is not None:
|
||||
value = self._theme_switch_pending
|
||||
self._theme_switch_pending = None
|
||||
return value
|
||||
return None
|
||||
# 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):
|
||||
|
||||
Reference in New Issue
Block a user