# Custom Streamdeck Custom Streamdeck is a local control surface platform for a 10-button Raspberry Pi Pico device. It pairs physical button input with a desktop web app, persistent configuration, and a backend action engine so the deck can launch apps, send shortcuts, switch folders, and run backend plugins. This repository is organized more like an application than a hardware experiment: the Pico emits button events, the backend manages state and automation, and the frontend provides a live configuration surface for day-to-day use. ## What The App Does - Reads button press and release events from a Raspberry Pi Pico over USB serial - Stores profiles, folders, button mappings, and action configuration in SQLite - Serves a local web UI for deck configuration and live monitoring - Executes built-in actions such as key presses, action chains, app launch, and folder navigation - Loads backend plugins for feature-specific actions such as HTTP requests, media control, and clipboard tools - Broadcasts live state updates over WebSocket so the UI stays in sync with the device ## Production View The app is intended to run as a local always-on controller on a Windows workstation: - The backend is the source of truth for device state and button configuration. - The frontend is a built artifact served by FastAPI in production-style usage. - The Pico is treated as an attached input device rather than a development-only serial toy. - Button layouts are protected by a canonical hardware mapping so profile changes do not drift physical position assignments. - Plugins extend backend behavior without requiring frontend-specific code for every new action. This is still a local app, not a hosted SaaS service, but the codebase now behaves like an operational desktop product. ## Architecture ### Hardware Layer The Pico firmware in `pico/main.py` scans the 10 physical buttons and prints JSON events over USB serial. Button wiring order: 1. GP28 2. GP27 3. GP26 4. GP22 5. GP21 6. GP20 7. GP18 8. GP19 9. GP17 10. GP16 Each button’s other leg goes to GND. The firmware uses internal pull-ups, so a pressed button reads as LOW. ### Backend Layer The FastAPI backend: - auto-detects the Pico serial port, or uses a configured port override - reads and validates button events - persists state in SQLite under `data/` - executes actions through a central action engine - loads plugin modules from `plugins/` - serves the built frontend and WebSocket updates Key backend areas: - `backend/main.py`: app lifecycle, API routes, WebSocket endpoint - `backend/database.py`: SQLite schema, settings, profiles, folders, buttons, event history - `backend/services/serial_service.py`: Pico connection and event loop - `backend/services/actions.py`: action execution engine - `backend/services/plugins.py`: runtime plugin loading and dispatch - `backend/services/apps.py`: Windows app discovery and launch support ### Frontend Layer The frontend is a React + Vite application that: - shows connection and sync status - manages profiles and folders - edits button labels, colors, icons, trigger modes, and actions - renders plugin action fields dynamically from backend metadata - stays synchronized through backend WebSocket broadcasts Primary frontend source lives in `frontend/src/`. ## Current Action Model Built-in actions currently include: - No-op / display-only buttons - Keyboard shortcut execution - Chained multi-step actions - Windows app launch - Folder open - Folder rotation - Plugin-backed actions Plugin actions currently include examples such as: - HTTP requests - Media controls - Clipboard tools ## Plugin System Backend plugins live in `plugins/` and expose a top-level `PLUGIN` object. The backend imports each plugin, reads its declared actions, and exposes those actions to the frontend automatically. That means new capabilities can usually be added by shipping a Python plugin file without hand-building a matching frontend form. Example shape: ```python class MyPlugin: name = "My Plugin" desc = "Does a thing" version = "0.1.0" actions = [ { "id": "do_thing", "name": "Do Thing", "fields": [{"id": "value", "label": "Value", "type": "text"}], } ] def on_load(self, ctx): pass def on_event(self, ctx, event): pass def execute_action(self, ctx, action_id, config, event): pass PLUGIN = MyPlugin() ``` ## Repository Layout - `backend/`: FastAPI app, database, action engine, serial service, plugin runtime - `frontend/`: React configuration UI - `plugins/`: backend plugin modules - `pico/`: MicroPython firmware for the device - `pc/`: utility scripts used during hardware bring-up and diagnostics - `tests/`: backend tests - `data/`: local runtime state, including SQLite data ## Local Setup ### Requirements - Windows machine - Python 3.13 or compatible recent Python 3 - Node.js and npm - Raspberry Pi Pico flashed with the firmware in `pico/main.py` ### Install Backend Dependencies ```powershell python -m pip install -r requirements.txt ``` ### Install Frontend Dependencies ```powershell cd frontend npm install cd .. ``` ### Install Overlay Dependencies ```powershell cd overlay npm install cd .. ``` ## Running The App ### Production-Style Local Run Build the frontend first: ```powershell cd frontend npm run build cd .. ``` Start the backend: ```powershell python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 ``` Then open: `http://127.0.0.1:8000/` In this mode, FastAPI serves the built frontend from `frontend/dist`. ### Frontend Development Mode Run the backend in one shell: ```powershell python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 ``` Run Vite in another: ```powershell cd frontend npm run dev ``` Then open the Vite URL, usually: `http://127.0.0.1:5173/` ### Transparent Profile Overlay The optional Electron overlay listens to the backend WebSocket at `ws://127.0.0.1:8000/ws` and shows a click-through popup when the visible deck changes. It appears on startup, active profile changes, and active folder changes, then fades away after 7 seconds. Start the backend first, then run: ```powershell cd overlay npm run dev ``` The overlay is frameless, transparent, always on top, and non-interactable so it can sit over other desktop apps without stealing clicks. It does not put a button in the taskbar; the app lives in the Windows notification area with a tray menu for showing the last overlay state or quitting it. To build the Windows installer locally: ```powershell cd overlay npm run dist:win ``` The installer is written to `overlay/release/`. ## Operational Notes ### Device Detection By default the backend attempts to auto-detect the Pico using USB metadata. If that fails, the serial port can be set in the application settings. ### Persistence Profiles, folders, button assignments, manual app entries, and event history are stored in SQLite under `data/streamdeck.sqlite`. ### Live Updates The UI receives `state.updated` and action/device events over WebSocket. This keeps the browser view aligned with the live backend without manual refreshes. ### Windows Integration App launch discovery currently focuses on Windows environments: - Start Menu shortcuts - installed applications from uninstall registry keys - manually registered app paths ### Logging And Diagnostics The backend records event history in SQLite. During hardware bring-up, `pc/listen_buttons.py` can still be used as a simple serial diagnostic tool. ## Testing Run the backend test suite from the repo root: ```powershell $env:PYTHONPATH = (Get-Location).Path pytest -q ``` ## Deployment Considerations For a reliable day-to-day setup on a dedicated machine: - build the frontend and serve the compiled assets from FastAPI - pin Python dependencies from `requirements.txt` - run the backend under a process manager, scheduled task, or startup shortcut - keep the Pico on a stable USB port when possible - treat `data/` as local runtime state and back it up if profiles matter This app is best thought of as a local control appliance: hardware input on one side, desktop automation and operator UI on the other. ### Overlay Release Automation The Gitea workflow in `.gitea/workflows/overlay-release.yml` builds the Electron overlay on a Windows runner for pushes and pull requests that touch `overlay/`. Pushing a tag that matches `v*` or `overlay-v*` also creates or reuses a Gitea release and uploads the generated installer `.exe`. The workflow uses the built-in `GITEA_TOKEN` with `releases: write` permission, so the repository Actions settings must allow release writes for the job token. ## Legacy Utility For raw serial logging without the full app: ```powershell python .\pc\listen_buttons.py ``` If auto-detect fails: ```powershell python .\pc\listen_buttons.py --port COM5 ```