From fcaa168d2d7a36f7ca37e741f79e77c11768ac33 Mon Sep 17 00:00:00 2001 From: Clive Blackledge Date: Mon, 13 Oct 2025 04:32:05 -0700 Subject: [PATCH] Ble reconnect prefetch bug fix, plus some speed enhancements (#8324) * Fixing bluetooth reconnects and adding performance * Added comments --- src/mesh/PhoneAPI.cpp | 58 +++++++++++++++++++++++----------- src/mesh/PhoneAPI.h | 7 ++++ src/nimble/NimbleBluetooth.cpp | 51 ++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index e8dc9843c..9eeadf5a2 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -72,6 +72,7 @@ void PhoneAPI::handleStartConfig() LOG_INFO("Start API client config"); nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos + nodeInfoQueue.clear(); resetReadIndex(); } @@ -94,6 +95,7 @@ void PhoneAPI::close() fromRadioScratch = {}; toRadioScratch = {}; nodeInfoForPhone = {}; + nodeInfoQueue.clear(); packetForPhone = NULL; filesManifest.clear(); fromRadioNum = 0; @@ -431,17 +433,25 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) break; case STATE_SEND_OTHER_NODEINFOS: { + LOG_DEBUG("Send known nodes"); + if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { + // Serve the next cached node without re-reading from the DB iterator. + nodeInfoForPhone = nodeInfoQueue.front(); + nodeInfoQueue.pop_front(); + } + if (nodeInfoForPhone.num != 0) { // Just in case we stored a different user.id in the past, but should never happen going forward sprintf(nodeInfoForPhone.user.id, "!%08x", nodeInfoForPhone.num); - LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, - nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, + nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.node_info = nodeInfoForPhone; - // Stay in current state until done sending nodeinfos - nodeInfoForPhone.num = 0; // We just consumed a nodeinfo, will need a new one next time + nodeInfoForPhone = {}; + prefetchNodeInfos(); } else { LOG_DEBUG("Done sending nodeinfo"); + nodeInfoQueue.clear(); state = STATE_SEND_FILEMANIFEST; // Go ahead and send that ID right now return getFromRadio(buf); @@ -545,6 +555,30 @@ void PhoneAPI::releaseQueueStatusPhonePacket() } } +void PhoneAPI::prefetchNodeInfos() +{ + bool added = false; + // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. + while (nodeInfoQueue.size() < kNodePrefetchDepth) { + auto nextNode = nodeDB->readNextMeshNode(readIndex); + if (!nextNode) + break; + + auto info = TypeConversions::ConvertToNodeInfo(nextNode); + bool isUs = info.num == nodeDB->getNodeNum(); + info.hops_away = isUs ? 0 : info.hops_away; + info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; + info.snr = isUs ? 0 : info.snr; + info.via_mqtt = isUs ? false : info.via_mqtt; + info.is_favorite = info.is_favorite || isUs; + nodeInfoQueue.push_back(info); + added = true; + } + + if (added) + onNowHasData(0); +} + void PhoneAPI::releaseMqttClientProxyPhonePacket() { if (mqttClientProxyMessageForPhone) { @@ -581,20 +615,8 @@ bool PhoneAPI::available() return true; case STATE_SEND_OTHER_NODEINFOS: - if (nodeInfoForPhone.num == 0) { - auto nextNode = nodeDB->readNextMeshNode(readIndex); - if (nextNode) { - nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(nextNode); - bool isUs = nodeInfoForPhone.num == nodeDB->getNodeNum(); - nodeInfoForPhone.hops_away = isUs ? 0 : nodeInfoForPhone.hops_away; - nodeInfoForPhone.last_heard = isUs ? getValidTime(RTCQualityFromNet) : nodeInfoForPhone.last_heard; - nodeInfoForPhone.snr = isUs ? 0 : nodeInfoForPhone.snr; - nodeInfoForPhone.via_mqtt = isUs ? false : nodeInfoForPhone.via_mqtt; - nodeInfoForPhone.is_favorite = nodeInfoForPhone.is_favorite || isUs; // Our node is always a favorite - - onNowHasData(0); - } - } + if (nodeInfoQueue.empty()) + prefetchNodeInfos(); return true; // Always say we have something, because we might need to advance our state machine case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 0d7772d17..692fdd0b9 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -3,6 +3,7 @@ #include "Observer.h" #include "mesh-pb-constants.h" #include "meshtastic/portnums.pb.h" +#include #include #include #include @@ -79,6 +80,10 @@ class PhoneAPI /// We temporarily keep the nodeInfo here between the call to available and getFromRadio meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; + // Prefetched node info entries ready for immediate transmission to the phone. + std::deque nodeInfoQueue; + // Tunable size of the node info cache so we can keep BLE reads non-blocking. + static constexpr size_t kNodePrefetchDepth = 4; meshtastic_ToRadio toRadioScratch = { 0}; // this is a static scratch object, any data must be copied elsewhere before returning @@ -158,6 +163,8 @@ class PhoneAPI void releaseQueueStatusPhonePacket(); + void prefetchNodeInfos(); + void releaseMqttClientProxyPhonePacket(); void releaseClientNotification(); diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 60165b2c0..4b0c33609 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -48,6 +48,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread uint8_t queue_size = 0; uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; size_t numBytes = 0; + bool hasChecked = false; + bool phoneWants = false; protected: virtual int32_t runOnce() override @@ -60,7 +62,11 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread LOG_DEBUG("Queue_size %u", queue_size); queue_size = 0; } - // Note: phoneWants/hasChecked logic removed since onRead() handles getFromRadio() directly + if (!hasChecked && phoneWants) { + // Pull fresh data while we're outside of the NimBLE callback context. + numBytes = getFromRadio(fromRadioBytes); + hasChecked = true; + } // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback return INT32_MAX; @@ -117,6 +123,8 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks bluetoothPhoneAPI->queue_size++; bluetoothPhoneAPI->setIntervalFromNow(0); } + } else { + LOG_DEBUG("Drop duplicate ToRadio packet (%u bytes)", val.length()); } } }; @@ -129,17 +137,32 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks virtual void onRead(NimBLECharacteristic *pCharacteristic) #endif { + bluetoothPhoneAPI->phoneWants = true; + bluetoothPhoneAPI->setIntervalFromNow(0); std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); - // Get fresh data immediately when client reads - bluetoothPhoneAPI->numBytes = bluetoothPhoneAPI->getFromRadio(bluetoothPhoneAPI->fromRadioBytes); + if (!bluetoothPhoneAPI->hasChecked) { + // Fetch payload on demand; prefetch keeps this fast for the first read. + bluetoothPhoneAPI->numBytes = bluetoothPhoneAPI->getFromRadio(bluetoothPhoneAPI->fromRadioBytes); + bluetoothPhoneAPI->hasChecked = true; + } - // Set the characteristic value with whatever data we have pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes); + if (bluetoothPhoneAPI->numBytes != 0) { +#ifdef NIMBLE_TWO + // Notify immediately so subscribed clients see the packet without an extra read. + pCharacteristic->notify(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes, BLE_HS_CONN_HANDLE_NONE); +#else + pCharacteristic->notify(); +#endif + } + if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload bluetoothPhoneAPI->setIntervalFromNow(0); bluetoothPhoneAPI->numBytes = 0; + bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; } }; @@ -271,6 +294,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothPhoneAPI->close(); bluetoothPhoneAPI->numBytes = 0; bluetoothPhoneAPI->queue_size = 0; + bluetoothPhoneAPI->hasChecked = false; + bluetoothPhoneAPI->phoneWants = false; } // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection @@ -278,6 +303,15 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks #ifdef NIMBLE_TWO // Restart Advertising ble->startAdvertising(); +#else + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (!pAdvertising->start(0)) { + if (pAdvertising->isAdvertising()) { + LOG_DEBUG("BLE advertising already running"); + } else { + LOG_ERROR("BLE failed to restart advertising"); + } + } #endif } }; @@ -401,15 +435,18 @@ void NimbleBluetooth::setupService() // Define the characteristics that the app is looking for if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); - FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); + // Allow notifications so phones can stream FromRadio without polling. + FromRadioCharacteristic = + bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); } else { ToRadioCharacteristic = bleService->createCharacteristic( TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); - FromRadioCharacteristic = bleService->createCharacteristic( - FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); + FromRadioCharacteristic = + bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | + NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::NOTIFY); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);