Eliminating foot-gun and placing Phone NodeInfo into a mutex

This commit is contained in:
Clive Blackledge 2025-10-14 22:36:07 -07:00
parent 1314e5d105
commit e4f5b8c696
3 changed files with 71 additions and 36 deletions

View File

@ -71,8 +71,12 @@ void PhoneAPI::handleStartConfig()
LOG_DEBUG("Got %d files in manifest", filesManifest.size()); LOG_DEBUG("Got %d files in manifest", filesManifest.size());
LOG_INFO("Start API client config"); LOG_INFO("Start API client config");
nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue.
nodeInfoQueue.clear(); {
std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoForPhone = {};
nodeInfoQueue.clear();
}
resetReadIndex(); resetReadIndex();
} }
@ -94,8 +98,12 @@ void PhoneAPI::close()
onConnectionChanged(false); onConnectionChanged(false);
fromRadioScratch = {}; fromRadioScratch = {};
toRadioScratch = {}; toRadioScratch = {};
nodeInfoForPhone = {}; // Clear cached node info under lock because NimBLE callbacks can still be draining it.
nodeInfoQueue.clear(); {
std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoForPhone = {};
nodeInfoQueue.clear();
}
packetForPhone = NULL; packetForPhone = NULL;
filesManifest.clear(); filesManifest.clear();
fromRadioNum = 0; fromRadioNum = 0;
@ -241,13 +249,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
LOG_DEBUG("Send My NodeInfo"); LOG_DEBUG("Send My NodeInfo");
auto us = nodeDB->readNextMeshNode(readIndex); auto us = nodeDB->readNextMeshNode(readIndex);
if (us) { if (us) {
nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(us); auto info = TypeConversions::ConvertToNodeInfo(us);
nodeInfoForPhone.has_hops_away = false; info.has_hops_away = false;
nodeInfoForPhone.is_favorite = true; info.is_favorite = true;
{
std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoForPhone = info;
}
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
fromRadioScratch.node_info = nodeInfoForPhone; fromRadioScratch.node_info = info;
// Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS
nodeInfoForPhone.num = 0; {
std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoForPhone.num = 0;
}
} }
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
// If client only wants node info, jump directly to sending nodes // If client only wants node info, jump directly to sending nodes
@ -434,23 +449,30 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
case STATE_SEND_OTHER_NODEINFOS: { case STATE_SEND_OTHER_NODEINFOS: {
LOG_DEBUG("Send known nodes"); LOG_DEBUG("Send known nodes");
if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { meshtastic_NodeInfo infoToSend = {};
// Serve the next cached node without re-reading from the DB iterator. {
nodeInfoForPhone = nodeInfoQueue.front(); std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoQueue.pop_front(); if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) {
// Serve the next cached node without re-reading from the DB iterator.
nodeInfoForPhone = nodeInfoQueue.front();
nodeInfoQueue.pop_front();
}
infoToSend = nodeInfoForPhone;
if (infoToSend.num != 0)
nodeInfoForPhone = {};
} }
if (nodeInfoForPhone.num != 0) { if (infoToSend.num != 0) {
// Just in case we stored a different user.id in the past, but should never happen going forward // 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); sprintf(infoToSend.user.id, "!%08x", infoToSend.num);
LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", infoToSend.num, infoToSend.last_heard,
nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); infoToSend.user.id, infoToSend.user.long_name);
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
fromRadioScratch.node_info = nodeInfoForPhone; fromRadioScratch.node_info = infoToSend;
nodeInfoForPhone = {};
prefetchNodeInfos(); prefetchNodeInfos();
} else { } else {
LOG_DEBUG("Done sending nodeinfo"); LOG_DEBUG("Done sending nodeinfo");
std::lock_guard<std::mutex> guard(nodeInfoMutex);
nodeInfoQueue.clear(); nodeInfoQueue.clear();
state = STATE_SEND_FILEMANIFEST; state = STATE_SEND_FILEMANIFEST;
// Go ahead and send that ID right now // Go ahead and send that ID right now
@ -559,20 +581,23 @@ void PhoneAPI::prefetchNodeInfos()
{ {
bool added = false; bool added = false;
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. // 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); std::lock_guard<std::mutex> guard(nodeInfoMutex);
if (!nextNode) while (nodeInfoQueue.size() < kNodePrefetchDepth) {
break; auto nextNode = nodeDB->readNextMeshNode(readIndex);
if (!nextNode)
break;
auto info = TypeConversions::ConvertToNodeInfo(nextNode); auto info = TypeConversions::ConvertToNodeInfo(nextNode);
bool isUs = info.num == nodeDB->getNodeNum(); bool isUs = info.num == nodeDB->getNodeNum();
info.hops_away = isUs ? 0 : info.hops_away; info.hops_away = isUs ? 0 : info.hops_away;
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
info.snr = isUs ? 0 : info.snr; info.snr = isUs ? 0 : info.snr;
info.via_mqtt = isUs ? false : info.via_mqtt; info.via_mqtt = isUs ? false : info.via_mqtt;
info.is_favorite = info.is_favorite || isUs; info.is_favorite = info.is_favorite || isUs;
nodeInfoQueue.push_back(info); nodeInfoQueue.push_back(info);
added = true; added = true;
}
} }
if (added) if (added)
@ -614,10 +639,17 @@ bool PhoneAPI::available()
case STATE_SEND_COMPLETE_ID: case STATE_SEND_COMPLETE_ID:
return true; return true;
case STATE_SEND_OTHER_NODEINFOS: case STATE_SEND_OTHER_NODEINFOS: {
if (nodeInfoQueue.empty()) std::lock_guard<std::mutex> guard(nodeInfoMutex);
prefetchNodeInfos(); if (nodeInfoQueue.empty()) {
// Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it.
goto PREFETCH_NODEINFO;
}
}
return true; // Always say we have something, because we might need to advance our state machine return true; // Always say we have something, because we might need to advance our state machine
PREFETCH_NODEINFO:
prefetchNodeInfos();
return true;
case STATE_SEND_PACKETS: { case STATE_SEND_PACKETS: {
if (!queueStatusPacketForPhone) if (!queueStatusPacketForPhone)
queueStatusPacketForPhone = service->getQueueStatusForPhone(); queueStatusPacketForPhone = service->getQueueStatusForPhone();

View File

@ -5,6 +5,7 @@
#include "meshtastic/portnums.pb.h" #include "meshtastic/portnums.pb.h"
#include <deque> #include <deque>
#include <iterator> #include <iterator>
#include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
@ -84,6 +85,8 @@ class PhoneAPI
std::deque<meshtastic_NodeInfo> nodeInfoQueue; std::deque<meshtastic_NodeInfo> nodeInfoQueue;
// Tunable size of the node info cache so we can keep BLE reads non-blocking. // Tunable size of the node info cache so we can keep BLE reads non-blocking.
static constexpr size_t kNodePrefetchDepth = 4; static constexpr size_t kNodePrefetchDepth = 4;
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
std::mutex nodeInfoMutex;
meshtastic_ToRadio toRadioScratch = { meshtastic_ToRadio toRadioScratch = {
0}; // this is a static scratch object, any data must be copied elsewhere before returning 0}; // this is a static scratch object, any data must be copied elsewhere before returning

View File

@ -139,7 +139,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
{ {
bluetoothPhoneAPI->phoneWants = true; bluetoothPhoneAPI->phoneWants = true;
bluetoothPhoneAPI->setIntervalFromNow(0); bluetoothPhoneAPI->setIntervalFromNow(0);
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex); std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->nimble_mutex); // BLE callbacks run in NimBLE task
if (!bluetoothPhoneAPI->hasChecked) { if (!bluetoothPhoneAPI->hasChecked) {
// Fetch payload on demand; prefetch keeps this fast for the first read. // Fetch payload on demand; prefetch keeps this fast for the first read.