- C++ 96.6%
- C 3.4%
| boards | ||
| include | ||
| src | ||
| .gitignore | ||
| platformio.ini | ||
| README.md | ||
esp32-streamdeck
A DIY Stream Deck, approximately $40 in parts. Runs on the Elecrow CrowPanel 7" ESP32-S3.
This repo is the firmware, the C++ code that runs on the display module. It draws
a 3×3 tile grid in LVGL, reads GT911 touch, and POSTs {"key": "<id>"} over
WiFi to a companion HTTP server
(vipul-sharma20/esp32-streamdeck-server)
which runs the mapped shell command. An optional second tab renders today's
daily-notes markdown and refreshes when you save in your editor.
Deck layer — 3×3 action grid
Notes layer (opt-in feature, see doc for more details)
This repo is the firmware only. It expects a companion HTTP server: vipul-sharma20/esp32-streamdeck-server
Stack
- Arduino framework on PlatformIO (
espressif32@6.9.0) - LVGL 8 for UI
- Arduino_GFX
1.4.7for RGB parallel driver (pinned — see notes below) - GT911 driven directly over I²C (no third-party touch lib)
ARDUINO_USB_CDC_ON_BOOT=0— serial goes out UART0 via the onboard CH340-class bridge, not the S3's native USB
Hardware
Targets ESP32-S3-WROOM-1-N4R8 (4 MB flash, 8 MB OPI PSRAM). The board
variant JSON is vendored at boards/esp32-s3-devkitc1-n4r8.json — required,
the default esp32-s3-devkitc-1 ships a bootloader that never initializes
PSRAM and you'll reboot-loop on framebuffer alloc.
Pins and RGB timings are copied verbatim from Elecrow's V1/V2 LovyanGFX factory demo. Tested on V1/V2 boards — V3.0 routes GT911 RST through a PCA9557 expander and is not supported by this firmware.
Setup
pipx install platformio # or brew install platformio
git clone https://github.com/vipul-sharma20/esp32-streamdeck
cd esp32-streamdeck
cp include/secrets.h.example include/secrets.h
$EDITOR include/secrets.h # WIFI_SSID, WIFI_PASS, SERVER_HOST, SERVER_PORT
pio run -t upload --upload-port /dev/cu.usbserial-XX
ESP32-S3 is 2.4 GHz only — the SSID you give it has to be on the 2.4 GHz
band. upload_speed = 460800 is set in platformio.ini; the UART bridge
chokes at 921600.
Boot a companion server (see
esp32-streamdeck-server)
at $SERVER_HOST:$SERVER_PORT before expecting taps to do anything.
Default actions (macOS)
The 3×3 grid in src/main.cpp ships with:
| Tile | Key sent | Server default |
|---|---|---|
| Mic Mute | mute_mic |
toggle input volume 0↔75 via osascript |
| Play/Pause | play_pause |
Music app playpause |
| Next | next_track |
Music app next track |
| Vol Down | vol_down |
system output −10 |
| Vol Up | vol_up |
system output +10 |
| DND | dnd_toggle |
runs a user-defined Shortcut Toggle Focus |
| Slack | open_slack |
open -a Slack |
| Alacritty | open_terminal |
open -a Alacritty |
| Lock | lock_screen |
pmset displaysleepnow |
Firmware owns the keys and labels; the server owns the commands. Swap the
server action in actions.yaml without reflashing.
Custom actions
Two places to touch, deliberately decoupled:
1. Firmware (the tile)
Edit TILES[9] in src/main.cpp:
static const Tile TILES[9] = {
{ "my_new_key", "My Label", LV_SYMBOL_WIFI, 0x3B82F6 },
...
};
Fields: internal key (sent as {"key": "..."}), display label, LVGL symbol
(grep .pio/libdeps/crowpanel_7/lvgl/src/misc/lv_symbol_def.h for valid ones —
LVGL 8 has no LV_SYMBOL_MINUS), accent color (hex RGB).
Then pio run -t upload.
2. Server (the action)
Add to actions.yaml on the server repo:
my_new_key:
run: "osascript -e 'display notification \"hi\"'"
my_toggle_key:
toggle: true
on: "<shell cmd>"
off: "<shell cmd>"
Server hot-reloads actions.yaml per request — no restart needed.
Notes layer (opt-in)
Second tab shows today's markdown diary, parsed server-side into a block AST
(h1/h2/h3/todo/bullet/paragraph/spacer), rendered on-device as
styled LVGL labels. Polls mtime every 3s while the tab is active; refetches
full AST only on change. Save in vim → tab updates in ~3s.
Compile-time flag in platformio.ini:
build_flags =
...
-DENABLE_NOTES_LAYER=1 ; 0 to compile it out entirely
When =0: no Notes tab, no ArduinoJson linked, the header shows a single
"Stream Deck" title instead of tabs. Smaller firmware, cleaner for people who
don't keep dated markdown notes.
Server-side opt-in is independent — toggle diary.enabled in the server's
config.yaml and point diary.path at whatever daily-notes folder you keep
(works with vimwiki, Obsidian dailies, Logseq journals, any YYYY-MM-DD.md
scheme).
Layout on disk
esp32-streamdeck/
├── platformio.ini
├── boards/
│ └── esp32-s3-devkitc1-n4r8.json # vendored board variant (required)
├── include/
│ ├── lv_conf.h # LVGL compile config
│ ├── secrets.h # gitignored
│ └── secrets.h.example
├── src/
│ └── main.cpp # everything
└── CONTEXT.md # deep notes on hiccups / gotchas