Bluetooth Pairing System Architecture
Comprehensive code analysis of in-person BLE discovery, RSSI signal smoothing, 512-byte GATT MTU limits, group vs 1-on-1 payloads, and Time Wilt lifetime flags.
Interactive BLE Radar & GATT MTU Payload Visualizer
Use the interactive simulation below to observe live RSSI signal smoothing, device cache grace timers, and the 512-byte GATT ATT MTU payload boundary in action.
user_id key removed from response to prevent >512B GATT truncation.
1. Dual BLE Stack Architecture
Wiltkey implements in-person, proximity-gated pairing over Bluetooth Low Energy. Because client devices must act simultaneously as a Central (Scanner / GATT Client) and a Peripheral (Advertiser / GATT Server), the app orchestrates two underlying Flutter plugins within BlePairingManager (located at lib/features/proximity/controllers/ble_pairing_manager.dart):
flutter_blue_plus: Handles scanning for nearby devices, adapter state monitoring, MTU requests (requestMtu(512)), and GATT Client connections.ble_peripheral: Runs the local GATT Server, advertises custom service UUIDs, and processes inbound write/read requests.
Advertising Naming Scheme
A device advertises a short local name encoding its identity and mode:
- 1-on-1 Mode:
WK:<ShortName>:<ShortID>(e.g.,WK:Alice:A1B2) - Group Invite Mode:
WKG:<GroupName>:<ShortID>(e.g.,WKG:Devs:C3D4)
2. RSSI Signal Smoothing, Grace Timers & Hardware Compatibility
Bluetooth RSSI readings fluctuate rapidly due to multipath interference and RF noise. Without filtering, devices on a proximity radar would continuously jump, flicker, or disappear between advertising packets.
Exponential Moving Average Smoothing
When a scan result arrives, BlePairingManager applies an exponential moving average (EMA) filter to smooth the RSSI before updating the UI state:
Dual Temporal Grace Windows
To prevent devices from blinking out during irregular advertising intervals on older phones, _deviceCache maintains two temporal grace windows:
Earlier development builds used a strict physical touch threshold of
-35 dBm. However, real-world testing revealed that many older and lower-end Android smartphones possess insensitive BLE antennas that rarely register signals stronger than -50 dBm even when touching. Rather than restricting pairing to modern flagship devices for a gimmick, the team relaxed the proximity gate to -85 dBm (kNearRssi = -85). Because BLE advertising range is inherently short (a few meters), -85 dBm dramatically increases device support while preserving physical proximity verification.
3. Exact Handshake Payload Schemas
Pairing data travels as UTF-8 encoded JSON across the GATT characteristic 098ed89c-5b2d-4f70-91f6-7ccff798f5b2.
A. 1-on-1 Byte-Budget Pairing Request
B. 1-on-1 Time Wilt Pairing Request
When pairing in Time Wilt mode, the initiator sets "tw": 1 and embeds the pre-negotiated absolute Unix expiry timestamp ("twx" in milliseconds). No pad file is created; the seed is stored locally for on-demand expansion.
C. Group Invite Response (Time Wilt Mode)
4. The 512-Byte GATT MTU Ceiling & Truncation Guard
BLE GATT read operations enforce a maximum ATT MTU payload size of 512 bytes. If a response exceeds 512 bytes, the operating system silently truncates the byte array. The client receives incomplete JSON, causing a parse failure and stalling the handshake.
When adding Time Wilt group parameters (e.g.
group_wilt_lifetime, group_seed_encrypted, lane_size, total_size, slot_index), the JSON payload size reached ~560 bytes. The team discovered that the joiner client never actually reads user_id from the invite response (it derives the host's ID as SHA-256(pubkey)).To resolve this, line 496 of
ble_pairing_manager.dart explicitly strips user_id from group invite responses:
5. Slot Allocation & Re-Meeting Mechanics
When a host invites or re-meets a group member over BLE:
- The host calls
GroupDatabase.instance.getLaneByMember(groupId, peerId). - Re-Meeting Existing Member (Time Wilt renewal or byte refill): If the peer already has an assigned slot, the host reuses that existing
slot_indexrather than allocating a new one (preventing duplicate slot consumption). - First-Time Joiner: If no lane exists, the host queries
getEmptyLanes(groupId)and assigns the lowest available slot index.
6. Function-to-Function Code Execution Reference
| Function Name | File Location | Description & Key Invariants |
|---|---|---|
initializeBle() |
ble_pairing_manager.dart:L192 |
Initializes permissions, hooks adapter listener, starts GATT server, and triggers scan loop. |
initGattServer() |
ble_pairing_manager.dart:L256 |
Registers service 4f7091f6... and characteristic 098ed89c... with read/write/notify properties. |
startScanningFlow() |
ble_pairing_manager.dart:L560 |
Runs 45s scan, updates EMA smoothed RSSI, filters by `WK:`/`WKG:`, and manages `_deviceCache`. |
_handleIncomingPairRequest() |
ble_pairing_manager.dart:L384 |
Parses GATT write JSON, extracts `tw`/`twx` flags, and invokes `onIncomingRequest` UI confirmation. |
respondToPairRequest() |
ble_pairing_manager.dart:L419 |
Derives pairwise seed `sha256(pubA + pubB)`, assigns slot, removes `user_id`, and updates GATT response bytes. |
_finalizeHandshake() |
ble_pairing_manager.dart:L1058 |
Writes 1-on-1 OTP pad or stores Time Wilt / group seed and expiry in SQLite DB. |