From 17e6941687bf6fab3872ac99a3452cb5ea8c604d Mon Sep 17 00:00:00 2001 From: PaulDWhite Date: Thu, 26 Mar 2026 20:19:16 -0400 Subject: [PATCH 1/4] Update, BLE, switching handling, disconnect and connect. --- src/sp140/ble/ble_core.cpp | 57 +++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 9ea844b..8549ae1 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -122,14 +122,28 @@ bool startAdvertising(NimBLEServer *server) { // Flutter app's `startScan()` filters for CONFIG_SERVICE_UUID. adv.addServiceUUID(NimBLEUUID(CONFIG_SERVICE_UUID)); + // Scan response: manufacturer data with pairing-mode flag so the Flutter app + // can hide non-pairable controllers from the connect list. + // Format: Espressif company ID (0x02E5 LE) + 1 flag byte. + NimBLEExtAdvertisement scanRsp(BLE_HCI_LE_PHY_1M, BLE_HCI_LE_PHY_1M); + scanRsp.setLegacyAdvertising(true); + scanRsp.setScannable(true); + const uint8_t mfrData[] = {0xE5, 0x02, + static_cast(allowOpenAdvertising ? 0x01 : 0x00)}; + scanRsp.setManufacturerData(mfrData, sizeof(mfrData)); + auto *advertising = server->getAdvertising(); advertising->stop(); advertising->removeAll(); const bool configured = advertising->setInstanceData(kExtAdvInstance, adv); + const bool scanRspConfigured = + configured ? advertising->setScanResponseData(kExtAdvInstance, scanRsp) + : false; const bool started = configured && advertising->start(kExtAdvInstance); USBSerial.printf( - "[BLE] Ext adv configure=%d start=%d mode=%s bonds=%u whitelist=%u\n", - configured, started, allowOpenAdvertising ? "OPEN" : "BONDED", + "[BLE] Ext adv cfg=%d scanRsp=%d start=%d mode=%s bonds=%u wl=%u\n", + configured, scanRspConfigured, started, + allowOpenAdvertising ? "OPEN" : "BONDED", static_cast(bondCount), static_cast(whiteListCount)); return started; #else @@ -159,6 +173,12 @@ bool startAdvertising(NimBLEServer *server) { advertising->setScanFilter(false, true); } + // Manufacturer data with pairing-mode flag (updated every restart). + // Espressif company ID (0x02E5 LE) + 1 flag byte. + const std::string mfrPayload = {'\xE5', '\x02', + static_cast(allowOpenAdvertising ? 0x01 : 0x00)}; + advertising->setManufacturerData(mfrPayload); + const bool started = advertising->start(); USBSerial.printf("[BLE] Legacy adv start=%s mode=%s bonds=%u whitelist=%u\n", started ? "OK" : "FAIL", @@ -201,9 +221,18 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { xTimerStart(gConnTuneTimer, 0); } USBSerial.printf( - "Device connected handle=%u addr=%s bonded=%d encrypted=%d\n", + "Device connected handle=%u addr=%s bonded=%d encrypted=%d pairing=%d\n", connectedHandle, connInfo.getAddress().toString().c_str(), - connInfo.isBonded() ? 1 : 0, connInfo.isEncrypted() ? 1 : 0); + connInfo.isBonded() ? 1 : 0, connInfo.isEncrypted() ? 1 : 0, + pairingModeActive ? 1 : 0); + + // During pairing mode, proactively request fresh security negotiation. + // This helps recover from stale iOS bonds where iOS tries to restore + // encryption with keys the controller no longer has (rc=19 failures). + if (pairingModeActive && !connInfo.isEncrypted()) { + USBSerial.println("[BLE] Pairing mode: requesting fresh security exchange"); + ble_gap_security_initiate(connInfo.getConnHandle()); + } } void onDisconnect(NimBLEServer *server, NimBLEConnInfo &connInfo, @@ -244,9 +273,23 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { connInfo.isEncrypted() ? 1 : 0, connInfo.getSecKeySize()); if (!connInfo.isEncrypted() || !connInfo.isBonded()) { - USBSerial.println("[BLE] Rejecting untrusted BLE session"); - if (pServer != nullptr) { - pServer->disconnect(connInfo.getConnHandle()); + if (pairingModeActive) { + // During pairing mode, a failed auth likely means the phone has a stale + // bond (e.g. iOS cached old encryption keys). Delete any peer data we + // have and request fresh pairing instead of rejecting outright. + const NimBLEAddress peerAddr = connInfo.getAddress(); + const int bondIdx = NimBLEDevice::getNumBonds(); + (void)bondIdx; // suppress unused warning + USBSerial.printf("[BLE] Pairing mode: auth failed for %s, " + "requesting fresh pairing\n", + peerAddr.toString().c_str()); + NimBLEDevice::deleteBond(peerAddr); + ble_gap_security_initiate(connInfo.getConnHandle()); + } else { + USBSerial.println("[BLE] Rejecting untrusted BLE session"); + if (pServer != nullptr) { + pServer->disconnect(connInfo.getConnHandle()); + } } } } From 647dbc0693ff8b27b3029266f52cdf5526737296 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Thu, 26 Mar 2026 21:33:14 -0400 Subject: [PATCH 2/4] Update src/sp140/ble/ble_core.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sp140/ble/ble_core.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 8549ae1..138b968 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -278,8 +278,6 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { // bond (e.g. iOS cached old encryption keys). Delete any peer data we // have and request fresh pairing instead of rejecting outright. const NimBLEAddress peerAddr = connInfo.getAddress(); - const int bondIdx = NimBLEDevice::getNumBonds(); - (void)bondIdx; // suppress unused warning USBSerial.printf("[BLE] Pairing mode: auth failed for %s, " "requesting fresh pairing\n", peerAddr.toString().c_str()); From 10b726bbc077fb037c7592223bd89d9c4ecbc8d3 Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Thu, 26 Mar 2026 21:33:41 -0400 Subject: [PATCH 3/4] Update src/sp140/ble/ble_core.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sp140/ble/ble_core.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index 138b968..bcf08da 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -139,7 +139,8 @@ bool startAdvertising(NimBLEServer *server) { const bool scanRspConfigured = configured ? advertising->setScanResponseData(kExtAdvInstance, scanRsp) : false; - const bool started = configured && advertising->start(kExtAdvInstance); + const bool started = + configured && scanRspConfigured && advertising->start(kExtAdvInstance); USBSerial.printf( "[BLE] Ext adv cfg=%d scanRsp=%d start=%d mode=%s bonds=%u wl=%u\n", configured, scanRspConfigured, started, From ab53579b2c1df6e4894747124b2eb47ad3a56cdd Mon Sep 17 00:00:00 2001 From: Zach Whitehead Date: Thu, 26 Mar 2026 21:34:00 -0400 Subject: [PATCH 4/4] Update src/sp140/ble/ble_core.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sp140/ble/ble_core.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sp140/ble/ble_core.cpp b/src/sp140/ble/ble_core.cpp index bcf08da..9c420d7 100644 --- a/src/sp140/ble/ble_core.cpp +++ b/src/sp140/ble/ble_core.cpp @@ -279,10 +279,11 @@ class BleServerConnectionCallbacks : public NimBLEServerCallbacks { // bond (e.g. iOS cached old encryption keys). Delete any peer data we // have and request fresh pairing instead of rejecting outright. const NimBLEAddress peerAddr = connInfo.getAddress(); - USBSerial.printf("[BLE] Pairing mode: auth failed for %s, " + USBSerial.printf("[BLE] Pairing mode: auth failed for addr=%s id=%s, " "requesting fresh pairing\n", - peerAddr.toString().c_str()); - NimBLEDevice::deleteBond(peerAddr); + peerAddr.toString().c_str(), + identityAddress.toString().c_str()); + NimBLEDevice::deleteBond(identityAddress); ble_gap_security_initiate(connInfo.getConnHandle()); } else { USBSerial.println("[BLE] Rejecting untrusted BLE session");