A BLE GATT server for ESP32 (Arduino framework) that exposes a dynamic form schema and receives submissions as JSON — pairs with a Web Bluetooth client that reads the schema, renders the matching form, and writes back the submitted values. No app install, no WiFi network required upfront.
Web Bluetooth client
│
▼
ESP32 ── BLE GATT Server
├── Field N schema (READ) → one field's definition, sent as JSON
├── Field N value (WRITE) → that field's submitted value, sent as JSON
├── Commit (WRITE) → triggers assembling + dispatching the submission
└── Status (READ / NOTIFY) → ACK/NACK after submission
GattFormData (bundled in this same package) wraps the submitted JSON
payload with typed getters. It has no BLE dependency, so it's covered by
fast native unit tests — see test/test_native.
#include <GattForm.h>
GattForm form;
void setup() {
form.setName("MyDevice")
.addText("ssid", "SSID")
.addPassword("pass", "Password")
.addRange("bright", "Brightness", 0, 255)
.onSubmit([](GattFormData& d) {
Serial.println(d.getString("ssid"));
form.ack();
})
.begin();
}
void loop() {}Add this repo to your project's platformio.ini:
lib_deps =
https://github.com/virgileMaon/GattForm.git#v0.1.0Pin to a tag (as above) rather than tracking the default branch, so an upstream change never silently breaks your build.
docs/index.html is a generic Web Bluetooth client for any
GattForm device — it doesn't hardcode any field, it just reads the Schema
characteristic and renders whatever fields the firmware defined
(addText, addRange, addSelect, ...). Point it at any board running this
library and it works, no per-project customization needed.
Requirements: Google Chrome or another browser with Web Bluetooth support (Firefox and Safari don't have it).
To try it locally, just open the file in Chrome — Web Bluetooth works from
file://. A hosted copy is also kept live at
https://virgilemaon.github.io/GattForm/, so most projects using this
library don't need to publish their own.
It talks to the device using the fixed UUIDs declared in
src/GattForm.h (SERVICE_UUID, COMMIT_UUID, STATUS_UUID,
plus a per-field schema/value characteristic pair derived from
FIELD_SCHEMA_UUID_BASE/FIELD_VALUE_UUID_BASE) — any firmware built with
this library exposes exactly those, so the same page works across every
project using it, with no changes.
BLE characteristics are unencrypted by default — anyone within radio range
running a BLE sniffer can passively capture whatever's written to them,
including a password field's submitted value. Call .requireEncryption()
before .begin() to require BLE bonding on every characteristic (schema,
value, commit, status):
form.setName("MyDevice")
.addPassword("pass", "Password")
.requireEncryption()
.onSubmit([](GattFormData& d) { /* ... */ form.ack(); })
.begin();What it does and doesn't protect against:
- Stops passive eavesdropping — a nearby sniffer only ever sees encrypted packets, never the plaintext values.
- Doesn't stop an active man-in-the-middle during the very first pairing. Pairing uses "Just Works" (no PIN prompt) — the only mode a generic board without a display or keyboard can offer — so there's no out-of-band step to authenticate that first pairing against.
Trade-offs to know before turning it on:
- The web client's first connection now triggers an OS-level Bluetooth pairing prompt (Windows/Android's own dialog, outside the page's control) instead of the plain "click to connect" of the unencrypted default. Reconnecting afterward doesn't prompt again — the bond is remembered on both sides.
- The ESP32 stores bonded-device keys in NVS with limited slots — a device paired with many different phones over its lifetime (e.g. a shared/public provisioning kiosk) can run out and need its bonds cleared.
- Neither test env can exercise this:
pio test -e nativehas no real BLE stack, andpio test -e esp32devwould need a peer that actually pairs. See examples/demo_encryption for a manual check instead. - Interop varies by OS/BLE stack — pairing is handled by the underlying BLE
library, not this one, and its own logging never says whether pairing
actually succeeded.
requireEncryption()registers a security callback that does: watch Serial forGattForm: BLE pairing succeeded.orGattForm: BLE pairing FAILED (HCI reason 0x..)when debugging a connection that pairs but then won't connect. - Tested on Android and Windows 11 — works on both, but pairing can be a bit finicky on Windows (may take a retry or two on first pairing).
# Fast unit tests (JSON parsing logic), no hardware required
pio test -e native
# BLE lifecycle tests, run on an actual ESP32 board
pio test -e esp32devCI (.github/workflows/tests.yml) runs the native
unit tests on every push/PR, since those need no hardware. The esp32dev
tests need a real board to execute (Unity results come back over serial), so
CI only compiles that env and both examples/ projects instead — enough to
catch a build break, not a substitute for actually running
pio test -e esp32dev on hardware before a release.
examples/demo_defaults is a self-contained
PlatformIO project (its own platformio.ini, depending on this repo via
symlink://../..) that sets a default value on every field type, so you can
eyeball that docs/index.html pre-fills them correctly —
this isn't covered by the automated tests since it's a visual/UX check on
the actual web form.
cd examples/demo_defaults
pio run -t upload --upload-port COMx # flash the demo to your boardThen open docs/index.html in Chrome, connect to "GattForm Demo", and
check the form against the expected values (also logged over Serial at
115200 baud on boot):
| Field | Expected pre-filled value |
|---|---|
| SSID (text) | MyWifi |
| Password | (empty — no default, by design) |
user@example.com |
|
| Enable feature (bool) | checked |
| Count (number) | 4 |
| Brightness (range) | 128 |
| Mode (select) | normal |
examples/demo_encryption is the same kind of
self-contained project, with .requireEncryption() turned on — see its
src/main.cpp for what to expect (an OS-level pairing prompt on first
connect) and how to confirm reads/writes are actually rejected without it.
src/GattForm.h/.cpp BLE GATT server: builds the form schema, handles submissions
src/GattFormData.h/.cpp JSON payload wrapper, hardware-independent
docs/index.html Generic Web Bluetooth client (can be served via GitHub Pages)
test/test_native/ Unit tests, run on the host
test/test_embedded/ Integration tests, run on an ESP32 board
examples/demo_defaults/ Manual/visual check for field default values (see Testing)
examples/demo_encryption/ Manual check for requireEncryption() (see Testing)