A 4×4 BLE + USB HID macropad with 16 WS2812B RGB LEDs, 7 switchable profiles, and a JSON configuration protocol over three transports (USB Serial, USB Raw HID, BLE NUS).
Supported hardware: nice!nano · Tenstar Robot Pro Micro nRF52840 (any board that exposes raw nRF52840 GPIO numbers with an identity pin map).
| Feature | Detail |
|---|---|
| Keys | 4×4 matrix, 8 ms per-key debounce |
| Profiles | 7 profiles, switchable via button |
| LEDs | 16× WS2812B — 5 modes per profile (per-key, solid, rainbow, reactive, breathe) |
| Indicator LEDs | 4 LEDs: profile binary display, battery bar, pairing flash, charging sweep |
| BLE HID | Keyboard + consumer-control, multi-bond |
| USB HID | Keyboard + consumer-control (always active, no pairing needed) |
| BLE Battery Service | Standard 0x180F, updated every 60 s |
| Config transports | USB CDC Serial · USB Raw HID · BLE NUS (same JSON protocol on all three) |
| Storage | LittleFS on internal flash; auto-format on corruption |
| Deep sleep | System OFF (~2 µA) after 15 min idle (battery only); any key or button wakes |
| LED dimming | WS2812B dims to off after 5 min idle, restores on keypress |
| Battery monitor | ADC on P0.04, 12-bit, 8-sample average, non-linear LiPo curve |
| Charging detect | Heuristic: USB connected + voltage rising over 2 min |
| Factory reset | Hold button ≥ 5 s at boot |
| # | Name | Layout |
|---|---|---|
| 1 | Hex | 0-9, A-F hex keypad (orange row + green keys) |
| 2 | Numpad | Standard numpad layout (7-8-9-/ · 4-5-6-* · 1-2-3-· · 0-.-Enter-+) |
| 3 | Media | Play/Pause, Next, Prev, Stop, Vol+, Vol−, Mute |
| 4-7 | Profile 4-7 | Empty (distinct LED colours) |
Uses raw nRF52840 GPIO numbers (P0.xx = 0–31, P1.xx = 32+).
Compiled with board target adafruit:nrf52:pca10056 (identity g_ADigitalPinMap).
| GPIO # | nRF52840 | nice!nano label | Function |
|---|---|---|---|
| 11 | P0.11 | D7 | Row 0 (open-drain scan, ext. 10 kΩ pull-up) |
| 32 | P1.00 | D6 | Row 1 (open-drain scan, ext. 10 kΩ pull-up) |
| 24 | P0.24 | D5 | Row 2 (open-drain scan, ext. 10 kΩ pull-up) |
| 22 | P0.22 | D4 | Row 3 (open-drain scan, ext. 10 kΩ pull-up) |
| 20 | P0.20 | D3 | Col 0 (driven LOW during scan) |
| 17 | P0.17 | D2 | Col 1 (driven LOW during scan) |
| 8 | P0.08 | D0 | Col 2 (driven LOW during scan) |
| 6 | P0.06 | D1 | Col 3 (driven LOW during scan) |
| 10 | P0.10 | D8 | WS2812B DIN |
| 13 | P0.13 | — | VCC enable (set HIGH at boot to power WS2812B) |
| 9 | P0.09 | D9 | Indicator LED bit 0 (LSB) |
| 2 | P0.02 | D10 | Indicator LED bit 1 |
| 29 | P0.29 | D15 | Indicator LED bit 2 |
| 31 | P0.31 | D14 | Indicator LED bit 3 (MSB) |
| 4 | P0.04 | A0 | Battery ADC (AIN2, voltage divider ÷2) |
| 36 | P1.04 | A1 | Profile button (INPUT_PULLUP, active LOW) |
Diode direction: column → row (anode on column wire, cathode on row wire).
Rows are scanned open-drain: each row pin switches between OUTPUT LOW (scan active)
and INPUT (high-Z idle) to avoid bus conflicts. Column pins use INPUT_PULLUP
(internal ~13 kΩ) plus external 10 kΩ pull-ups (combined ~5.6 kΩ).
A pressed key forward-biases the diode and pulls the column pin LOW.
Key row 0 (top) ← pixels 15 14 13 12
Key row 1 → pixels 8 9 10 11
Key row 2 ← pixels 7 6 5 4
Key row 3 (bot) → pixels 0 1 2 3
Strip connector attaches at physical bottom-left.
| Pattern | Profile |
|---|---|
| ○○○● | 1 |
| ○○●○ | 2 |
| ○○●● | 3 |
| ○●○○ | 4 |
| ○●○● | 5 |
| ○●●○ | 6 |
| ○●●● | 7 |
LED 0 (LSB) slow-pulses when battery ≤ 10 %. All 4 LEDs flash ~8 Hz in pairing mode. Double-tap shows a 3-second battery bar (1–4 LEDs filled).
| Gesture | Action |
|---|---|
| Single tap | Cycle to next profile (1 → 2 → … → 7 → 1) |
| Double tap | Show battery level for 3 s |
| Hold 3 s | Enter BLE pairing mode (clears bonds, restarts advertising) |
| Release after long hold | Exit pairing mode |
| Tool / Library | Version | Notes |
|---|---|---|
| arduino-cli or Arduino IDE 2 | any | |
| Adafruit nRF52 BSP | ≥ 1.6 (1.7.0 tested) | includes Bluefruit + TinyUSB |
| ArduinoJson | ≥ 7.0 | |
| Adafruit NeoPixel | any |
BSP board manager URL:
https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
# Compile
arduino-cli compile --fqbn adafruit:nrf52:pca10056 --output-dir build HexPad-nRF52840.ino
# Convert to UF2
python3 /path/to/uf2conv.py \
--family 0xADA52840 --base 0x26000 \
build/HexPad-nRF52840.ino.hex \
-o build/HexPad-nRF52840.uf2uf2conv.py ships with the Adafruit nRF52 BSP at:
~/.arduino15/packages/adafruit/hardware/nrf52/<version>/tools/uf2conv/uf2conv.py
Board target must be adafruit:nrf52:pca10056 (Nordic nRF52840 DK). This board
definition has an identity g_ADigitalPinMap so raw GPIO numbers in code map directly
to physical pins. Using adafruit:nrf52:feather52840 will map all GPIO numbers wrong.
Enter the bootloader (double-tap reset — LED pulses). A mass-storage drive appears.
Copy HexPad-nRF52840.uf2 onto it; device reboots automatically.
Hold the profile button while plugging in USB (or at power-on). Keep holding for 5 seconds until the indicator LEDs blink 3 times. All profiles reset to defaults and the active profile is saved.
| Field | Value |
|---|---|
| VID / PID | 0x1209 / 0xDD16 |
| Manufacturer | DailyDuino |
| Product | DailyDuino Hexpad 16 |
| HID reports | Report ID 1: keyboard · ID 2: consumer · ID 3: vendor config (32 B) |
| Field | Value |
|---|---|
| Device name | DailyDuino Hexpad 16 |
| TX power | +4 dBm |
| BLE ATT MTU | 247 bytes (BANDWIDTH_MAX, HVN queue = 3) |
| Advertising | Fast 30 s, then slow; restarts on disconnect |
| NUS TXD | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E (notify) |
| NUS RXD | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E (write) |
| Battery Service | UUID 0x180F |
All three transports speak the same line-delimited JSON protocol (newline \n as
message terminator).
| Transport | How to connect |
|---|---|
| USB CDC Serial | 115200 baud — any serial terminal |
| USB Raw HID | Report ID 3, 32-byte reports — vendor 0xFF60, usage 0x61 |
| BLE NUS | Write to RXD characteristic; responses arrive on TXD notifications |
| Command | Required fields | Response |
|---|---|---|
info |
— | Device name, firmware, proto version, profile count, battery % |
get_profile |
profile (0-6, optional) |
Full profile: name, LED config, all 16 key bindings, per-key LED colors |
set_key |
profile, key (0-15), action object |
{"ok":true} |
set_led |
profile, any of: enabled, brightness, mode, color, anim_speed |
{"ok":true} |
set_led_color |
profile, key (0-15), rgb ([r,g,b]) |
{"ok":true} |
set_profile_name |
profile, name |
{"ok":true} |
switch_profile |
profile (0-6) |
{"ok":true} |
get_battery |
— | pct, mv, charging, usb |
save |
— | Persists current config to LittleFS |
reset_defaults |
— | Restores factory defaults and saves |
sleep |
— | Enters System OFF deep sleep |
All requests may include an "id" field; the value is echoed back in the response.
| Type | Extra fields | Description |
|---|---|---|
none |
— | No action |
key |
keycode, mods |
Single HID keycode with modifier mask |
combo |
keys (array), mods |
Multiple keycodes sent simultaneously |
text |
text (string) |
Types a string via ASCII→HID lookup |
consumer |
consumer (uint16) |
HID consumer-control usage code |
macro |
steps (array) |
Sequence of press/release/tap/delay steps |
Modifier mask bits: LCTRL=0x01 LSHIFT=0x02 LALT=0x04 LGUI=0x08 RCTRL=0x10 RSHIFT=0x20 RALT=0x40 RGUI=0x80
| Value | Name | Description |
|---|---|---|
| 0 | per-key | Individual RGB per key stored in profile |
| 1 | solid | All keys one colour |
| 2 | rainbow | Hue cycle animation across all keys |
| 3 | reactive | Keys flash white on press, fade to base colour |
| 4 | breathe | All keys pulse in/out at anim_speed rate |
→ {"cmd":"info"}
← {"ok":true,"name":"DailyDuino Hexpad 16","firmware":"3.0.0-nrf","proto":2,"profiles":7,"keys":16,"leds":16,"profile":0,"battery":85}
→ {"cmd":"get_profile","profile":0}
← {"ok":true,"profile":0,"data":{"name":"Hex","led":{...},"keys":[...],"led_colors":[...]}}
→ {"cmd":"set_key","profile":0,"key":0,"action":{"type":"key","keycode":4,"mods":0}}
← {"ok":true}
→ {"cmd":"save"}
← {"ok":true}
| Threshold | Value |
|---|---|
| Deep sleep trigger | 15 min idle, battery only (USB connected = no sleep) |
| LED dim trigger | 5 min idle |
| Battery full | 4200 mV |
| Battery empty | 3000 mV |
| Low battery warning | ≤ 3300 mV (~10 %) |
| Wake sources | Any column pin LOW or button pin LOW (GPIO SENSE) |
| File | Purpose |
|---|---|
HexPad-nRF52840.ino |
Main sketch — setup, loop, key + button event dispatch |
Config.h / .cpp |
Pin constants, data structures, LittleFS persistence, factory defaults |
Matrix.h / .cpp |
4×4 scan with open-drain rows, 8 ms debounce |
HIDOutput.h / .cpp |
BLE HID + USB HID reports, BLE NUS, USB Raw HID, held-key tracking |
LedControl.h / .cpp |
WS2812B driver, 5 animation modes, snake LED map, idle dimming |
Indicator.h / .cpp |
4-LED indicator: profile, battery, pairing, charging, low-batt |
Button.h / .cpp |
Debounced single / double / long-press state machine |
Power.h / .cpp |
Battery ADC, BLE BAS, charging heuristic, deep sleep |
Protocol.h / .cpp |
Transport-agnostic JSON command dispatcher |
INTERFACE.md |
Config protocol reference for client developers |