Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Software/web-server/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ pyyaml==6.0.3
aiofiles==25.1.0
websockets==15.0.1
spidev>=3.6
gpiozero>=2.0
110 changes: 91 additions & 19 deletions Software/web-server/strobe_calibration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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"""
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
11 changes: 1 addition & 10 deletions packaging/src/lib/pitrac-common-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -747,4 +738,4 @@ install_dependencies_from_apt() {
ldconfig

return 0
}
}
Loading