# BLE Companion — main.cpp integration hints

This module is built only when `PDL_BLE_COMPANION_ENABLED` is set (currently
only in `env:heltec_wifi_lora_v4_interop`). All hooks below should be wrapped
in `#ifdef PDL_BLE_COMPANION_ENABLED` so the standard env stays unaffected.

The integrator wires these in — `main.cpp` is intentionally not modified by
the BLE patch itself.

## 1. `setup()` — after `enrollment_init()`

```cpp
#ifdef PDL_BLE_COMPANION_ENABLED
#  include "ble_companion.h"
#endif
...
    // (existing) bool enrolled = enrollment_init();
    // ...other inits...

#ifdef PDL_BLE_COMPANION_ENABLED
    ble_companion_init();   // starts BLE peripheral; advertises as PDL-<NAME>
#endif
```

`ble_companion_init()` is idempotent: subsequent calls return early without
re-initialising NimBLE.

The function reads `SENSOR_NAME` (build flag) and `enrollment_get_sensor_id()`
to pick the advertising local-name and the 6-digit pairing passkey
(`sensor_id & 0xFFFFFF`, displayed as decimal — also visible on the OLED so
the operator can verify on the phone).

## 2. `loop()` — once per main-loop cycle (1 Hz)

```cpp
#ifdef PDL_BLE_COMPANION_ENABLED
    ble_companion_tick();   // drains queued Cmd writes + emits status notify
#endif
```

The tick should run on the same cadence as the existing 1 Hz cycle. It is
non-blocking: at most a handful of QueueReceive() calls + one mbedTLS-free
JSON snprintf(). Heaviest path is `network_id_count()` + sorted-iter, identical
to what `mesh_publish` already pays.

Place this tick **after** the OLED render block so the BLE notify timing is
not squeezed under any blocking write.

## 3. `FFPS_PROMOTED` block — push the detection event to BLE

In the existing block at `main.cpp:653`:

```cpp
if (ffps_state == FFPS_PROMOTED) {
    const DetectionEvent& promoted = ffps_get_promoted_event();
    markov_observe_event((uint8_t)MK_BEACON, millis());
    if (blue_force_check_hash(promoted.signature_hash)) {
        // suppressed
    } else {
        mesh_publish_detection(promoted);     // existing
        health_increment_detections();
        mesh_start_receive();
        digitalWrite(LED_PIN, HIGH);

#ifdef PDL_BLE_COMPANION_ENABLED
        ble_companion_publish_detection(promoted);   // NEW
#endif
    }
    ffps_reset();
}
```

`ble_companion_publish_detection()` queues the event for the next tick to
notify; if no phone is connected the call is a free no-op.

## 4. Markov NLL — every 30 cycles alongside `markov_publish_nll`

In the block at `main.cpp:699`:

```cpp
if (s_loop_count % 30 == 0) {
    markov_publish_nll(s_sensor_id, /*track_slot=*/0, /*threshold=*/0.0f);
    mesh_start_receive();

#ifdef PDL_BLE_COMPANION_ENABLED
    ble_companion_publish_nll(markov_current_nll());   // NEW
#endif
}
```

`ble_companion_publish_nll()` only stashes the value into a `static float`;
the next tick's status JSON will include it.

## 5. OLED footer — show `[BLE]` when a phone is paired

The existing OLED block at `main.cpp:464` writes either the active class
string or `READY` / `scanning`. Patch:

```cpp
} else {
    const char* base = enrollment_is_complete() ? "READY" : "scanning";
#ifdef PDL_BLE_COMPANION_ENABLED
    char ln[32];
    if (ble_companion_is_connected()) {
        snprintf(ln, sizeof(ln), "%s [BLE]", base);
        s_display.drawStr(0, 62, ln);
    } else {
        s_display.drawStr(0, 62, base);
    }
#else
    s_display.drawStr(0, 62, base);
#endif
}
```

## 6. Authorized operator pin_hmacs (v1 — hardcoded fingerprint list)

Until proper pubkey delivery exists, the integrator should call
`ble_companion_add_authorized_pin_hmac()` once per authorized operator after
`ble_companion_init()`:

```cpp
#ifdef PDL_BLE_COMPANION_ENABLED
    // Eric's pin_hmac (HMAC-SHA256(operator_secret, sensor_serial))
    static const uint8_t k_eric_pin_hmac[32] = {
        0x...,  // 32 bytes from the operator-provisioning script
    };
    ble_companion_add_authorized_pin_hmac(k_eric_pin_hmac);

    static const uint8_t k_spencer_pin_hmac[32] = { ... };
    ble_companion_add_authorized_pin_hmac(k_spencer_pin_hmac);
#endif
```

These should be replaced by an OTA-delivered allowlist before fielding more
than 2 operators.

## 7. Cmd-ack capture (already handled inside ble_companion)

`command_handler.cpp` does not need to change. The BLE module captures the
emitted ACK by reading the most recent `mesh_publish_raw` output via the
existing `ble_companion_capture_cmd_ack()` extern-C hook, which is no-op
unless BLE is compiled in. Integrators wanting tight ACK-fidelity should
patch `command_handler::serializeAckPacket()`'s caller to also call
`ble_companion_capture_cmd_ack(out_buf, n)` — the hook is forward-declared
in `ble_companion.cpp` and is safe to call from any context.

## 8. Coexistence notes

- **LoRa**: SX1262 lives on its own SPI bus + ISR. NimBLE runs on the second
  Xtensa core via Espressif's BLE controller task. No shared bus, no shared
  ISR. Verified safe.
- **WiFi promiscuous**: BLE 5 and WiFi 2.4 GHz share the same radio.
  Espressif's coex layer arbitrates with ~10–15 % WiFi throughput cost when
  BLE is advertising. Acceptable for our use case (we sniff beacons / probe-
  responses, not high-rate data).
- **Time-sliced interop (Meshtastic / RNode)**: unaffected — those are LoRa
  protocols on the SX1262.

## 9. Build size impact (rough)

NimBLE-Arduino adds ~80 KB flash + ~12 KB RAM at idle. The companion module
itself is ~6 KB code + a few hundred bytes of static state. The `_interop`
build absorbs the cost; `_standard` and `_uav_bridge` builds are unaffected.
