---
title: "DIY keyboard, part 3: rebinding keys wirelessly over BLE"
author: "Veselin Slavchev"
date: 2026-05-05
tags: ["bluetooth", "arduino", "DIY"]
description: "How I added a second BLE GATT service to my ESP32 keyboard so keys can be rebound wirelessly from a Web Bluetooth app, with layouts saved in flash."
url: https://vsslavchev.com/blog/diy-keyboard-ble-rebinding/
---

# DIY keyboard, part 3: rebinding keys wirelessly over BLE

**In short:** My ESP32 keyboard now has a second BLE GATT service next to the standard HID one. A web app connects to it with Web Bluetooth, reads the layout, and writes a new one in small chunks; the keyboard checks it and saves it in flash (NVS), and only a physical four-key combo lets a new device pair.

Previous: [part 2, scanning the key matrix and fixing ghosting](https://vsslavchev.com/blog/diy-keyboard-key-matrix/).

If I wanted to rebind a key on my custom keyboard I needed to reprogram it: open the keyboard case, connect with a cable, edit the firmware source, compile, and flash the board. But when I made two keyboards for friends they wanted to rebind some keys too, and that was a problem. I imagined having a nice UI for the task and making it even a fun experience. There is something special in taking care of your comfort and knowing that a minor improvement will pay off in the long run.

The solution also needed to be wireless and since the keyboard was already using BLE I only had to extend it. But first I had to learn how it really works. The way devices find each other is through advertising: a peripheral (the keyboard) repeatedly broadcasts small packets saying it exists. A central (the PC) scans for these packets and can initiate a connection. Once connected, both sides stop advertising and communicate through a client-server model called GATT (Generic Attribute Profile).

GATT structures data as characteristics contained inside services. A service groups related functionality, and each characteristic in it is a value that can be read, written, or subscribed to for notifications. A keyboard exposes a standard HID (Human Interface Device) service. At connect time the PC reads a report descriptor that tells it the shape of the data — in this case an 8-byte struct: one byte for modifier keys (Ctrl, Shift, Alt, GUI), one reserved byte, and six slots for simultaneously held key HID codes. Every time a key is pressed or released the keyboard sends an updated report via a GATT notification and the PC translates it into key events.

The ESP32 microcontroller has BLE built in and the Arduino BLE library provides all the GATT plumbing. The keyboard object sets up a `BLEHIDDevice` with the standard keyboard report descriptor. When a key is pressed the firmware resolves the physical position through a matrix scan, looks up the keycode in the active layout level, and calls `keyboard.press(keycode)` which updates the 8-byte report and notifies the host.

The key rebinding part is a second BLE service sitting alongside the HID one. It has three characteristics:

- A readable characteristic that returns the current layout as 192 bytes (2 layers × 4 rows × 12 keys × 2 bytes per key — one for the keycode, one for an optional modifier).
- A writable data characteristic that accepts chunks of new layout bytes, so the whole layout can be sent in small BLE packets.
- A writable control characteristic that accepts commands: enter config mode (pauses HID output), commit (applies and saves the received layout), or exit (cancels and resumes normal typing).

Another technology I wanted to learn about was the NVS (Non-Volatile Storage). The layout is persisted in the ESP32's NVS, a key-value flash store that survives power cycles. So on boot the firmware reads the stored layout and validates it with a simple XOR checksum before applying it. If the checksum fails — for example after a firmware update that changes the layout size — the default layout is used instead.

Security is worth thinking about for this new keyboard setup because we don't want someone to change our keys. Normally BLE keyboards are open to pairing from any device in range. For our case: to pair a new device you have to physically hold a four-key combo on the leftmost column at the same time. While the combo is held the firmware opens advertising to all, the LEDs stay on as a visual indicator, and keypresses from the combo keys are suppressed so nothing reaches the PC. A reconnect from a paired device works normally without any combo.

The web app that handles rebinding connects using the Web Bluetooth API, which lets a browser act as a BLE central directly — no native app or driver needed. The browser's `navigator.bluetooth.requestDevice()` shows the system Bluetooth picker filtered to devices advertising the config service UUID. After connecting, the app reads the full current layout from the keyboard, renders it as a 4×12 grid with one button per key, and lets you click any key to open a picker with all available keys. When you hit save the app sends an enter-config command, streams the 192-byte layout in 18-byte chunks, and finishes with a commit command. The keyboard applies the new mapping immediately and writes it to flash. The web app only works in Chromium-based browsers since Firefox and Safari have not implemented the Web Bluetooth spec.

The result is a keyboard that can be configured wirelessly with a webapp: [check it out!](https://keyboard.vsslavchev.com/)

References:

- Web Bluetooth Community Group. "Web Bluetooth specification." Accessed 5 May, 2026. [https://webbluetoothcg.github.io/web-bluetooth/](https://webbluetoothcg.github.io/web-bluetooth/)
- Espressif. "ESP32 Non-Volatile Storage Library." Accessed 5 May, 2026. [https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_flash.html](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_flash.html)
