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 |
|---|---|---|
![]() |
![]() |
![]() |
| 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│
└─────────────────────┘
- Bitfocus Companion-style modules — actions + feedbacks per integration; live state pushes to button visuals
- Stream Deck-style multi-state buttons — toggle on/off with different icons and labels
- Multi-action sequences — one press → many ordered actions, with delays
- Animated icons — GIF or sprite sheets cycled at source FPS on device
- Web configurator — drag-drop deck editor, button editor with 4 tabs (Appearance / Icon / Actions / States), icon library
- System tray — runs in tray on Linux/Mac/Windows; auto-start at login
- WiFi provisioning — captive portal on first boot; no cable after that
- OTA updates — agent serves binaries; device pulls and verifies SHA-256
- Cross-platform agent — Linux, macOS, Windows, RPi (one Python codebase)
- Pluggable architecture — drop a Python file into
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.
- Viewe UEDX80480050E-WB-A — 5" 800×480 RGB IPS, ESP32-S3-WROOM-1 N16R8
- 2 × EC11 rotary encoders (any 5- or 7-pin variant)
- 1 × PCF8575 I²C GPIO expander (~10 zł)
- Pull-ups, debounce caps, wires (BOM in docs/soldering.md)
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 | shWindows (PowerShell):
iwr -useb https://raw.githubusercontent.com/kamil/streamdeck/main/install/install.ps1 | iexManual (any OS with Python 3.11+):
pipx install streamdeck-agentOpen http://localhost:8088. Register a device, copy the auth token.
cd firmware && pio run -t uploadAfter first boot the deck creates a StreamDeck-XXXX WiFi access point.
Connect with your phone — captive portal appears. Enter:
- Your WiFi SSID + password
- The agent's IP (e.g.
192.168.1.10) and port (8088) - Device ID + auth token from step 3
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.
- Single source of truth for the protocol: pydantic schema in
agent/streamdeck_agent/proto/messages.py. The firmware encoder/decoder infirmware/lib/proto/is verified against it by a live Python ↔ C++ round-trip in CI — no schema drift. - All logic is host-testable. Firmware components (quadrature, OTA
semver, provisioning state machine, UI model) are pure C++ with no
Arduino dependency. Hardware-only files in
src/are thin shims. - Server-side rendering of button bitmaps. Device gets gradient text + animations with no on-device compositing — same trick Bitfocus uses.
- Modules are isolated. Each plugin gets a
dictof 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.





