⌨️ Firmware for Schnell Keypad — a wireless macro keypad built on the Espressif ESP32-S3-WROOM-1, by DriftKingTW.
It works as both a USB HID keyboard and a Bluetooth LE keyboard, with on-device layout switching, macros, a rotary encoder, an OLED display, and a Wi-Fi configuration page.
- Dual output — switch on the fly between USB HID and BLE HID.
- 5 × 7 key matrix with multiple, switchable layouts.
- Macros — key-stroke combos, string output, or string + Enter.
- Tap-Toggle layers — momentary layer switch, double-tap to lock.
- Rotary encoders — onboard encoder plus an optional I²C extension board (PCF8574: 3 keys + encoder).
- Bi-directional switch for quick layout cycling.
- OLED status display (SSD1306 128×32) — battery, connection, layout, key feedback.
- WS2812 status LED and battery monitoring with low-battery alert.
- Power management — auto screen sleep, deep sleep with key-press wake, "caffeinated" (stay-awake) and output-lock modes.
- Configuration — Wi-Fi web UI, Improv
serial provisioning, or direct serial
keyconfig.jsonupload.
| Part | Detail |
|---|---|
| MCU | ESP32-S3-WROOM-1 (N4R2 — 4 MB flash, 2 MB PSRAM) |
| Display | SSD1306 128×32 OLED (I²C) |
| LED | 1× WS2812 |
| Input | 5×7 matrix, onboard rotary encoder, bi-directional switch, 3 config buttons |
| Extension (optional) | PCF8574 rotary-encoder board @ 0x38 |
The board definition lives in
boards/esp32-s3-wroom-1-n4r2.jsonso the project builds without any local PlatformIO customization.
The firmware is organized into focused modules under src/:
| Module | Responsibility |
|---|---|
main.cpp |
setup()/loop(), FreeRTOS tasks, key/power/config logic |
usbhid / blehid |
USB / BLE HID transport wrappers (each isolates one HID library) |
keyboard_output |
KeyboardOutput interface over USB/BLE |
config_store |
parses & caches keyconfig.json once |
display_state |
mutex-guarded OLED state |
web_server |
HTTP configuration server + Improv provisioning |
helper.hpp |
small SPIFFS/format helpers |
⚠️ Note for contributors:USBHIDKeyboard.h(TinyUSB) andBleKeyboard.h(NimBLE) define conflicting macros and must not be included in the same translation unit — that is why each is wrapped in its own module. Module.cppfiles should not includemain.hpporhelper.hpp; depend onmainviaexterndeclarations instead.
This is a PlatformIO project.
pio run # build firmware
pio run -t upload # build + flash over USB
pio run -t uploadfs # build + flash the SPIFFS filesystem (web UI + config)
pio device monitor # serial monitorEnvironment: esp32-s3-wroom-1-n4r2 (see platformio.ini).
Each release ships prebuilt binaries. Flash offsets for the N4R2 (no-OTA) layout:
| File | Offset | Contents |
|---|---|---|
firmware-merged.bin |
0x0 |
bootloader + partitions + app (program only) |
spiffs.bin |
0x210000 |
filesystem (web UI + default config) |
# Update the program only (keeps existing settings):
esptool.py --chip esp32s3 write_flash 0x0 firmware-merged.bin
# Fresh device (program + filesystem):
esptool.py --chip esp32s3 write_flash 0x0 firmware-merged.bin 0x210000 spiffs.binRe-flashing
spiffs.binoverwrites the on-device configuration with the packaged defaults. To change only the program, flashfirmware-merged.binalone. Helper scripts are also provided intools/.
- Web UI — long-press the boot-mode config button to enter Wi-Fi mode, then
open
http://schnell.local(or the IP shown on the OLED). Keymaps, macros, layouts and network settings can be edited live. The companion editor is the Schnell Keypad Configuration Tool. - Improv — provision Wi-Fi credentials over serial.
- Serial — paste a
keyconfig.jsoninto the serial monitor to update the keymap directly.
GitHub Actions handles builds and releases automatically:
- CI (
.github/workflows/ci.yml) — compiles on every push / PR tomaster. - Release (
.github/workflows/release.yml) — pushing av*tag builds the firmware, SPIFFS image and merged binary, publishes a GitHub Release with the artifacts attached, and then notifies the web configuration tool (see below).
The in-firmware version string is injected from the git tag at build time
(scripts/version.py), so it always matches the release.
Use an annotated tag — its message is reused as the Discord announcement text (see below), so write a short, user-facing summary:
git tag -a v1.2.0 -m "Add macro export; fix occasional Bluetooth disconnects."
git push origin v1.2.0 # use -beta.N / -rc.N for pre-releasesAfter the release is created, release.yml sends a repository_dispatch to the
Schnell Keypad Configuration Tool,
which downloads the new firmware-merged.bin + spiffs.bin and publishes them
to its in-browser firmware installer — no manual upload needed.
- A stable tag (e.g.
v1.2.0) updates the installer's stable channel and is archived so it stays selectable as an older version. - A pre-release tag (containing
beta/rc, e.g.v1.2.0-beta.1) updates the beta channel, which only ever keeps the latest build.
This requires a WEB_TOOL_DISPATCH_TOKEN repository secret — a Personal Access
Token with Contents: read and write on the configuration-tool repo (the
default GITHUB_TOKEN cannot trigger workflows across repositories).
release.yml also posts an announcement to a Discord channel: the version and
channel (Stable/Beta), a link to the in-browser installer (pre-selected to the
matching channel), and a link to the GitHub release.
- The embed description uses the annotated tag message, so keep it short and user-facing. A lightweight tag (no message) falls back to a generic line.
- Set a
DISCORD_WEBHOOK_URLrepository secret to a channel webhook. The step is skipped if it is unset and never fails the release if Discord is down.
Licensed under the GNU General Public License — see LICENCE.