An open-source Stream Deck / Bitfocus Companion alternative built on a 5" ESP32-S3 touch display + 2 rotary encoders. After first flash the device only needs USB-C power; everything else (config, firmware updates, button artwork) flows over WiFi from a companion agent running on your computer.
| Home page | OBS page | Settings |
|---|---|---|
http://localhost:8088)| Dashboard | Deck editor | Firmware |
|---|---|---|
Deck screenshots are captured live via
POST /api/devices/{id}/screenshot— the agent sends a WS request, the deck dumps its LVGL framebuffer (raw RGB565) over HTTP, the agent converts to PNG.
┌─────────────────┐ WiFi WS ┌─────────────────────┐
│ ESP32-S3 deck │ ◄──────────────────► │ streamdeck-agent │
│ • 800×480 LVGL │ config, frames, │ • web configurator │
│ • 2 EC11 encs │ stats, events │ • plugin modules │
│ • WiFi/BT5 LE │ │ • OBS/WLED/HTTP/MQTT│
└─────────────────┘ └─────────────────────┘
│
┌──────────┴──────────┐
│ Browser-based UI │
│ http://localhost:8088│
└─────────────────────┘
modules/ to add an integration| Module | Actions | Feedbacks |
|---|---|---|
system |
hotkey, type_text, volume_set/delta, toggle_mute, run, open_url | volume_current, is_muted |
obs |
switch_scene, toggle_record/stream/mic, set_source_visible | is_recording, is_streaming, is_mic_muted, current_scene |
wled |
set_power, toggle, set_brightness, adjust_brightness, set_preset, set_color | is_on, brightness, current_preset |
http |
request, get, post_json | — |
multi_action |
run_macro, run_sequence | — |
Adding new modules: subclass Module, decorate methods with @action /
@feedback. Auto-discovered, surfaced in the web UI without code changes.
Follow docs/soldering.md. Verify with the bring-up sketch in firmware/bringup/.
Linux / macOS:
curl -sSf https://raw.githubusercontent.com/kamil/streamdeck/main/install/install.sh | sh
Windows (PowerShell):
iwr -useb https://raw.githubusercontent.com/kamil/streamdeck/main/install/install.ps1 | iex
Manual (any OS with Python 3.11+):
pipx install streamdeck-agent
Open http://localhost:8088. Register a device, copy the auth token.
cd firmware && pio run -t upload
After first boot the deck creates a StreamDeck-XXXX WiFi access point.
Connect with your phone — captive portal appears. Enter:
192.168.1.10) and port (8088)The deck reboots, joins your WiFi, pairs with the agent. Done — the device is now wireless and configurable from the web UI.
agent/ Python agent (FastAPI + WS + tray + modules)
streamdeck_agent/
config/ Pydantic config models + JSON store
core/ Server, render, dispatcher, auth, firmware, stats
modules/ system, obs, wled, http, multi_action
audio/ Linux/Windows/macOS volume backends
tray/ pystray-based tray icon + state machine
proto/ WS message types
tests/{unit,integration}/
firmware/ ESP32 firmware
lib/ Pure-C++ logic libs (host-testable)
encoder/ Quadrature decoder + multi-encoder
ui_state/ Profile/page/button model + touch translation
proto/ WS protocol encode/decode (matches Python)
provisioning/ Captive portal state machine + form parsing
ota/ Semver compare + URL building + SHA-256
src/ Hardware-only glue (LVGL, WiFi, OTA)
ui/ LVGL bridge (status bar, grid, animations)
net/ WiFi provisioning portal, OTA fetcher
main.cpp Boot + dispatch
bringup/ Standalone sketch for hardware bring-up
tests/ Python ↔ C++ live round-trip
CMakeLists.txt Native (host) test build
platformio.ini Hardware build (ESP32-S3)
web/ Svelte configurator
src/{pages,components,lib}/
install/ Cross-platform installers + service files
docs/ Soldering, architecture, etc.
make test # everything: agent + firmware native + interop + web type-check
make test-agent # 318+ pytest cases
make test-firmware # 6 doctest binaries
make test-interop # Python ↔ C++ live round-trip
CI runs all of these on Linux/macOS/Windows + Python 3.11/3.12/3.13. See .github/workflows/ci.yml.
agent/streamdeck_agent/proto/messages.py. The firmware encoder/decoder
in firmware/lib/proto/ is verified against it by a live Python ↔ C++
round-trip in CI — no schema drift.src/ are thin shims.dict of settings, exposes
actions + feedbacks, never reaches into the registry. Pluggable via
Python entry points (planned) or drop-in files (today).MIT — do whatever, just don't blame me if your encoder catches fire.