Skip to content

Implement unique and configurable BLE Device ID (with sample GATT/advertising spec) for reliable connection filtering #112

@mariobehling

Description

@mariobehling

When multiple badges are nearby, they currently advertise with identical name/service/manufacturer data. Apps then “pick the first/strongest,” which is unreliable. We need a per-device unique ID that is readable and writable from the app, persists in NVM, and is visible in advertising for pre-connect filtering.

Requirements

  1. Unique Device ID
  • Stored in non-volatile memory (flash/NVS).
  • Default: derived from MCU unique ID (hashed/shortened) if unset.
  • App can read and change the ID over GATT.
  • Length: 6–16 bytes recommended (printable base32/base16 string or raw bytes).
  1. Advertising
  • Include the Device ID in either:

    • Manufacturer Specific Data (MSD), or
    • Service Data for the custom service (preferred).
  • Keep total ADV payload within 31 bytes (legacy ADV) or use extended ADV if available.

  1. Security (recommended)
  • Writing Device ID should require an ownership token or at least an encrypted link (LE Secure Connections).
  • Optional: simple provisioning step to set owner token once.

Proposed GATT Layout (Custom Service)

Custom Service UUID (128-bit):
b0ad0001-1a2b-4c3d-9e5f-112233445566 (example; generate final UUIDs in repo)

Characteristics:

  1. Device ID Characteristic

    • UUID: b0ad0002-1a2b-4c3d-9e5f-112233445566
    • Properties: READ | WRITE (optionally WRITE_WITHOUT_RESPONSE)
    • Max length: 16 bytes
    • Format: bytes or ASCII (e.g., Base32 “BM-ABCDE1”)
    • Persistence: write updates NVM; read returns current value
    • Security: require encryption and/or validated ownership token to WRITE
  2. Ownership Token (optional but recommended)

    • UUID: b0ad0003-1a2b-4c3d-9e5f-112233445566
    • Properties: WRITE
    • Usage: app writes a pre-shared or first-time provisioned token; firmware validates before allowing Device ID writes (token stored securely in NVM)
  3. Device Capabilities / Version (optional)

    • UUID: b0ad0004-1a2b-4c3d-9e5f-112233445566

    • Properties: READ

    • Payload example (TLV):

      • 0x01 FW major.minor.patch
      • 0x02 features bitmask (e.g., LED, e-paper, UART, etc.)

Note: Standard Device Information Service (0x180A) is read-only and not suitable for a writable Device ID; keep ours in a custom service.


Advertising Payload (example)

Option A – Service Data (recommended):

  • AD Type: Service UUID (16/128) + Service Data

  • Service UUID: b0ad0001-1a2b-4c3d-9e5f-112233445566

  • Service Data (up to ~24 bytes):

    • Byte 0: Version (e.g., 0x01)
    • Byte 1: Length of Device ID (N)
    • Bytes 2..(1+N): Device ID bytes (e.g., 8 bytes)
    • Optional: TxPower or flags

Option B – Manufacturer Specific Data:

  • Company ID: TBD (0xFFFF placeholder if none)
  • Layout: [ver][len][device_id…]

Keep device GAP name short (e.g., “BadgeMagic”) to save ADV space.


Example App Flow

  • First run (provisioning):

    1. Scan for ADV with our service UUID.
    2. Show list with Device ID suffix and RSSI.
    3. User selects device → connect → write Ownership Token (if used) → write friendly Device ID (e.g., “BM-LAB-01”).
    4. Save peripheral identifier (addr/UUID) for fast reconnect.
  • Next runs:

    • Try direct reconnect using saved identifier; fallback to filtered scan by service UUID + Device ID in ADV/Service Data.

Acceptance Criteria

  • Device advertises Service UUID and includes Device ID in Service Data or MSD.
  • GATT service with readable/writable Device ID characteristic implemented.
  • Writes persist in NVM and survive reset.
  • (If enabled) Ownership token gating write access is implemented and documented.
  • Sample code/API documented for Android, iOS, and Python (bleak).
  • Backward-compat: if no ID in NVM, firmware generates default from MCU UID.

Reference Snippets (pseudo-code)

Firmware (C-like pseudo for read/write + NVM):

#define DEV_ID_MAX 16
static uint8_t dev_id[DEV_ID_MAX];
static uint8_t dev_id_len;

void load_dev_id_from_nvm(void) {
    if (!nvm_read("dev_id", dev_id, &dev_id_len) || dev_id_len == 0) {
        // derive from MCU UID (hash/truncate)
        dev_id_len = 8;
        hw_read_unique_id(dev_id, dev_id_len);
        nvm_write("dev_id", dev_id, dev_id_len);
    }
}

int on_dev_id_read(uint8_t *out, uint8_t *out_len) {
    memcpy(out, dev_id, dev_id_len);
    *out_len = dev_id_len;
    return 0;
}

int on_dev_id_write(const uint8_t *in, uint8_t in_len) {
    if (!is_authorized()) return ERR_UNAUTHORIZED;
    if (in_len == 0 || in_len > DEV_ID_MAX) return ERR_LENGTH;
    memcpy(dev_id, in, in_len);
    dev_id_len = in_len;
    nvm_write("dev_id", dev_id, dev_id_len);
    update_adv_payload_with_dev_id(dev_id, dev_id_len);
    return 0;
}

Android (filter by service + parse Service Data ID)

// After scanning, read Service Data for our custom service UUID and match Device ID.
// If multiple matches, show list with last 4 bytes of ID + RSSI.

iOS (retrieve by saved identifier; fallback to filtered scan)

// Use CBPeripheral.identifier for reconnect; if not found, scan for service UUID
// and verify Device ID by reading Service Data or GATT characteristic.

Python (bleak)

# Discover -> filter by service uuid -> match Device ID from advertisement metadata
# Connect -> read/write Device ID characteristic as needed.

Open Questions

  • Finalize UUIDs (service and characteristics).
  • Choose ID format (raw bytes vs printable string).
  • Security level for writes: encryption only vs token-based auth.
  • Company ID for MSD (if we standardize on MSD).

Impact
This enables reliable multi-device operation at workshops and events, improves UX (clear identity/personalization), and lays groundwork for secure ownership and provisioning.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions