From 2d86a1b9dabde744b247211a8bbb5f9c3a3a29b6 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Wed, 25 Mar 2026 21:04:30 -0400 Subject: [PATCH] replace gpiozero with direct lgpio calls --- Software/web-server/requirements.txt | 1 - .../web-server/strobe_calibration_manager.py | 110 +++++++++++++++--- packaging/src/lib/pitrac-common-functions.sh | 11 +- 3 files changed, 92 insertions(+), 30 deletions(-) diff --git a/Software/web-server/requirements.txt b/Software/web-server/requirements.txt index cf32276e..a365dfbf 100644 --- a/Software/web-server/requirements.txt +++ b/Software/web-server/requirements.txt @@ -9,4 +9,3 @@ pyyaml==6.0.3 aiofiles==25.1.0 websockets==15.0.1 spidev>=3.6 -gpiozero>=2.0 diff --git a/Software/web-server/strobe_calibration_manager.py b/Software/web-server/strobe_calibration_manager.py index 02411804..43d16b83 100644 --- a/Software/web-server/strobe_calibration_manager.py +++ b/Software/web-server/strobe_calibration_manager.py @@ -5,10 +5,12 @@ """ import asyncio +import ctypes import gc import logging import os import time +from ctypes.util import find_library from typing import Any, Dict, Optional logger = logging.getLogger(__name__) @@ -18,11 +20,6 @@ except ImportError: spidev = None -try: - from gpiozero import DigitalOutputDevice -except ImportError: - DigitalOutputDevice = None - class StrobeCalibrationManager: """Manages strobe LED calibration via SPI hardware on the Connector Board""" @@ -33,6 +30,9 @@ class StrobeCalibrationManager: SPI_ADC_DEVICE = 1 SPI_MAX_SPEED_HZ = 1_000_000 + # Pi 5 uses gpiochip4; keep gpiochip0 as a fallback for older boards/installations + GPIO_CHIP_CANDIDATES = (4, 0) + # DIAG pin gates the strobe LED (BCM numbering) DIAG_GPIO_PIN = 10 @@ -70,7 +70,8 @@ def __init__(self, config_manager): self._spi_dac = None self._spi_adc = None - self._diag_pin = None + self._lgpio = None + self._gpiochip = None self._cancel_requested = False @@ -84,11 +85,73 @@ def __init__(self, config_manager): # Hardware lifecycle # ------------------------------------------------------------------ + def _load_lgpio(self): + """Load liblgpio and declare the C function signatures we use.""" + if self._lgpio is not None: + return self._lgpio + + # Use the C library directly so we can match the C++ strobe path + library_name = find_library("lgpio") or "liblgpio.so.1" + try: + lgpio = ctypes.CDLL(library_name) + except OSError as exc: + raise RuntimeError("liblgpio not available -- not running on a Raspberry Pi?") from exc + + # Declare the liblgpio call signatures we use + lgpio.lgGpiochipOpen.argtypes = [ctypes.c_int] + lgpio.lgGpiochipOpen.restype = ctypes.c_int + lgpio.lgGpiochipClose.argtypes = [ctypes.c_int] + lgpio.lgGpiochipClose.restype = ctypes.c_int + lgpio.lgGpioClaimOutput.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ] + lgpio.lgGpioClaimOutput.restype = ctypes.c_int + lgpio.lgGpioWrite.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] + lgpio.lgGpioWrite.restype = ctypes.c_int + + self._lgpio = lgpio + return lgpio + + def _open_diag_gpio(self): + """Open and claim the DIAG GPIO line via liblgpio.""" + lgpio = self._load_lgpio() + self._lgpio = lgpio + + errors = [] + # Try the Pi 5 gpiochip first, then fall back to the legacy numbering + for chip in self.GPIO_CHIP_CANDIDATES: + handle = lgpio.lgGpiochipOpen(chip) + if handle < 0: + errors.append(f"gpiochip{chip} open failed ({handle})") + continue + + result = lgpio.lgGpioClaimOutput(handle, 0, self.DIAG_GPIO_PIN, 0) + if result == 0: + self._gpiochip = handle + return + + errors.append(f"gpiochip{chip} claim failed ({result})") + close_result = lgpio.lgGpiochipClose(handle) + if close_result < 0: + logger.debug("Error closing gpiochip%s after claim failure (%s)", chip, close_result) + + raise RuntimeError(f"Failed to open DIAG GPIO via lgpio: {'; '.join(errors)}") + + def _set_diag(self, output: bool): + """Drive the DIAG GPIO line high or low.""" + if self._lgpio is None or self._gpiochip is None: + raise RuntimeError("DIAG GPIO not open") + + result = self._lgpio.lgGpioWrite(self._gpiochip, self.DIAG_GPIO_PIN, output) + if result < 0: + raise RuntimeError(f"lgGpioWrite failed ({result})") + def _open_hardware(self): if spidev is None: raise RuntimeError("spidev library not available -- not running on a Raspberry Pi?") - if DigitalOutputDevice is None: - raise RuntimeError("gpiozero library not available -- not running on a Raspberry Pi?") self._spi_dac = spidev.SpiDev() self._spi_dac.open(self.SPI_BUS, self.SPI_DAC_DEVICE) @@ -100,23 +163,33 @@ def _open_hardware(self): self._spi_adc.max_speed_hz = self.SPI_MAX_SPEED_HZ self._spi_adc.mode = 0 - self._diag_pin = DigitalOutputDevice(self.DIAG_GPIO_PIN) + self._open_diag_gpio() def _close_hardware(self): - for name, resource in [("diag", self._diag_pin), - ("dac", self._spi_dac), - ("adc", self._spi_adc)]: + if self._gpiochip is not None and self._lgpio is not None: + try: + self._set_diag(False) + time.sleep(0.1) + except Exception: + logger.debug("Error turning off diag", exc_info=True) + + try: + result = self._lgpio.lgGpiochipClose(self._gpiochip) + if result < 0: + logger.debug("Error closing diag gpiochip handle (%s)", result) + except Exception: + logger.debug("Error closing diag", exc_info=True) + + for name, resource in [("dac", self._spi_dac), ("adc", self._spi_adc)]: if resource is None: continue try: - if name == "diag": - resource.off() - time.sleep(0.1) resource.close() except Exception: logger.debug(f"Error closing {name}", exc_info=True) - self._diag_pin = None + self._gpiochip = None + self._lgpio = None self._spi_dac = None self._spi_adc = None @@ -148,7 +221,6 @@ def get_led_current(self) -> float: """ msg = [0x01, self.ADC_CH0_CMD, 0x00] spi = self._spi_adc - diag = self._diag_pin gc.disable() try: @@ -161,10 +233,10 @@ def get_led_current(self) -> float: time.sleep(0) try: - diag.on() + self._set_diag(True) response = spi.xfer2(msg) finally: - diag.off() + self._set_diag(False) try: os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0)) except (PermissionError, AttributeError, OSError): diff --git a/packaging/src/lib/pitrac-common-functions.sh b/packaging/src/lib/pitrac-common-functions.sh index 2cc130d0..b3f4f986 100755 --- a/packaging/src/lib/pitrac-common-functions.sh +++ b/packaging/src/lib/pitrac-common-functions.sh @@ -440,15 +440,6 @@ install_python_dependencies() { log_info "Installing Python dependencies for web server..." - # gpiozero (in requirements.txt) needs these system GPIO backends on Raspberry Pi OS. - # They're not available on PyPI so we install them via apt. - for pkg in python3-lgpio python3-rpi-lgpio; do - if ! dpkg -l | grep -q "^ii $pkg"; then - log_info "Installing system GPIO backend: $pkg" - INITRD=No apt-get install -y "$pkg" 2>/dev/null || log_warn "Could not install $pkg" - fi - done - if [[ $EUID -eq 0 ]]; then if pip3 install -r "$web_server_dir/requirements.txt" --break-system-packages --ignore-installed 2>/dev/null; then log_success "Python dependencies installed successfully" @@ -747,4 +738,4 @@ install_dependencies_from_apt() { ldconfig return 0 -} \ No newline at end of file +}