"""Raspberry Pi model detection for choosing a WS2812 backend.""" from __future__ import annotations from functools import lru_cache @lru_cache(maxsize=1) def board_model() -> str: try: with open("/proc/device-tree/model", "rb") as f: return f.read().rstrip(b"\x00").decode("ascii") except OSError: return "Unknown" @lru_cache(maxsize=1) def board_family() -> str: model = board_model().lower() if "raspberry pi 5" in model: return "pi5" if "raspberry pi 4" in model: return "pi4" if "zero 2" in model: return "pi_zero2" if "zero" in model: return "pi_zero" if "raspberry pi 3" in model: return "pi3" return "unknown" def supports_ws281x() -> bool: return board_family() != "pi5" def pio_device(index: int = 0) -> str | None: from pathlib import Path path = Path(f"/dev/leds{index}") return str(path) if path.exists() else None def pio_available() -> bool: return pio_device(0) is not None def pi5_second_spi_bus() -> int: """ Second strip on Pi 5 SPI — prefer spi1 from dtoverlay=spi1-3cs (/dev/spidev1.0, MOSI on GPIO 20). Falls back to built-in spidev10.0. """ from pathlib import Path if Path("/dev/spidev1.0").exists(): return 1 if Path("/dev/spidev10.0").exists(): return 10 raise RuntimeError( "No second SPI device found. Add to /boot/firmware/config.txt under [all]:\n" " dtoverlay=spi1-3cs\n" "Then reboot. Wire strip 1 DIN to GPIO 20 (SPI1 MOSI).\n" "Note: dtparam=spi1=on is ignored on Pi 5; use the overlay in [all], not [cm5]." )