diff --git a/src/mesh/BLEMeshHandler.cpp b/src/mesh/BLEMeshHandler.cpp index ed566593c8..173e9efb31 100644 --- a/src/mesh/BLEMeshHandler.cpp +++ b/src/mesh/BLEMeshHandler.cpp @@ -3,8 +3,12 @@ #if HAS_BLE_MESH #include "BLEMeshHandler.h" +#include "Throttle.h" +#include "UptimeClock.h" #include "main.h" +#include + BLEMeshHandler *bleMeshHandler = nullptr; // AD type constants from the Bluetooth Core Supplement, spelled locally so this file need not pick @@ -242,6 +246,106 @@ void BLEMeshHandler::deliverToRouter(const uint8_t *data, size_t len, int8_t rss enqueueReceived(p.release()); } +BLEMeshHandler::PartialAdv *BLEMeshHandler::partialFor(uint8_t addrType, const uint8_t addr[6], uint8_t sid) +{ + for (auto &slot : partials) { + if (slot.inUse && slot.addrType == addrType && slot.sid == sid && memcmp(slot.addr, addr, sizeof(slot.addr)) == 0) + return &slot; + } + return nullptr; +} + +BLEMeshHandler::PartialAdv &BLEMeshHandler::claimPartial(uint8_t addrType, const uint8_t addr[6], uint8_t sid) +{ + const uint32_t now = Time::getMillis(); + PartialAdv *pick = &partials[0]; + for (auto &slot : partials) { + if (!slot.inUse) { + pick = &slot; + break; + } + if ((uint32_t)(now - slot.lastMs) > (uint32_t)(now - pick->lastMs)) + pick = &slot; + } + pick->inUse = true; + pick->overflowed = false; + pick->addrType = addrType; + memcpy(pick->addr, addr, sizeof(pick->addr)); + pick->sid = sid; + pick->len = 0; + pick->lastMs = now; + return *pick; +} + +void BLEMeshHandler::onScanReport(uint8_t addrType, const uint8_t addr[6], uint8_t sid, ScanReportStatus status, + const uint8_t *data, size_t len, int8_t rssi) +{ + if (!isRunning || !addr || (len > 0 && !data)) + return; + + PartialAdv *slot = partialFor(addrType, addr, sid); + if (slot && Throttle::hasElapsed(slot->lastMs, BLE_MESH_REASSEMBLY_TIMEOUT_MS)) { + slot->inUse = false; + slot = nullptr; + } + + if (status == ScanReportStatus::Truncated) { + if (slot) + slot->inUse = false; + return; + } + + if (status == ScanReportStatus::Complete && !slot) { + handleAdvertisementData(data, len, rssi); + return; + } + + if (!slot) + slot = &claimPartial(addrType, addr, sid); + + if (!slot->overflowed) { + if (slot->len + len > slot->data.size()) { + slot->overflowed = true; + } else if (len > 0) { + memcpy(slot->data.data() + slot->len, data, len); + slot->len += len; + } + } + slot->lastMs = Time::getMillis(); + + if (status == ScanReportStatus::Complete) { + slot->inUse = false; + if (!slot->overflowed) + handleAdvertisementData(slot->data.data(), slot->len, rssi); + } +} + +void BLEMeshHandler::handleAdvertisementData(const uint8_t *data, size_t len, int8_t rssi) +{ + if (!isRunning || !data) + return; + + // An advertisement carries several AD structures, and the mesh one is not necessarily first. + size_t offset = 0; + while (offset + 1 < len) { + uint8_t adLen = data[offset]; + // An AD structure spans data[offset] .. data[offset + adLen]; anything else is truncated. + if (adLen == 0 || offset + adLen >= len) + break; + + uint8_t adType = data[offset + 1]; + if (adType == BLE_MESH_AD_TYPE_MFG_DATA && adLen >= 4) { + uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8); + if (companyId == BLE_MESH_COMPANY_ID && data[offset + 4] == BLE_MESH_PROTOCOL_VERSION) { + // Skip the type byte, the 2-byte company ID and the 1-byte version. + deliverToRouter(&data[offset + 5], adLen - 4, rssi); + return; + } + } + offset += adLen + 1; + } +} + void BLEMeshHandler::enqueueReceived(meshtastic_MeshPacket *p) { router->enqueueReceivedMessage(p); diff --git a/src/mesh/BLEMeshHandler.h b/src/mesh/BLEMeshHandler.h index 35bed5994a..c69d00a471 100644 --- a/src/mesh/BLEMeshHandler.h +++ b/src/mesh/BLEMeshHandler.h @@ -17,10 +17,10 @@ #define BLE_MESH_COMPANY_ID 0xFFFF #define BLE_MESH_PROTOCOL_VERSION 1 -// One unfragmented extended advertising payload caps at 251 bytes, not the 254 an AUX_ADV_IND -// holds: HCI LE Set Extended Advertising Data spends four of its 255 parameter bytes on handle, -// operation, fragment preference and length. Each platform static_asserts this against its own -// stack's constant. +// The advertising data one HCI LE Set Extended Advertising Data command carries. One AUX_ADV_IND +// holds 245 bytes of it, so the Link Layer chains more into an AUX_CHAIN_IND, and a receiving +// controller reports anything over 229 bytes as several HCI reports, which onScanReport() +// reassembles. Each platform static_asserts this against its own stack's constant. #define BLE_MESH_ADV_TOTAL_MAX 251 // Flags AD structure (3) + manufacturer-data AD header (2) + company ID (2) + version (1). @@ -38,6 +38,16 @@ #define BLE_MESH_ADV_EVENTS 3 #endif +// Advertisers whose split reports can be reassembled at once; a further one evicts the stalest. +#ifndef BLE_MESH_REASSEMBLY_SLOTS +#define BLE_MESH_REASSEMBLY_SLOTS 2 +#endif + +// HCI marks no first report, so a partial advertisement this old is abandoned rather than joined +// to whatever its advertiser sends next. +#ifndef BLE_MESH_REASSEMBLY_TIMEOUT_MS +#define BLE_MESH_REASSEMBLY_TIMEOUT_MS 100 +#endif /// A burst of `events` advertising events at `intervalUnits` x 0.625 ms, as a duration in ms. The /// extra interval covers the up-to-10 ms advDelay the Link Layer adds before each event. @@ -73,7 +83,7 @@ class BLEMeshHandler : private concurrency::OSThread, public MeshTransportBase return config.network.enabled_protocols & meshtastic_Config_NetworkConfig_ProtocolFlags_BLE_BROADCAST; } - /// Packets refused for not fitting one unfragmented advertisement. The ceiling is + /// Packets refused for not fitting one advertisement. The ceiling is /// BLE_MESH_MAX_PROTO_LEN for the whole encoded MeshPacket, below LoRa's MAX_RADIO_PAYLOAD_LEN. uint32_t txDroppedTooLarge = 0; @@ -119,6 +129,19 @@ class BLEMeshHandler : private concurrency::OSThread, public MeshTransportBase /// Decode a received advertisement payload and enqueue it into the router. void deliverToRouter(const uint8_t *data, size_t len, int8_t rssi); + /// How much of an advertisement one scan report carries, as HCI LE Extended Advertising Report + /// grades it. + enum class ScanReportStatus : uint8_t { Complete, Incomplete, Truncated }; + + /// One extended scan report. A controller splits an advertisement over 229 bytes into several + /// reports from the same (address, SID), each but the last Incomplete; this joins them and hands + /// the whole advertisement to handleAdvertisementData(). A Truncated advertisement is dropped. + void onScanReport(uint8_t addrType, const uint8_t addr[6], uint8_t sid, ScanReportStatus status, const uint8_t *data, + size_t len, int8_t rssi); + + /// Find the mesh manufacturer-data structure in a whole advertisement and deliver its packet. + void handleAdvertisementData(const uint8_t *data, size_t len, int8_t rssi); + /// Hand an accepted packet on. Virtual only so the native tests can observe what survives the /// ingress guards without standing up a live Router; production always takes the default. virtual void enqueueReceived(meshtastic_MeshPacket *p); @@ -149,6 +172,24 @@ class BLEMeshHandler : private concurrency::OSThread, public MeshTransportBase // one-shot readiness callback would race. runOnce() polls platformReady() and calls // onBluetoothReady() itself, exactly once. bool readyHandled = false; + + /// An advertisement whose reports have not all arrived. Touched only from the scan callback, + /// which runs on the BLE host task alone. + struct PartialAdv { + bool inUse; + // Longer than any mesh advertisement, so the rest is skipped until its last report. + bool overflowed; + uint8_t addrType; + uint8_t addr[6]; + uint8_t sid; + uint16_t len; + uint32_t lastMs; + std::array data; + }; + std::array partials{}; + + PartialAdv *partialFor(uint8_t addrType, const uint8_t addr[6], uint8_t sid); + PartialAdv &claimPartial(uint8_t addrType, const uint8_t addr[6], uint8_t sid); }; extern BLEMeshHandler *bleMeshHandler; diff --git a/src/platform/esp32/ESP32BLEMesh.cpp b/src/platform/esp32/ESP32BLEMesh.cpp index 01604d521c..d700312099 100644 --- a/src/platform/esp32/ESP32BLEMesh.cpp +++ b/src/platform/esp32/ESP32BLEMesh.cpp @@ -14,7 +14,7 @@ #if BLE_MESH_USE_EXT_ADV static_assert(BLE_MESH_ADV_TOTAL_MAX <= BLE_HCI_MAX_EXT_ADV_DATA_LEN, - "advertisement budget must fit NimBLE's unfragmented ext-adv data limit"); + "advertisement budget must fit one NimBLE LE Set Extended Advertising Data command"); #endif void ESP32BLEMesh::start() @@ -249,7 +249,7 @@ void ESP32BLEMesh::handleAdvertisement(const struct ble_gap_disc_desc *desc) if (!isRunning || !desc) return; - handleAdvertisementData(desc->rssi, desc->data, desc->length_data); + handleAdvertisementData(desc->data, desc->length_data, desc->rssi); } #if BLE_MESH_USE_EXT_ADV @@ -258,41 +258,14 @@ void ESP32BLEMesh::handleExtendedAdvertisement(const struct ble_gap_ext_disc_des if (!isRunning || !desc) return; - // Nothing chains on send, so anything flagged INCOMPLETE is another advertiser's. - if (desc->data_status != BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE || !desc->data) - return; + ScanReportStatus status = ScanReportStatus::Truncated; + if (desc->data_status == BLE_GAP_EXT_ADV_DATA_STATUS_COMPLETE) + status = ScanReportStatus::Complete; + else if (desc->data_status == BLE_GAP_EXT_ADV_DATA_STATUS_INCOMPLETE) + status = ScanReportStatus::Incomplete; - handleAdvertisementData(desc->rssi, desc->data, desc->length_data); + onScanReport(desc->addr.type, desc->addr.val, desc->sid, status, desc->data, desc->length_data, desc->rssi); } #endif -void ESP32BLEMesh::handleAdvertisementData(int8_t rssi, const uint8_t *data, uint8_t len) -{ - if (!isRunning || !data) - return; - - // An advertisement carries several AD structures, and the mesh one is not necessarily first. - uint16_t offset = 0; - while (offset + 1 < len) { - uint8_t adLen = data[offset]; - // An AD structure spans data[offset] .. data[offset + adLen]; anything else is truncated. - if (adLen == 0 || offset + adLen >= len) - break; - - uint8_t adType = data[offset + 1]; - if (adType == BLE_HS_ADV_TYPE_MFG_DATA && adLen >= 4) { - uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8); - if (companyId == BLE_MESH_COMPANY_ID && data[offset + 4] == BLE_MESH_PROTOCOL_VERSION) { - // Skip the type byte, the 2-byte company ID and the 1-byte version. - const uint8_t *payload = &data[offset + 5]; - size_t payloadLen = adLen - 4; - - deliverToRouter(payload, payloadLen, rssi); - return; - } - } - offset += adLen + 1; - } -} - #endif // HAS_BLE_MESH && ARCH_ESP32 diff --git a/src/platform/esp32/ESP32BLEMesh.h b/src/platform/esp32/ESP32BLEMesh.h index 9c83cca51f..8281ee621b 100644 --- a/src/platform/esp32/ESP32BLEMesh.h +++ b/src/platform/esp32/ESP32BLEMesh.h @@ -50,7 +50,6 @@ class ESP32BLEMesh : public BLEMeshHandler bool configureAdvInstance(); bool advInstanceConfigured = false; #endif - void handleAdvertisementData(int8_t rssi, const uint8_t *data, uint8_t len); }; #endif // HAS_BLE_MESH && ARCH_ESP32 diff --git a/src/platform/nrf52/NRF52BLEMesh.cpp b/src/platform/nrf52/NRF52BLEMesh.cpp index c938ca048b..dca30db556 100644 --- a/src/platform/nrf52/NRF52BLEMesh.cpp +++ b/src/platform/nrf52/NRF52BLEMesh.cpp @@ -195,9 +195,10 @@ void NRF52BLEMesh::onBleEvent(ble_evt_t *event) case BLE_GAP_EVT_ADV_REPORT: { ble_gap_evt_adv_report_t *report = &event->evt.gap_evt.params.adv_report; - if (report->type.status == BLE_GAP_ADV_DATA_STATUS_COMPLETE) { - instance->handleScanResult(report); - } + // The SoftDevice joins a chained advertisement in the scan buffer itself and reports it once + // (report_incomplete_evts is unsupported), so a whole advertisement is always COMPLETE. + if (report->type.status == BLE_GAP_ADV_DATA_STATUS_COMPLETE) + instance->handleAdvertisementData(report->data.p_data, report->data.len, report->rssi); // The SoftDevice pauses scanning after each report; hand the buffer back to resume. bleMeshScanReportData.len = sizeof(bleMeshScanBuffer); @@ -225,33 +226,4 @@ void NRF52BLEMesh::onBleEvent(ble_evt_t *event) } } -void NRF52BLEMesh::handleScanResult(ble_gap_evt_adv_report_t *report) -{ - if (!isRunning || !report->data.p_data) - return; - - const uint8_t *data = report->data.p_data; - uint16_t len = report->data.len; - uint16_t offset = 0; - - while (offset + 1 < len) { - uint8_t adLen = data[offset]; - if (adLen == 0 || offset + adLen >= len) - break; - - uint8_t adType = data[offset + 1]; - if (adType == BLE_GAP_AD_TYPE_MANUFACTURER_SPECIFIC_DATA && adLen >= 4) { - uint16_t companyId = data[offset + 2] | (data[offset + 3] << 8); - if (companyId == BLE_MESH_COMPANY_ID && data[offset + 4] == BLE_MESH_PROTOCOL_VERSION) { - const uint8_t *payload = &data[offset + 5]; - size_t payloadLen = adLen - 4; - - deliverToRouter(payload, payloadLen, report->rssi); - return; - } - } - offset += adLen + 1; - } -} - #endif // HAS_BLE_MESH && ARCH_NRF52 diff --git a/src/platform/nrf52/NRF52BLEMesh.h b/src/platform/nrf52/NRF52BLEMesh.h index 1a2d278130..8b31b40fa1 100644 --- a/src/platform/nrf52/NRF52BLEMesh.h +++ b/src/platform/nrf52/NRF52BLEMesh.h @@ -35,7 +35,6 @@ class NRF52BLEMesh : public BLEMeshHandler private: void startScanning(); void stopScanning(); - void handleScanResult(ble_gap_evt_adv_report_t *report); // A SoftDevice advertising set of its own, allocated by passing BLE_GAP_ADV_SET_HANDLE_NOT_SET. // Handle 0 is Bluefruit's phone advertisement; sharing it means tearing that down and restoring diff --git a/test/test_ble_mesh/BleMesh.cpp b/test/test_ble_mesh/BleMesh.cpp index b577657bb8..c6d2ed6bcc 100644 --- a/test/test_ble_mesh/BleMesh.cpp +++ b/test/test_ble_mesh/BleMesh.cpp @@ -4,6 +4,7 @@ #if defined(ARCH_PORTDUINO) && HAS_BLE_MESH +#include "UptimeClock.h" #include "mesh/BLEMeshHandler.h" #include "mesh/NodeDB.h" #include "mesh/Router.h" @@ -25,6 +26,8 @@ class FakeBLEMesh : public BLEMeshHandler bool ready = true; bool advertising = false; + using BLEMeshHandler::ScanReportStatus; + void start() override { isRunning = true; } void stop() override { isRunning = false; } @@ -34,6 +37,11 @@ class FakeBLEMesh : public BLEMeshHandler return onCancelSending(medium, from, id); } uint8_t build(const meshtastic_MeshPacket *mp, uint8_t *out, size_t cap) { return buildAdvPayload(mp, out, cap); } + void report(uint8_t sid, ScanReportStatus status, const uint8_t *data, size_t len, const uint8_t *addr = defaultAddr) + { + onScanReport(0 /* public */, addr, sid, status, data, len, -60); + } + static constexpr uint8_t defaultAddr[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; int32_t pump() { return runOnce(); } void enqueueReceived(meshtastic_MeshPacket *p) override @@ -109,6 +117,22 @@ size_t encodeForAir(const meshtastic_MeshPacket &mp, uint8_t *out, size_t cap) return pb_encode_to_bytes(out, cap, &meshtastic_MeshPacket_msg, &mp); } +/// Build a whole advertisement of exactly `total` bytes, the size an HCI split is measured against. +size_t advertisementOf(FakeBLEMesh &h, size_t total, uint32_t id, uint8_t *out, meshtastic_MeshPacket *sent = nullptr) +{ + for (size_t payload = 1; payload <= MAX_ENCRYPTED_FOR_TEST; payload++) { + auto p = encryptedPacket(0x3061b02e, id, payload); + if (h.build(&p, out, BLE_MESH_ADV_TOTAL_MAX) == total) { + if (sent) + *sent = p; + return total; + } + } + return 0; +} + +using Status = FakeBLEMesh::ScanReportStatus; + } // namespace void test_advertisement_carries_the_packet(void) @@ -159,9 +183,9 @@ void test_drops_a_packet_too_large_for_one_advertisement(void) { FakeBLEMesh h; h.start(); - // One unfragmented extended advertisement holds 251 bytes and nothing chains, so the largest - // packets cannot ride BLE. They still go out over LoRa: Router::send has already handed them to - // the radio by the time the transport refuses. + // One advertisement holds 251 bytes, one HCI Set Extended Advertising Data command, so the + // largest packets cannot ride BLE. They still go out over LoRa: Router::send has already handed + // them to the radio by the time the transport refuses. auto p = encryptedPacket(0x3061b02e, 0x04050b6e, MAX_ENCRYPTED_FOR_TEST); uint8_t adv[BLE_MESH_ADV_TOTAL_MAX]; @@ -520,6 +544,99 @@ void test_legacy_burst_duration_is_in_milliseconds(void) TEST_ASSERT_GREATER_OR_EQUAL_INT32(3 * 30, bleMeshBurstMs(3, 48)); } +// HCI LE Extended Advertising Report carries at most 229 bytes, so a controller hands a 245-byte +// advertisement (one full AUX_ADV_IND) up as a 229-byte INCOMPLETE head and a 16-byte COMPLETE tail. +// Dropping the head and parsing the tail alone loses every packet over 221 bytes. +void test_a_split_advertisement_is_joined(void) +{ + FakeBLEMesh h; + h.start(); + uint8_t adv[BLE_MESH_ADV_TOTAL_MAX]; + meshtastic_MeshPacket p; + TEST_ASSERT_EQUAL_MESSAGE(245, advertisementOf(h, 245, 0x0a0b0c0d, adv, &p), "a 245-byte advertisement exists"); + + h.report(0, Status::Incomplete, adv, 229); + TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "nothing delivered from the head alone"); + h.report(0, Status::Complete, adv + 229, 16); + + TEST_ASSERT_EQUAL_MESSAGE(1, h.received.size(), "the joined advertisement delivered once"); + TEST_ASSERT_EQUAL_UINT32(p.id, h.received[0].id); + TEST_ASSERT_EQUAL_UINT32(p.encrypted.size, h.received[0].encrypted.size); + TEST_ASSERT_EQUAL_MEMORY(p.encrypted.bytes, h.received[0].encrypted.bytes, p.encrypted.size); +} + +void test_an_unsplit_advertisement_is_delivered(void) +{ + FakeBLEMesh h; + h.start(); + uint8_t adv[BLE_MESH_ADV_TOTAL_MAX]; + TEST_ASSERT_EQUAL(120, advertisementOf(h, 120, 0x0a0b0c0d, adv)); + + h.report(0, Status::Complete, adv, 120); + TEST_ASSERT_EQUAL(1, h.received.size()); +} + +// TRUNCATED means the controller lost the rest of the chain; what arrived is not a packet. +void test_a_truncated_advertisement_is_dropped(void) +{ + FakeBLEMesh h; + h.start(); + uint8_t adv[BLE_MESH_ADV_TOTAL_MAX]; + TEST_ASSERT_EQUAL(245, advertisementOf(h, 245, 0x0a0b0c0d, adv)); + + h.report(0, Status::Incomplete, adv, 229); + h.report(0, Status::Truncated, adv + 229, 8); + TEST_ASSERT_EQUAL_MESSAGE(0, h.received.size(), "a truncated advertisement is not delivered"); + + // The abandoned head must not be joined to the advertiser's next burst. + h.report(0, Status::Incomplete, adv, 229); + h.report(0, Status::Complete, adv + 229, 16); + TEST_ASSERT_EQUAL_MESSAGE(1, h.received.size(), "the next burst arrives whole"); +} + +// The key is (address, SID): two advertisers, or two sets of one, interleave on air. +void test_interleaved_advertisers_stay_apart(void) +{ + FakeBLEMesh h; + h.start(); + uint8_t a[BLE_MESH_ADV_TOTAL_MAX]; + uint8_t b[BLE_MESH_ADV_TOTAL_MAX]; + TEST_ASSERT_EQUAL(245, advertisementOf(h, 245, 0x0000000a, a)); + TEST_ASSERT_EQUAL(240, advertisementOf(h, 240, 0x0000000b, b)); + const uint8_t otherAddr[6] = {0x66, 0x55, 0x44, 0x33, 0x22, 0x11}; + + h.report(0, Status::Incomplete, a, 229); + h.report(0, Status::Incomplete, b, 229, otherAddr); + h.report(0, Status::Complete, a + 229, 16); + h.report(0, Status::Complete, b + 229, 11, otherAddr); + + TEST_ASSERT_EQUAL(2, h.received.size()); + TEST_ASSERT_EQUAL_UINT32(0x0000000a, h.received[0].id); + TEST_ASSERT_EQUAL_UINT32(0x0000000b, h.received[1].id); +} + +// HCI marks no first report, so a head whose tail never came must expire rather than prefix the +// advertiser's next advertisement. +void test_a_stale_partial_is_abandoned(void) +{ + FakeBLEMesh h; + h.start(); + uint8_t stale[BLE_MESH_ADV_TOTAL_MAX]; + uint8_t fresh[BLE_MESH_ADV_TOTAL_MAX]; + TEST_ASSERT_EQUAL(245, advertisementOf(h, 245, 0x0000000a, stale)); + TEST_ASSERT_EQUAL(245, advertisementOf(h, 245, 0x0000000b, fresh)); + + Time::setTestMillis(10000); + h.report(0, Status::Incomplete, stale, 229); + Time::advanceTestMillis(BLE_MESH_REASSEMBLY_TIMEOUT_MS + 1); + h.report(0, Status::Incomplete, fresh, 229); + h.report(0, Status::Complete, fresh + 229, 16); + Time::useRealClock(); + + TEST_ASSERT_EQUAL(1, h.received.size()); + TEST_ASSERT_EQUAL_UINT32(0x0000000b, h.received[0].id); +} + void setUp(void) {} void tearDown(void) {} @@ -552,6 +669,11 @@ void setup() RUN_TEST(test_ingress_ignores_our_own_advertisement); RUN_TEST(test_pump_waits_for_the_platform); RUN_TEST(test_legacy_burst_duration_is_in_milliseconds); + RUN_TEST(test_a_split_advertisement_is_joined); + RUN_TEST(test_an_unsplit_advertisement_is_delivered); + RUN_TEST(test_a_truncated_advertisement_is_dropped); + RUN_TEST(test_interleaved_advertisers_stay_apart); + RUN_TEST(test_a_stale_partial_is_abandoned); exit(UNITY_END()); } diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 8c3308ceca..24f4026e3d 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -404,8 +404,8 @@ custom_sdkconfig = ; OBSERVER not set), so ble_gap_ext_disc returns BLE_HS_ENOTSUP and the transport can advertise ; but never receive. CONFIG_BT_NIMBLE_ROLE_OBSERVER=y - ; The default 1650 is the chained ceiling and is reserved per instance, for a transport capped - ; at one 251-byte PDU. + ; The default 1650 is the chained ceiling and is reserved per instance, for a transport that + ; sets at most 251 bytes. CONFIG_BT_NIMBLE_EXT_ADV_MAX_SIZE=257 ; The controller counts advertising sets, scans and connections alike as "activities", and short ; of the budget both the scan enable and the second ext_adv_configure fail with HCI 0x07, Memory