# LuxStats Hardware/software status LEDs for your desk: a Pimoroni **Tiny 2040** drives a NeoPixel strip while a **PyQt5 configurator applet** owns the configuration, maps LED zones to live statuses, and pushes every change to the board instantly. The board is treated as read-only: config lives on the host and is re-pushed on connect. ``` luxstats/ ├── firmware/ │ ├── boot.py # enables the second USB CDC (data) serial channel │ └── code.py # LuxStats firmware v2 (CircuitPython, RAM-only config) ├── host/ │ ├── luxstats_configurator.py # PyQt5 applet │ ├── system_monitor.py # status probes + notification watcher │ └── requirements.txt └── README.md ``` ## Hardware setup Wire NeoPixel data-in to **GP0** (configurable), power from 5V/USB, share ground. Default pixel order `GRBW` (SK6812 RGBW); use `GRB` for WS2812B. 1. Install CircuitPython 8/9 on the Tiny 2040. 2. Copy Adafruit `neopixel.mpy` into `CIRCUITPY/lib/`. 3. Copy `firmware/boot.py` and `firmware/code.py` to the `CIRCUITPY` root. 4. Power-cycle once; the board then exposes two serial ports (REPL + the LuxStats data channel). ## Host setup ```bash cd host pip install -r requirements.txt # PyQt5, pyserial, psutil python3 luxstats_configurator.py ``` Linux extras: `dialout` group for serial, `fuser` (psmisc) for camera detection, `dbus-monitor` for the notification watcher, `nvidia-smi` for NVIDIA GPUs (AMD uses sysfs/hwmon automatically), and the `drivetemp`/`nvme` kernel sensors for drive temperatures. ## Running in the background (Arch Linux) `./luxstats.sh` auto-detects the newest installed Python 3, runs directly on the system interpreter when `python-pyqt5 python-pyserial python-psutil` are installed via pacman (no venv), and otherwise creates a managed venv under `~/.local/share/luxstats/venv`. It launches the app detached (`start`/`stop`/`status`), logging to `~/.local/state/luxstats/luxstats.log`. An optional systemd user unit is in `packaging/luxstats.service`. The app auto-connects at startup (and on every link drop) to the device named `Tiny 2040 (8MB) - CircuitPython CDC2 control` on any `/dev/ttyACM#` port - the name is stored in the config and editable on the Connection tab. ## Zone model Everything on the strip is an **ordered list of zones**. A zone is either a status source or an ambient effect region: * **Status zones** — known sources are locked to their correct type: `cpu`/`gpu`/`ram`/`disk*` are percent (gradient, optional bar-graph fill); `*_temp` sources report **raw Celsius** and map onto a gradient between a per-zone min/max °C; `eth` and `wifi` are **positional** — one color per position (down/10M/100M/1G/2.5G/10G and down/2.4/5/6 GHz, no blending) — and `eth` reads 0 unless the link carrier is actually up; `vpn`/`cam` are on/off colors; `custom` takes pushed RGBW values. * **Ambient zones** — off/solid/breathe/rainbow/sparkle with color and speed, assigned to an explicit range. Pixels not claimed by any zone stay off. Zones **may not overlap**: the app flags conflicts (and ranges past the end of the strip) and blocks pushes until fixed. Reorder zones with Move up/down. Each row has a **Test** start/stop toggle that blinks that exact LED range on the strip while you dial in positions; the camera glow zone (start/count) has its own start/stop test button. The **camera glow** overlay uses assigned LEDs with range syntax (`1-5,9-13`) and the same animation presets as ambient zones (solid/breathe/rainbow/sparkle). Overlays render on top of zones, in order: camera glow → notification blinker → range-test blink. Any start/stop test (zone range, camera glow, raw fill) pauses automated polling and resumes it when the test stops; fake-status buttons pause polling for 10 s. ## Automation The Automation tab discovers every source — including **all mounted physical drives** (usage per drive, temperature where a `nvme`/`drivetemp` sensor exists, plus `disk`/`disk_temp` aggregates) and **separate CPU/GPU utilisation and temperature** — and pushes the enabled ones at your chosen interval (adjustable in 100 ms steps; slow probes are cached internally so 10 Hz polling stays cheap). Camera use toggles the glow automatically — detection ignores PipeWire/WirePlumber/PulseAudio holding `/dev/video*` open, and both on AND off transitions are pushed. Desktop notifications (Linux/DBus) fire the blinker. The watcher toggle, camera-automation toggle, blinker period/count, and poll interval all persist in the host config. ## Configuration & persistence Every edit auto-pushes to the device after a short debounce — there is no Apply button. Config is saved on the host at `~/.config/luxstats/config.json` (Linux/macOS) or `%APPDATA%\luxstats` (Windows), reloaded at startup, and pushed on every connect. Profiles can be exported/imported as JSON. ## Connection reliability The firmware uses non-blocking serial writes (it can never stall waiting on the host to read a reply) and only acks `status` updates when asked. The app drains stale input before every request so request/response can't desync, serialises writes across threads, pings the device after ~10 s of idle, and auto-reconnects every few seconds if the link drops without a manual disconnect. ## Serial protocol (JSON lines, host → device) | Command | Purpose | |---|---| | `{"cmd":"ping"}` | health check, returns `{"pong":1,"fw":2}` | | `{"cmd":"config","config":{...}}` | apply full configuration (RAM only) | | `{"cmd":"get_config"}` | read back the active config | | `{"cmd":"status","values":{"cpu":0.4,"gpu_temp":0.6,"disk_sda":0.8,...}}` | push live values (silent; add `"ack":1` for a reply) | | `{"cmd":"notify","color":[r,g,b,w],"period":0.4,"count":6}` | blinker (`count:-1` = until cleared) | | `{"cmd":"notify_clear"}` | stop the blinker | | `{"cmd":"camera","active":true}` | camera glow on/off | | `{"cmd":"test","start":2,"count":3,"active":true}` | blink a range while configuring (auto-expires after 3 min) | | `{"cmd":"raw","pixels":[[r,g,b,w],...]}` / `{"cmd":"raw_off"}` | direct test mode | Custom sources: add a zone with any name (type `custom` or `bool`) and push matching keys via `status` — CI state, mic mute, meeting status, etc.