⚡ CODE DEEP DIVE BlePairingManager Analysis

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.

BLE Scanning Radar & RSSI Smoothing STATUS: SCANNING
GATT 512-Byte Payload MTU Bar 434 / 512 BYTES
84.7% MTU Utilized (SAFE < 512B)


                            
                            
✓ SAFEGUARD ACTIVE: 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.
// Core UUIDs & Constants (lib/features/proximity/controllers/ble_pairing_manager.dart) const int kNearRssi = -85; // RSSI proximity gate threshold const String kGattServiceUuid = '4f7091f6-7ccf-f798-f5b2-098ed89c5b2d'; const String kGattCharUuid = '098ed89c-5b2d-4f70-91f6-7ccff798f5b2';

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:

final double oldRssi = _smoothedRssi[id] ?? r.rssi.toDouble(); _smoothedRssi[id] = 0.3 * r.rssi + 0.7 * oldRssi; final int smoothedRssiValue = _smoothedRssi[id]?.round() ?? r.rssi;

Dual Temporal Grace Windows

To prevent devices from blinking out during irregular advertising intervals on older phones, _deviceCache maintains two temporal grace windows:

// Device lingers 8s after last packet before eviction static const Duration _seenGrace = Duration(seconds: 8); // Hysteresis window (12s) prevents toggling at the -85 dBm boundary static const Duration _inRangeGrace = Duration(seconds: 12);
ℹ️ HISTORICAL CONTEXT: SHIFT FROM -35 dBm TO -85 dBm
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

{ "type": "pairing_request", "device_name": "Pixel 8", "short_nick": "Alice", "profile_image": "data:image/png;base64,iVBORw0KG...", "user_id": "a1b2c3d4e5f6...", "pubkey": "04e5f6a7b8c9...", "buffer_bytes": 10485760 }

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.

{ "type": "pairing_request", "device_name": "Pixel 8", "short_nick": "Alice", "pubkey": "04e5f6a7b8c9...", "buffer_bytes": 0, "tw": 1, "twx": 1786118400000 }

C. Group Invite Response (Time Wilt Mode)

{ "status": "accepted", "pubkey": "04f7a8b9c0d1...", "pairing_type": "group_invite", "group_id": "9f8e7d6c5b4a...", "group_seed_encrypted": "e1f2a3b4c5d6...", "lane_size": 1048576, "total_size": 10485760, "slot_index": 2, "group_name": "Dev Team", "host_name": "Bob's Phone", "host_short_nick": "Bob", "group_wilt_lifetime": 2592000 }

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.

🛑 CRITICAL SAFEGUARD: REMOVING REDUNDANT `user_id`
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:
// Drop user_id: joiner derives host id as sha256(pubkey). // Its ~78 bytes pushed Time Wilt invites over the 512B GATT limit. responseMap.remove("user_id");

5. Slot Allocation & Re-Meeting Mechanics

When a host invites or re-meets a group member over BLE:

  1. The host calls GroupDatabase.instance.getLaneByMember(groupId, peerId).
  2. Re-Meeting Existing Member (Time Wilt renewal or byte refill): If the peer already has an assigned slot, the host reuses that existing slot_index rather than allocating a new one (preventing duplicate slot consumption).
  3. 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.