Ship the portal FastAPI controller with presets, sequences, and live audio.

Move the app under src/, add ring-spanning patterns with shared fonts and
beat-driven motion, and wire global audio settings into the web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 00:59:03 +12:00
co-authored by Cursor
parent 5094c7bcee
commit 3d444d1cb5
127 changed files with 10318 additions and 62632 deletions
+69 -43
View File
@@ -14,7 +14,13 @@ Reference: [photo of the physical portal](https://technical.kiwi/images/portal/I
floor
```
WS2812 control for Raspberry Pi 5 (orchestrator) and Pico panel adapters.
WS2812 control for Raspberry Pi 5 (orchestrator) and Pico panel firmware.
| Path | What |
|------|------|
| [`src/`](src/) | Python app, web UI, patterns |
| [`test/`](test/) | Unit tests and hardware panel scripts |
| [`firmware/`](firmware/) | Pico SDK panel firmware |
## Panel IPs
@@ -22,7 +28,7 @@ Five Pico panels on `10.1.1.10`–`10.1.1.14`. Examples drive **all 5** by defau
## Panel sizes
Panels are **9 rows** tall; width varies by panel. Edit `PANEL_WIDTH_BY_INDEX` in `leds/array_config.py`:
Panels are **9 rows** tall; width varies by panel. Edit `PANEL_WIDTH_BY_INDEX` in [`src/leds/array_config.py`](src/leds/array_config.py):
| Panel | Face | IP | Size | LEDs | Firmware |
|-------|------|-----|------|------|----------|
@@ -32,99 +38,119 @@ Panels are **9 rows** tall; width varies by panel. Edit `PANEL_WIDTH_BY_INDEX` i
| 3 | top-right | 10.1.1.13 | 9×45 | 405 | `make deploy PANEL_ID=3` |
| 4 | bottom-right | 10.1.1.14 | 9×39? | 351 | `make deploy PANEL_ID=4` |
Panels **0→1→2→3→4** run clockwise from bottom-left (`PORTAL_PANEL_ORDER_CLOCKWISE` in `leds/array_config.py`).
Panels **0→1→2→3→4** run clockwise from bottom-left (`PORTAL_PANEL_ORDER_CLOCKWISE` in `src/leds/array_config.py`).
Panel 4 may be **38 or 39** wide — find the exact width:
```bash
make deploy PANEL_ID=4
pipenv run python examples/panel_sync_test.py --test width --panel-index 4
pipenv run python test/panel_sync_test.py --test width --panel-index 4
```
Last column that lights → set `PANEL_WIDTH_BY_INDEX[4]` in `leds/array_config.py` to **column + 1** (342 LEDs for 38, 351 for 39). Firmware takes pixel count from each UDP frame — no re-flash for width changes.
Last column that lights → set `PANEL_WIDTH_BY_INDEX[4]` in `src/leds/array_config.py` to **column + 1** (342 LEDs for 38, 351 for 39). Firmware takes pixel count from each UDP frame — no re-flash for width changes.
Animations render at each panel's own width automatically.
## Patterns
Each visual lives in its own file under [`src/patterns/`](src/patterns/). Drop a new `.py` file there and the web UI picks it up without restarting (hot-load). Every file is also a standalone CLI:
```bash
pipenv run python src/patterns/scanner.py
pipenv run python src/patterns/scanner.py --brightness 0.3 --speed 2 --beam ff0000
pipenv run python src/patterns/rainbow.py --panel-index 1
```
A pattern subclass declares optional named colours (shown as colour pickers in the UI) plus brightness and speed (time scale, independent of the 15 fps UDP cap):
```python
from leds.pattern import Color, Pattern, run_cli
class Scanner(Pattern):
id = "scanner"
label = "Scanner"
fps = 35
colors = {"beam": Color("#ff0000")}
def draw(self, surface, t, params):
...
if __name__ == "__main__":
run_cli(Scanner)
```
`t` is elapsed seconds × speed. Hardware brightness is applied when frames are sent, not inside `draw`.
## Examples
### Portal web simulator
### Portal web controller
Preview all five panels in the browser (3D interior view + flat layout map). Uses the same Python animations as the hardware.
Drive all five Pico panels from the browser. Starts black until you select a preset.
```bash
pipenv install fastapi "uvicorn[standard]"
pipenv run python examples/portal_simulator.py
pipenv run dev
# open http://localhost:8765/
```
Dev mode (default): **uvicorn reload** for Python changes, **browser auto-reload** when `web/` files change. Disable with `--no-reload` or `--no-browser-reload`.
Play by tapping **preset buttons**. Switch to **Edit mode** to add or edit presets — each preset is based on a pattern (colours, brightness, speed). **Try** previews without saving; **Save** persists to `db/presets.json`. Saving a file in `src/patterns/` updates the pattern list without a full page reload.
`pipenv run dev` enables **uvicorn reload** for `src/leds/` + `src/static/` and **browser auto-reload** when `src/static/` files change. Pattern files are hot-loaded in-process (not via uvicorn). Disable with `pipenv run python src/main.py --no-reload` or `--no-browser-reload`.
Options: `--host 0.0.0.0 --port 8765` to view from another device on the network.
Open via the simulator URL (`http://…:8765/`) — do not open `index.html` directly from the filesystem (`file://`), or module imports will fail.
Open via the server URL (`http://…:8765/`) — do not open `index.html` directly from the filesystem (`file://`), or module imports will fail.
Frontend uses **Web Components** and ES modules under `web/js/`:
Frontend uses **Web Components** and ES modules under `src/static/js/`:
| Module | Role |
|--------|------|
| `<portal-app>` | Root layout, frame streaming |
| `<portal-controls>` | Sidebar settings |
| `<portal-viewport>` | Three.js 3D scene |
| `<portal-schematic>` | Flat layout map |
| `<portal-panel>` | Single LED matrix canvas |
| `<portal-app>` | Root layout, status, hot-load events |
| `<portal-controls>` | Preset buttons, Run/Edit mode |
| `<portal-preset-editor>` | Create/edit preset from a pattern |
### Pico panel (UDP)
WS2812 defaults: **GP28** (strip 0) and **GP27** (strip 1). Override over UDP:
```bash
pipenv run python examples/panel_color_test.py --panel-index 0 --pins 28:405,27:351
pipenv run python test/panel_color_test.py --panel-index 0 --pins 28:405,27:351
```
| Command | What it does |
|---------|----------------|
| `pipenv run python examples/panel_rgb_cycle.py` | Red / green / blue on all 5 panels |
| `pipenv run python examples/panel_color_test.py` | One-shot RGB + white test |
| `pipenv run python examples/panel_test.py` | Layout tests (corners, rows, chase) |
| `pipenv run python examples/panel_sync_test.py` | Positioning + sync across all panels |
| `pipenv run python examples/panel_animations.py` | All animations on all panels |
| `pipenv run python examples/panel_animations.py rolling` | One animation |
| `pipenv run python examples/panel_text.py` | Show your name |
| `pipenv run python examples/animations.py --panel` | Same as panel_animations |
| `pipenv run python test/panel_rgb_cycle.py` | Red / green / blue on all 5 panels |
| `pipenv run python test/panel_color_test.py` | One-shot RGB + white test |
| `pipenv run python test/panel_test.py` | Layout tests (corners, rows, chase) |
| `pipenv run python test/panel_sync_test.py` | Positioning + sync across all panels |
| `pipenv run python test/panel_animations.py` | All patterns on all panels |
| `pipenv run python test/panel_animations.py rolling` | One pattern (playlist runner) |
| `pipenv run python src/patterns/rolling.py` | One pattern, standalone |
| `pipenv run python test/panel_text.py` | Show your name |
Single panel only:
```bash
pipenv run python examples/panel_animations.py --panel-index 1 # 9×45 @ 10.1.1.11
pipenv run python test/panel_animations.py --panel-index 1 # 9×45 @ 10.1.1.11
```
### Pi SPI (bench / single matrix)
### Pi 5 overlays (bench / local strips)
Optional local SPI tests via [`rpi5-ws2812`](https://github.com/niklasr22/rpi5-ws2812) on GPIO 10 — not used for the five portal panels.
405 LEDs need a ~10 KiB SPI frame. Default `spidev` bufsiz (4096) only updates ~169 LEDs — run once:
Optional local PIO tests on GPIO — not used for the five portal panels:
```bash
pipenv run python examples/setup_spi_bufsiz.py --install # sudo
pipenv run python test/setup_pi5_leds.py --install
```
| Command | What it does |
|---------|----------------|
| `pipenv run python examples/spi_rgb_test.py` | Red / green / blue over SPI |
| `pipenv run python examples/spi_panel_test.py` | Corners, rows, chase — verify all LEDs |
| `pipenv run python examples/animations.py` | All animations on local matrix |
| `pipenv run python examples/matrix_demo.py` | Rainbow only |
| `pipenv run python examples/led_demo.py` | Dual-strip chase + rainbow |
Colors use logical **RGB** everywhere (`(255,0,0)` = red). Pico firmware remaps to **GRB** on the wire; Pi SPI uses the same swap when needed. The web simulator stays logical RGB.
Colors use logical **RGB** everywhere (`(255,0,0)` = red). Pico firmware remaps to **GRB** on the wire; Pi SPI uses the same swap when needed. The web preview stays logical RGB.
```python
# MicroPython on Pico — this is the reference:
np.fill((255, 0, 0)); np.write() # red
# Logical RGB — firmware converts to GRB on the wire:
# (255, 0, 0) is red
```
```bash
pipenv run python examples/panel_rgb_cycle.py # same colors over UDP
pipenv run python test/panel_rgb_cycle.py # same colors over UDP
```
### Firmware