DIY Stream Deck under $40 using ESP32 touch display
  • C++ 96.6%
  • C 3.4%
Find a file
2026-04-21 02:29:39 +05:30
boards add: esp32 streamdeck firmware 2026-04-21 02:20:21 +05:30
include add: esp32 streamdeck firmware 2026-04-21 02:20:21 +05:30
src add: esp32 streamdeck firmware 2026-04-21 02:20:21 +05:30
.gitignore add: esp32 streamdeck firmware 2026-04-21 02:20:21 +05:30
platformio.ini add: esp32 streamdeck firmware 2026-04-21 02:20:21 +05:30
README.md update: README 2026-04-21 02:29:39 +05:30

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

Stream Deck grid

Notes layer (opt-in feature, see doc for more details)

Notes view

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.7 for 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