Files
firmware/variants
James RichandClaude Fable 5.1 9f5436300c Keep the mesh-peer advertisement alive: a CCCD write is not "arrived on instance 2"
On the bench the Pixel's GATT rx sat at zero from launch and across every node
rebuild while ble-adv, lora and udp all flowed, and its GATT tx was "accepted by
a peer". The peer was the iPad: logcat shows the Pixel's central connecting only
to random-address Apple peers and never to a public-address NimBLE device. The
V3's connectionless broadcast (instance 1) was on the air; its connectable
mesh-peer advertisement (instance 2) was not, so the Pixel's filtered scan had
nothing to find. The phone-API set (instance 0) had gone dark with it.

Root cause is a conflation in ESP32BLEGattMesh: onSubscribe() called
addLink(conn, true), flagging any link that wrote the mesh CCCD as viaMeshAdv.
That flag does two jobs - it is the slot count startAdvertising() gates on
(BLE_GATT_MESH_MAX_LINKS is 1) and it is the early return in
NimbleBluetoothServerCallback::onDisconnect that skips the phone-API session
reset and re-arm. Both sets advertise the same public address, so a central that
finds instance 2 in a scan can land its CONNECT_IND on instance 0's ADV_IND
(the earlier "onSubscribe never fired for a central that connected via the
phone-API instance" commit saw exactly that). Such a link, once subscribed,
counted against the mesh slot; when a second central (the iPad) took instance 2
the slot was full, and when the first link dropped on a monitor rebuild the early
return left instance 0 unarmed too - "peer slots full, not advertising" at
LOG_DEBUG was the only trace. Nothing recovered until reboot.

- onSubscribe() now calls addLink(conn, false): a CCCD write sets `subscribed`
  (still the only thing that makes a link a notify target) and leaves
  `viaMeshAdv` exactly as the instance-2 CONNECT event set it, which is the one
  meaning it can safely carry.
- onDisconnect() re-arms the mesh-peer advertisement for every dropped link and
  lets startAdvertising() decide, since a phone-API link dropping is what frees
  the connection slot a deferred start was waiting for. startAdvertising() skips
  a set that ble_gap_ext_adv_active() says is already running, so the re-arm
  never interrupts a live advertisement.
- The slot-full line is LOG_INFO and names the conn handle holding the slot; an
  ENOMEM start (every connection slot taken) is LOG_WARN "deferred", and every
  ext-adv rc carries a hint: 6 = no free connection, 519 = controller activity
  budget (CONFIG_BT_CTRL_BLE_MAX_ACT).
- Subscribe and disconnect lines say which advertisement the link arrived on.

BLE_GATT_MESH_MAX_LINKS stays 1: two dual-role phones still race for the single
mesh slot by design, and the header now says so and what to do about it (run the
second phone PERIPHERAL_ONLY, or raise the slot with MAX_CONNECTIONS/MAX_ACT).
The esp32-common.ini note records that the duplicated CONFIG_BT_NIMBLE_MAX_
CONNECTIONS / MAX_ACT keys resolve to [ble_mesh_esp32]'s values - verified in
the generated sdkconfig.defaults - and that comment lines are stripped before
the custom_sdkconfig hash, so the note itself does not trigger a libs rebuild.

Built for heltec-v3 (pio run, 90 s, no IDF-lib recompile). Not yet flashed or
verified on the bench.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 10:25:38 -05:00
..