Loading 3D model… large boards can take a moment.
A custom BLE macro pad controlled by a Windows desktop application. Press a key, run a command. Switch profile, change context. Everything is driven by the app, the device is a pure peripheral.
BLEDeck is a 4×4 mechanical keypad with per-key RGB lighting, a rotary encoder, and an OLED display, built around an ESP32. It communicates over BLE with a Windows app that maps each key to a shell command or executable. Profiles let you maintain separate key layouts for different workflows.
The device carries no persistent state. Every time the app connects, it pushes the current profile names and key colors to the device. This is by design, the app is the source of truth.
| Component | Part |
|---|---|
| MCU | ESP32 DevKitC v1 (NodeMCU-32S) |
| Keys | 16 × Gateron switches (CherryMX style) on Kailh hot-swap sockets |
| RGB | 16 × WS2812B-4020 side-emitting SMD LEDs |
| Display | SSD1306 128×64 OLED |
| Encoder | EC11 rotary encoder with push button |
| Extra buttons | CON (confirm) + BACK, mounted beside the encoder |
| Battery | Optional 1S LiPo with 15 kΩ / 4.3 kΩ voltage divider on GPIO 13 |
| PCB | Custom KiCad design, v1.1 (files under pcb/) |
BLEDeck/
├── firmware/ PlatformIO project (ESP32 Arduino)
│ └── src/
│ ├── main.cpp Main loop, BLE callbacks, all handlers
│ ├── configuration.h Pin map, BLE limits, battery constants
│ ├── protocolparser.h Binary protocol definition + packet parser
│ ├── ota_manager.h/.cpp WiFi OTA update manager (ElegantOTA)
│ ├── menu.h Simple OLED settings menu
│ ├── images.h XBM splash/lock screen bitmaps
│ └── credentials.h WiFi + OTA password (gitignored, see below)
├── windows_app/ PyQt5 desktop application
│ ├── main.py BLEDeck GUI, BLE lifecycle, notification dispatch
│ ├── key_button.py KeyButton widget
│ ├── ble_protocol.py Packet builders, parsers, opcode constants
│ ├── ble_client.py BleakClient re-export, BLE UUIDs, simulator factory
│ ├── profile_manager.py Load/save profiles.json
│ ├── app_settings.py Persistent app-level settings (preferred device MAC, …)
│ ├── action_runner.py Command/macro dispatch with re-entrancy guard
│ ├── macro_models.py Immutable step types + JSON serialization
│ ├── macro_recorder.py pynput-based recorder with window anchor detection
│ ├── macro_player.py Synchronous macro playback
│ ├── macro_dialog.py MacroDialog QDialog (record, edit, reorder, test)
│ ├── win32_utils.py Windows API helpers (window/monitor detection)
│ └── tests/ Pytest suite
│ ├── test_ble_protocol.py
│ ├── test_profile_manager.py
│ ├── test_action_runner.py
│ └── test_macro_models.py
├── simulator/ BLE device simulator (no hardware needed)
│ ├── __main__.py Entry point: python -m simulator [--ble]
│ ├── _context.py Module-level singletons (DeviceState, active client)
│ ├── device_state.py Simulated device state
│ ├── command_handler.py PC→Device command handler
│ ├── event_emitter.py Device→PC event builders
│ ├── ble_server.py Real BLE via WinRT GattServiceProvider (Mode B)
│ ├── fake_bleak_client.py In-process loopback (Mode A)
│ ├── cli.py Interactive REPL
│ └── tests/ Pytest suite
├── pcb/ KiCad schematic + layout + Gerbers
├── docs/ Protocol reference and debugging guides
│ ├── ble_protocol_reference.md
│ ├── protocol_debugging.md
│ └── hex_quick_reference.md
└── debug/
└── protocol_decoder.py CLI tool to decode raw BLE packets
Full setup and configuration details: firmware/README.md
0xAA | OPCODE | LENGTH(2B) | PAYLOAD; max payload 256 bytes0x85OP_HELLO / OP_DEVICE_TELEMETRY handshake - on connect the app sends OP_HELLO with its protocol/app version, the device replies with telemetry (firmware version, uptime, free heap, BLE error count)firmware/src/credentials.h.example → firmware/src/credentials.h and fill in:
#define OTA_WIFI_SSID "your_ssid"
#define OTA_WIFI_PASSWORD "your_wifi_password"
#define OTA_HTTP_PASSWORD "your_ota_http_password"
cd firmware
pio run --target upload
Don't want to install PlatformIO? You can have GitHub build a firmware binary
with your own WiFi + OTA credentials baked in and email it to you. The
Build Personalized Firmware workflow handles it.
MAIL_USERNAME — the Gmail address that sends the buildMAIL_PASSWORD — a Gmail App Password
for that account (requires 2-Step Verification; your normal password will
not work)wifi_ssid, wifi_password — the network the device joins for OTAota_http_password — the password for the OTA upload pagerecipient_email — where to send the finished firmware.binfirmware.bin
arrives by email; flash it to the ESP32 with ElegantOTA or esptool.Spam note: the email is sent from a personal Gmail account with an attachment, so it can land in Spam/Junk. If it doesn't show up in your inbox within a few minutes, check there and mark it Not spam.
Privacy note: the credentials you enter are masked in the build logs and the generated
credentials.his deleted at the end of the run, but the finished binary still contains them and travels through Gmail. Use this for personal networks, not shared/sensitive ones.
Full setup and usage details: windows_app/README.md
subprocess) or recorded macroprofiles.json%APPDATA%\BLEDeck\logs\bledeck.log (5 × 20 MB rotation, 100 MB cap; KEEP_ALIVE traffic filtered out)powershell, cmd /c, iex, curl … | iex, bitsadmin, mshta before they can runcd windows_app
pip install -r requirements.txt
python main.py
Python 3.12 or later required. Tested on Windows 11.
A device simulator is included — no physical BLEDeck required.
Loopback mode (default): no Bluetooth, instant connect. Pick the row for your shell:
| Shell | Command |
|---|---|
| cmd.exe | set BLEDECK_SIM=1 && python windows_app\main.py |
| PowerShell | $env:BLEDECK_SIM=1; python windows_app\main.py |
| Git Bash | BLEDECK_SIM=1 python windows_app/main.py |
Real BLE mode (two machines): simulator on Machine A, app on Machine B:
python -m simulator --ble
Full details: simulator/README.md
Full reference: docs/ble_protocol_reference.md
| Direction | Opcodes |
|---|---|
| PC → Device | 0x01 KEEP_ALIVE, 0x02 CHANGE_PROFILE, 0x03 SYNC_PROFILES, 0x04 SET_RGB_KEY, 0x05 SET_ALL_RGB_KEYS, 0x06 LOCK_DEVICE, 0x07 HELLO |
| Device → PC | 0x81 KEEP_ALIVE_REPLY, 0x82 PROFILE_CHANGED, 0x83 BUTTON_PRESSED, 0x84 KEY_PRESSED, 0x85 BATTERY_STATUS, 0x86 DEVICE_TELEMETRY |
To decode a raw packet from the log:
cd debug
python protocol_decoder.py "aa 85 00 01 48"
# or interactive:
python protocol_decoder.py
For advanced debugging see docs/protocol_debugging.md
If you're interested in understanding the protocol, take a look at docs/hex_quick_reference.md
BLEDeck is designed for Windows power users who want a physical shortcut pad without writing firmware macros. Key assignments live entirely in the app, change them without reflashing.
Typical profiles:
The rotary encoder switches between profiles. The three encoder-side buttons (CON, BACK, PUSH) send named button events to the app, which can be mapped to any action.
Custom two-layer board designed in KiCad. Gerbers for JLCPCB are included under pcb/. Parts list and supplier links are in pcb/README.md.
Current version: v1.1
3D Models ready to be printed in PLA are available under enclosure/stls folder.
Ideally you need to assemble parts in the order:
Firmware and application source code are licensed under the MIT License.
Hardware design files (PCB, schematics) and third-party component symbols/footprints retain their original licenses as listed in pcb/README.md. The custom oledknob.step footprint is non-commercial use only.
No comments yet. Be the first to ask about this board.