diff --git a/src/DebugConfiguration.h b/src/DebugConfiguration.h index 98bbe0f729..173096b654 100644 --- a/src/DebugConfiguration.h +++ b/src/DebugConfiguration.h @@ -53,12 +53,42 @@ extern MemGet memGet; #define LOG_TRACE(...) SEGGER_RTT_printf(0, __VA_ARGS__) #else #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) -#define LOG_DEBUG(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_DEBUG, __VA_ARGS__) -#define LOG_INFO(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_INFO, __VA_ARGS__) -#define LOG_WARN(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_WARN, __VA_ARGS__) -#define LOG_ERROR(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_ERROR, __VA_ARGS__) -#define LOG_CRIT(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_CRIT, __VA_ARGS__) -#define LOG_TRACE(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_TRACE, __VA_ARGS__) +#define LOG_DEBUG(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_DEBUG, __VA_ARGS__); \ + } \ + } while (0) +#define LOG_INFO(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_INFO, __VA_ARGS__); \ + } \ + } while (0) +#define LOG_WARN(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_WARN, __VA_ARGS__); \ + } \ + } while (0) +#define LOG_ERROR(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_ERROR, __VA_ARGS__); \ + } \ + } while (0) +#define LOG_CRIT(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_CRIT, __VA_ARGS__); \ + } \ + } while (0) +#define LOG_TRACE(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_TRACE, __VA_ARGS__); \ + } \ + } while (0) #else #define LOG_DEBUG(...) #define LOG_INFO(...) @@ -70,7 +100,12 @@ extern MemGet memGet; #endif #if defined(DEBUG_HEAP) -#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__) +#define LOG_HEAP(...) \ + do { \ + if (console) { \ + console->log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__); \ + } \ + } while (0) // Macro-based heap debugging #define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap(); @@ -195,4 +230,4 @@ class Syslog bool vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) __attribute__((format(printf, 3, 0))); }; -#endif // HAS_NETWORKING \ No newline at end of file +#endif // HAS_NETWORKING diff --git a/src/NodeStatus.h b/src/NodeStatus.h index 550f6254a6..07d2dd4473 100644 --- a/src/NodeStatus.h +++ b/src/NodeStatus.h @@ -65,4 +65,4 @@ class NodeStatus : public Status } // namespace meshtastic -extern meshtastic::NodeStatus *nodeStatus; \ No newline at end of file +extern meshtastic::NodeStatus *nodeStatus; diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index d383a11e45..ab58b55e8d 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -555,4 +555,4 @@ void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) drawLine(x0, y1, x1, y0, BLACK); } -#endif \ No newline at end of file +#endif diff --git a/src/main.cpp b/src/main.cpp index c654822927..b7beba70ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -295,7 +295,7 @@ void printInfo() { LOG_INFO("S:B:%d,%s,%s,%s", HW_VENDOR, optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); } -#ifndef PIO_UNIT_TESTING +#if !defined(PIO_UNIT_TESTING) || !(PIO_UNIT_TESTING) void setup() { #if defined(R1_NEO) @@ -1573,7 +1573,7 @@ void scannerToSensorsMap(const std::unique_ptr &i2cScanner, Scan } #endif -#ifndef PIO_UNIT_TESTING +#if !defined(PIO_UNIT_TESTING) || !(PIO_UNIT_TESTING) void loop() { runASAP = false; diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index eb5ac5109d..0784d8c940 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -8,6 +8,10 @@ #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP +#if defined(ARCH_ESP32) +#include +#endif + template class Allocator { @@ -159,3 +163,92 @@ template class MemoryPool : public Allocator return nullptr; } }; + +#if defined(ARCH_ESP32) +// Simple fixed-size allocator that uses PSRAM. Used on ESP32-S3 builds so the +// large MeshPacket pool can live off-chip and free internal RAM. +template class PsramMemoryPool : public Allocator +{ + private: + T *pool; + bool used[MaxSize]; + + public: + PsramMemoryPool() : pool(nullptr), used{} + { + pool = static_cast(heap_caps_malloc(sizeof(T) * MaxSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + if (pool) { + memset(pool, 0, sizeof(T) * MaxSize); + } else { + LOG_WARN("Failed to allocate PSRAM pool of %d elements", MaxSize); + } + } + + ~PsramMemoryPool() override + { + if (pool) { + heap_caps_free(pool); + } + } + + bool isValid() const { return pool != nullptr; } + + void release(T *p) override + { + if (!pool || !p) { + LOG_DEBUG("Failed to release PSRAM memory, pointer is null or pool unavailable"); + return; + } + + int index = static_cast(p - pool); + if (index >= 0 && index < MaxSize) { + assert(used[index]); + used[index] = false; + LOG_HEAP("Released PSRAM pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from PSRAM pool!", p); + } + } + + protected: + T *alloc(TickType_t maxWait) override + { + if (!pool) + return nullptr; + + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated PSRAM pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + LOG_WARN("No free slots available in PSRAM memory pool!"); + return nullptr; + } +}; + +// Utility helpers for PSRAM-backed array allocations on ESP32 targets. +template inline T *psramAllocArray(size_t count) +{ + return static_cast(heap_caps_malloc(sizeof(T) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); +} + +template inline void psramFreeArray(T *ptr) +{ + if (ptr) + heap_caps_free(ptr); +} +#else +template inline T *psramAllocArray(size_t count) +{ + (void)count; + return nullptr; +} + +template inline void psramFreeArray(T *ptr) +{ + (void)ptr; +} +#endif diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 1b2af082d8..6b4504b058 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -305,15 +305,21 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p) #endif #endif - if (toPhoneQueue.numFree() == 0) { + // MAX_RX_TOPHONE is sized for PSRAM-backed builds. Fall back to a smaller + // runtime limit if the helper detects <2MB of PSRAM at boot. + const int queueLimit = get_rx_tophone_limit(); + const bool runtimeLimitReached = queueLimit > 0 && toPhoneQueue.numUsed() >= queueLimit; + + if (toPhoneQueue.numFree() == 0 || runtimeLimitReached) { + const bool runtimeControlled = runtimeLimitReached && queueLimit < MAX_RX_TOPHONE; if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - LOG_WARN("ToPhone queue is full, discard oldest"); + LOG_WARN("ToPhone queue %s, discard oldest", runtimeControlled ? "reached runtime limit" : "is full"); meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); if (d) releaseToPool(d); } else { - LOG_WARN("ToPhone queue is full, drop packet"); + LOG_WARN("ToPhone queue %s, drop packet", runtimeControlled ? "reached runtime limit" : "is full"); releaseToPool(p); fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets return; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index df9aece0ad..4cfad2b8fc 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -24,6 +24,7 @@ #include "modules/NeighborInfoModule.h" #include #include +#include #include #include #include @@ -37,6 +38,12 @@ #include #include #include +#if __has_include() +#include +#define NODEDB_HAS_ESP_PTR 1 +#else +#define NODEDB_HAS_ESP_PTR 0 +#endif #include #include #include @@ -67,6 +74,174 @@ meshtastic_DeviceUIConfig uiconfig{.screen_brightness = 153, .screen_timeout = 3 meshtastic_LocalModuleConfig moduleConfig; meshtastic_ChannelFile channelFile; +//------------------------------------------------------------------------------ +// Runtime instrumentation helpers +//------------------------------------------------------------------------------ + +namespace +{ + +// Log the pool headroom every 100 inserts (and when we hit MAX) so field logs +// capture how close we are to exhausting heap/PSRAM on real hardware. + +void logNodeInsertStats(size_t count, const char *poolLabel) +{ + if (count == 0) + return; + if ((count % 100) != 0 && count != MAX_NUM_NODES) + return; + + LOG_INFO("NodeDB %s pool usage %u/%u nodes, heap free %u, psram free %u", poolLabel, static_cast(count), + static_cast(MAX_NUM_NODES), memGet.getFreeHeap(), memGet.getFreePsram()); +} + +#if defined(CONFIG_IDF_TARGET_ESP32S3) +bool logPsramAllocationOnce(void *ptr, size_t capacity) +{ + static bool logged = false; + if (logged || !ptr) + return logged; + +#if NODEDB_HAS_ESP_PTR + bool inPsram = esp_ptr_external_ram(ptr); +#else + bool inPsram = false; +#endif + LOG_INFO("NodeDB PSRAM backing at %p (%s) capacity %u entries (~%u bytes)", ptr, inPsram ? "PSRAM" : "DRAM", + static_cast(capacity), static_cast(capacity * sizeof(meshtastic_NodeInfoLite))); + logged = true; + return logged; +} +#endif + +} // namespace + +#if defined(CONFIG_IDF_TARGET_ESP32S3) + +void NodeDB::initHotCache() +{ + // Pre-reserve the full cold store in PSRAM during boot so the high watermark + // shows up immediately in PSRAM usage logs and we avoid fragmented + // allocations later in the mission. + psramMeshNodes.resize(MAX_NUM_NODES); + hotNodes.resize(MAX_NUM_NODES); + hotDirty.assign(MAX_NUM_NODES, true); + meshNodes = &psramMeshNodes; + logPsramAllocationOnce(psramMeshNodes.data(), psramMeshNodes.capacity()); +} + +void NodeDB::refreshHotCache() +{ + for (size_t i = 0; i < numMeshNodes; ++i) { + if (hotDirty[i]) + syncHotFromCold(i); + } +} + +void NodeDB::syncHotFromCold(size_t index) +{ + if (index >= psramMeshNodes.size()) + return; + + const meshtastic_NodeInfoLite &node = psramMeshNodes[index]; + NodeHotEntry &hot = hotNodes[index]; + + hot.num = node.num; + hot.last_heard = node.last_heard; + hot.snr = node.snr; + hot.channel = node.channel; + hot.next_hop = node.next_hop; + hot.role = static_cast(node.user.role); + hot.hops_away = node.hops_away; + + uint8_t flags = 0; + if (node.via_mqtt) + flags |= HOT_FLAG_VIA_MQTT; + if (node.is_favorite) + flags |= HOT_FLAG_IS_FAVORITE; + if (node.is_ignored) + flags |= HOT_FLAG_IS_IGNORED; + if (node.has_hops_away) + flags |= HOT_FLAG_HAS_HOPS; + if (node.bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) + flags |= HOT_FLAG_IS_KEY_VERIFIED; + hot.flags = flags; + + hotDirty[index] = false; +} + +void NodeDB::markHotDirty(size_t index) +{ + if (index < hotDirty.size()) + hotDirty[index] = true; +} + +void NodeDB::markHotDirty(const meshtastic_NodeInfoLite *ptr) +{ + size_t idx = indexOf(ptr); + if (idx != std::numeric_limits::max()) + markHotDirty(idx); +} + +void NodeDB::clearSlot(size_t index) +{ + if (index >= psramMeshNodes.size()) + return; + + psramMeshNodes[index] = {}; + hotNodes[index] = NodeHotEntry{}; + hotDirty[index] = false; +} + +void NodeDB::swapSlots(size_t a, size_t b) +{ + if (a == b) + return; + + std::swap(psramMeshNodes[a], psramMeshNodes[b]); + std::swap(hotNodes[a], hotNodes[b]); + std::swap(hotDirty[a], hotDirty[b]); +} + +void NodeDB::copySlot(size_t src, size_t dst) +{ + if (src == dst) + return; + + psramMeshNodes[dst] = psramMeshNodes[src]; + hotNodes[dst] = hotNodes[src]; + hotDirty[dst] = hotDirty[src]; +} + +void NodeDB::moveSlot(size_t src, size_t dst) +{ + if (src == dst) + return; + + copySlot(src, dst); + clearSlot(src); +} + +bool NodeDB::isNodeEmpty(const meshtastic_NodeInfoLite &node) const +{ + return node.num == 0 && !node.has_user && !node.has_position && !node.has_device_metrics && !node.is_favorite && + !node.is_ignored && node.last_heard == 0 && node.channel == 0 && node.next_hop == 0 && node.bitfield == 0; +} + +size_t NodeDB::indexOf(const meshtastic_NodeInfoLite *ptr) const +{ + if (!ptr || psramMeshNodes.empty()) + return std::numeric_limits::max(); + + const meshtastic_NodeInfoLite *base = psramMeshNodes.data(); + size_t idx = static_cast(ptr - base); + if (idx >= psramMeshNodes.size()) + return std::numeric_limits::max(); + return idx; +} + +#endif + #ifdef USERPREFS_USE_ADMIN_KEY_0 static unsigned char userprefs_admin_key_0[] = USERPREFS_USE_ADMIN_KEY_0; #endif @@ -520,9 +695,16 @@ void NodeDB::installDefaultNodeDatabase() { LOG_DEBUG("Install default NodeDatabase"); nodeDatabase.version = DEVICESTATE_CUR_VER; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + initHotCache(); + for (size_t i = 0; i < psramMeshNodes.size(); ++i) + clearSlot(i); + nodeDatabase.nodes.clear(); +#else nodeDatabase.nodes = std::vector(MAX_NUM_NODES); - numMeshNodes = 0; meshNodes = &nodeDatabase.nodes; +#endif + numMeshNodes = 0; } void NodeDB::installDefaultConfig(bool preserveKey = false) @@ -988,8 +1170,18 @@ void NodeDB::resetNodes() { if (!config.position.fixed_position) clearLocalPosition(); +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (psramMeshNodes.empty()) + initHotCache(); + numMeshNodes = std::min(numMeshNodes, MAX_NUM_NODES); + if (numMeshNodes == 0) + numMeshNodes = 1; + for (size_t i = 1; i < psramMeshNodes.size(); ++i) + clearSlot(i); +#else numMeshNodes = 1; std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); +#endif devicestate.has_rx_text_message = false; devicestate.has_rx_waypoint = false; saveNodeDatabaseToDisk(); @@ -1000,7 +1192,25 @@ void NodeDB::resetNodes() void NodeDB::removeNodeByNum(NodeNum nodeNum) { - int newPos = 0, removed = 0; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + int newPos = 0; + int removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + if (hotNodes[i].num != nodeNum) { + if (newPos != i) + moveSlot(i, newPos); + newPos++; + } else { + removed++; + } + } + for (int i = newPos; i < numMeshNodes; i++) + clearSlot(i); + numMeshNodes -= removed; +#else + int newPos = 0; + int removed = 0; for (int i = 0; i < numMeshNodes; i++) { if (meshNodes->at(i).num != nodeNum) meshNodes->at(newPos++) = meshNodes->at(i); @@ -1010,6 +1220,7 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum) numMeshNodes -= removed; std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); +#endif LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); saveNodeDatabaseToDisk(); } @@ -1026,6 +1237,29 @@ void NodeDB::clearLocalPosition() void NodeDB::cleanupMeshDB() { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + int newPos = 0, removed = 0; + for (int i = 0; i < numMeshNodes; i++) { + auto &node = psramMeshNodes[i]; + if (node.has_user) { + if (node.user.public_key.size > 0) { + if (memfll(node.user.public_key.bytes, 0, node.user.public_key.size)) { + node.user.public_key.size = 0; + markHotDirty(i); + } + } + if (newPos != i) + moveSlot(i, newPos); + newPos++; + } else { + removed++; + } + } + for (int i = newPos; i < numMeshNodes; i++) + clearSlot(i); + numMeshNodes -= removed; +#else int newPos = 0, removed = 0; for (int i = 0; i < numMeshNodes; i++) { if (meshNodes->at(i).has_user) { @@ -1045,6 +1279,7 @@ void NodeDB::cleanupMeshDB() numMeshNodes -= removed; std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed, meshtastic_NodeInfoLite()); +#endif LOG_DEBUG("cleanupMeshDB purged %d entries", removed); } @@ -1198,16 +1433,41 @@ void NodeDB::loadFromDisk() LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); installDefaultNodeDatabase(); } else { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + initHotCache(); + size_t inserted = 0; + for (const auto &n : nodeDatabase.nodes) { + if (inserted >= MAX_NUM_NODES) + break; + if (isNodeEmpty(n)) + continue; + psramMeshNodes[inserted] = n; + hotDirty[inserted] = true; + syncHotFromCold(inserted); + ++inserted; + } + for (size_t i = inserted; i < psramMeshNodes.size(); ++i) + clearSlot(i); + numMeshNodes = inserted; + nodeDatabase.nodes.clear(); + LOG_INFO("Loaded saved nodedatabase version %d, with active nodes: %u", nodeDatabase.version, inserted); +#else meshNodes = &nodeDatabase.nodes; numMeshNodes = nodeDatabase.nodes.size(); LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); +#endif } +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (numMeshNodes > MAX_NUM_NODES) + numMeshNodes = MAX_NUM_NODES; +#else if (numMeshNodes > MAX_NUM_NODES) { LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); numMeshNodes = MAX_NUM_NODES; } meshNodes->resize(MAX_NUM_NODES); +#endif // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), @@ -1413,10 +1673,21 @@ bool NodeDB::saveNodeDatabaseToDisk() spiLock->lock(); FSCom.mkdir("/prefs"); spiLock->unlock(); +#endif +#if defined(CONFIG_IDF_TARGET_ESP32S3) + nodeDatabase.nodes.clear(); + nodeDatabase.nodes.reserve(numMeshNodes); + for (size_t i = 0; i < numMeshNodes; ++i) { + nodeDatabase.nodes.push_back(psramMeshNodes[i]); + } #endif size_t nodeDatabaseSize; pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); - return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); + bool success = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); +#if defined(CONFIG_IDF_TARGET_ESP32S3) + nodeDatabase.nodes.clear(); +#endif + return success; } bool NodeDB::saveToDiskNoRetry(int saveWhat) @@ -1497,10 +1768,18 @@ bool NodeDB::saveToDisk(int saveWhat) const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex) { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (readIndex < numMeshNodes) { + markHotDirty(readIndex); + return &psramMeshNodes[readIndex++]; + } + return NULL; +#else if (readIndex < numMeshNodes) return &meshNodes->at(readIndex++); else return NULL; +#endif } /// Given a node, return how many seconds in the past (vs now) that we last heard from it @@ -1533,12 +1812,27 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) size_t numseen = 0; // FIXME this implementation is kinda expensive +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + uint32_t now = getTime(); + for (int i = 0; i < numMeshNodes; i++) { + const NodeHotEntry &hot = hotNodes[i]; + if (localOnly && (hot.flags & HOT_FLAG_VIA_MQTT)) + continue; + int delta = static_cast(now - hot.last_heard); + if (delta < 0) + delta = 0; + if (delta < NUM_ONLINE_SECS) + numseen++; + } +#else for (int i = 0; i < numMeshNodes; i++) { if (localOnly && meshNodes->at(i).via_mqtt) continue; if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) numseen++; } +#endif return numseen; } @@ -1654,6 +1948,13 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) sortMeshDB(); notifyObservers(true); // Force an update whether or not our node counts have changed } +#if defined(CONFIG_IDF_TARGET_ESP32S3) + { + size_t idx = indexOf(info); + if (idx != std::numeric_limits::max()) + syncHotFromCold(idx); + } +#endif saveNodeDatabaseToDisk(); } @@ -1716,6 +2017,14 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde info->channel); info->has_user = true; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + { + size_t idx = indexOf(info); + if (idx != std::numeric_limits::max()) + syncHotFromCold(idx); + } +#endif + if (changed) { updateGUIforNode = info; notifyObservers(true); // Force an update whether or not our node counts have changed @@ -1763,6 +2072,13 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->has_hops_away = true; info->hops_away = mp.hop_start - mp.hop_limit; } +#if defined(CONFIG_IDF_TARGET_ESP32S3) + { + size_t idx = indexOf(info); + if (idx != std::numeric_limits::max()) + syncHotFromCold(idx); + } +#endif sortMeshDB(); } } @@ -1772,6 +2088,11 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite && lite->is_favorite != is_favorite) { lite->is_favorite = is_favorite; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + size_t idx = indexOf(lite); + if (idx != std::numeric_limits::max()) + syncHotFromCold(idx); +#endif sortMeshDB(); saveNodeDatabaseToDisk(); } @@ -1785,12 +2106,21 @@ bool NodeDB::isFavorite(uint32_t nodeId) if (nodeId == NODENUM_BROADCAST) return false; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + for (int i = 0; i < numMeshNodes; ++i) { + if (hotNodes[i].num == nodeId) + return (hotNodes[i].flags & HOT_FLAG_IS_FAVORITE) != 0; + } + return false; +#else meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite) { return lite->is_favorite; } return false; +#endif } bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) @@ -1804,11 +2134,33 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) if (p.to == NODENUM_BROADCAST) return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from - meshtastic_NodeInfoLite *lite = NULL; - bool seenFrom = false; bool seenTo = false; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + for (int i = 0; i < numMeshNodes; i++) { + const NodeHotEntry &hot = hotNodes[i]; + + if (hot.num == p.from) { + if (hot.flags & HOT_FLAG_IS_FAVORITE) + return true; + + seenFrom = true; + } + + if (hot.num == p.to) { + if (hot.flags & HOT_FLAG_IS_FAVORITE) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + } +#else + meshtastic_NodeInfoLite *lite = NULL; for (int i = 0; i < numMeshNodes; i++) { lite = &meshNodes->at(i); @@ -1832,6 +2184,7 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching // all favorited nodes first. } +#endif return false; } @@ -1848,6 +2201,27 @@ void NodeDB::sortMeshDB() bool changed = true; while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing changed = false; +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 + NodeHotEntry &prev = hotNodes[i - 1]; + NodeHotEntry &curr = hotNodes[i]; + if (prev.num == getNodeNum()) { + continue; + } else if (curr.num == getNodeNum()) { + swapSlots(i, i - 1); + changed = true; + } else if ((curr.flags & HOT_FLAG_IS_FAVORITE) && !(prev.flags & HOT_FLAG_IS_FAVORITE)) { + swapSlots(i, i - 1); + changed = true; + } else if (!(curr.flags & HOT_FLAG_IS_FAVORITE) && (prev.flags & HOT_FLAG_IS_FAVORITE)) { + continue; + } else if (curr.last_heard > prev.last_heard) { + swapSlots(i, i - 1); + changed = true; + } + } +#else for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1 if (meshNodes->at(i - 1).num == getNodeNum()) { // noop @@ -1866,6 +2240,7 @@ void NodeDB::sortMeshDB() changed = true; } } +#endif } LOG_INFO("Sort took %u milliseconds", millis() - lastSort); } @@ -1873,11 +2248,20 @@ void NodeDB::sortMeshDB() uint8_t NodeDB::getMeshNodeChannel(NodeNum n) { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + refreshHotCache(); + for (int i = 0; i < numMeshNodes; ++i) { + if (hotNodes[i].num == n) + return hotNodes[i].channel; + } + return 0; +#else const meshtastic_NodeInfoLite *info = getMeshNode(n); if (!info) { return 0; // defaults to PRIMARY } return info->channel; +#endif } std::string NodeDB::getNodeId() const @@ -1891,11 +2275,21 @@ std::string NodeDB::getNodeId() const /// NOTE: This function might be called from an ISR meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n) { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + for (int i = 0; i < numMeshNodes; i++) { + if (hotNodes[i].num == n) { + markHotDirty(i); + return &psramMeshNodes[i]; + } + } + return NULL; +#else for (int i = 0; i < numMeshNodes; i++) if (meshNodes->at(i).num == n) return &meshNodes->at(i); return NULL; +#endif } // returns true if the maximum number of nodes is reached or we are running low on memory @@ -1907,6 +2301,60 @@ bool NodeDB::isFull() /// Find a node in our DB, create an empty NodeInfo if missing meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + meshtastic_NodeInfoLite *lite = getMeshNode(n); + + if (!lite) { + if (isFull()) { + LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes, + memGet.getFreeHeap()); + refreshHotCache(); + uint32_t oldest = UINT32_MAX; + uint32_t oldestBoring = UINT32_MAX; + int oldestIndex = -1; + int oldestBoringIndex = -1; + for (int i = 1; i < numMeshNodes; i++) { + const NodeHotEntry &hot = hotNodes[i]; + if (!(hot.flags & HOT_FLAG_IS_FAVORITE) && !(hot.flags & HOT_FLAG_IS_IGNORED) && + !(hot.flags & HOT_FLAG_IS_KEY_VERIFIED) && hot.last_heard < oldest) { + oldest = hot.last_heard; + oldestIndex = i; + } + const auto &coldNode = psramMeshNodes[i]; + if (!(hot.flags & HOT_FLAG_IS_FAVORITE) && !(hot.flags & HOT_FLAG_IS_IGNORED) && + coldNode.user.public_key.size == 0 && hot.last_heard < oldestBoring) { + oldestBoring = hot.last_heard; + oldestBoringIndex = i; + } + } + if (oldestBoringIndex != -1) + oldestIndex = oldestBoringIndex; + + if (oldestIndex != -1) { + for (int i = oldestIndex; i < numMeshNodes - 1; i++) + copySlot(i + 1, i); + clearSlot(numMeshNodes - 1); + (numMeshNodes)--; + } + } + + if (numMeshNodes >= MAX_NUM_NODES) { + LOG_WARN("Unable to allocate new node %u, MAX_NUM_NODES reached", static_cast(n)); + return NULL; + } + + size_t index = numMeshNodes++; + clearSlot(index); + psramMeshNodes[index].num = n; + syncHotFromCold(index); + lite = &psramMeshNodes[index]; + LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); + logNodeInsertStats(numMeshNodes, "PSRAM"); + } + + markHotDirty(lite); + return lite; +#else meshtastic_NodeInfoLite *lite = getMeshNode(n); if (!lite) { @@ -1953,9 +2401,11 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) memset(lite, 0, sizeof(*lite)); lite->num = n; LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap()); + logNodeInsertStats(numMeshNodes, "Heap"); } return lite; +#endif } /// Sometimes we will have Position objects that only have a time, so check for diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index e8724f2c95..fc65b76ac0 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -4,9 +4,13 @@ #include #include #include +#include #include #include #include +#if defined(CONFIG_IDF_TARGET_ESP32S3) +#include +#endif #include "MeshTypes.h" #include "NodeStatus.h" @@ -14,6 +18,53 @@ #include "mesh-pb-constants.h" #include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode +#if defined(CONFIG_IDF_TARGET_ESP32S3) +/** + * Custom allocator that redirects NodeInfoLite storage into PSRAM so that the + * heavy payload stays out of internal RAM on ESP32-S3 devices. + */ +template struct PsramAllocator { + using value_type = T; + + PsramAllocator() noexcept = default; + + template PsramAllocator(const PsramAllocator &) noexcept {} + + [[nodiscard]] T *allocate(std::size_t n) + { + void *ptr = heap_caps_malloc(n * sizeof(T), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!ptr) + throw std::bad_alloc(); + return static_cast(ptr); + } + + void deallocate(T *p, std::size_t) noexcept + { + if (p) + heap_caps_free(p); + } + + template bool operator==(const PsramAllocator &) const noexcept { return true; } + template bool operator!=(const PsramAllocator &) const noexcept { return false; } +}; + +/** Lightweight DRAM copy of the latency-sensitive node fields. */ +struct NodeHotEntry { + uint32_t num = 0; + uint32_t last_heard = 0; + float snr = 0.0f; + uint8_t role = meshtastic_Config_DeviceConfig_Role_CLIENT; + uint8_t channel = 0; + uint8_t next_hop = 0; + uint8_t hops_away = 0; + uint8_t flags = 0; // bitmask, see NodeDB::HotFlags +}; + +using NodeInfoLiteVector = std::vector>; +#else +using NodeInfoLiteVector = std::vector; +#endif + #if ARCH_PORTDUINO #include "PortduinoGlue.h" #endif @@ -135,7 +186,7 @@ class NodeDB // Note: these two references just point into our static array we serialize to/from disk public: - std::vector *meshNodes; + NodeInfoLiteVector *meshNodes; bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI Observable newStatus; @@ -245,7 +296,12 @@ class NodeDB meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x) { assert(x < numMeshNodes); +#if defined(CONFIG_IDF_TARGET_ESP32S3) + markHotDirty(x); + return &psramMeshNodes[x]; +#else return &meshNodes->at(x); +#endif } virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); @@ -330,6 +386,31 @@ class NodeDB bool saveDeviceStateToDisk(); bool saveNodeDatabaseToDisk(); void sortMeshDB(); +#if defined(CONFIG_IDF_TARGET_ESP32S3) + enum HotFlags : uint8_t { + HOT_FLAG_VIA_MQTT = 1 << 0, + HOT_FLAG_IS_FAVORITE = 1 << 1, + HOT_FLAG_IS_IGNORED = 1 << 2, + HOT_FLAG_HAS_HOPS = 1 << 3, + HOT_FLAG_IS_KEY_VERIFIED = 1 << 4 + }; + + void initHotCache(); + void refreshHotCache(); + void syncHotFromCold(size_t index); + void markHotDirty(size_t index); + void markHotDirty(const meshtastic_NodeInfoLite *ptr); + void clearSlot(size_t index); + void swapSlots(size_t a, size_t b); + void copySlot(size_t src, size_t dst); + void moveSlot(size_t src, size_t dst); + bool isNodeEmpty(const meshtastic_NodeInfoLite &node) const; + size_t indexOf(const meshtastic_NodeInfoLite *ptr) const; + + NodeInfoLiteVector psramMeshNodes; + std::vector hotNodes; + std::vector hotDirty; +#endif }; extern NodeDB *nodeDB; @@ -372,4 +453,4 @@ extern uint32_t error_address; ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \ ModuleConfig_TelemetryConfig_size + ModuleConfig_size) -// Please do not remove this comment, it makes trunk and compiler happy at the same time. \ No newline at end of file +// Please do not remove this comment, it makes trunk and compiler happy at the same time. diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index b4af707ae8..cb839b59f9 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -1,4 +1,5 @@ #include "PacketHistory.h" +#include "MemoryPool.h" #include "configuration.h" #include "mesh-pb-constants.h" @@ -16,32 +17,61 @@ #define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging #define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots -PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members +PacketHistory::PacketHistory(uint32_t size) + : recentPacketsCapacity(0), recentPackets(NULL), recentPacketsInPsram(false) // Initialize members { if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); size = PACKETHISTORY_MAX; // Use default size if invalid } + LOG_DEBUG("Packet History - pre-alloc heap %u psram %u", memGet.getFreeHeap(), memGet.getFreePsram()); + // Allocate memory for the recent packets array recentPacketsCapacity = size; - recentPackets = new PacketRecord[recentPacketsCapacity]; - if (!recentPackets) { // No logging here, console/log probably uninitialized yet. - LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, - sizeof(PacketRecord) * recentPacketsCapacity); - recentPacketsCapacity = 0; // mark allocation fail - return; // return early + if (has_psram()) { + // Prefer PSRAM so the large history pool stays out of internal RAM on ESP32-S3 builds. + recentPackets = psramAllocArray(recentPacketsCapacity); + if (recentPackets) { + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsInPsram = true; + LOG_DEBUG("Packet History - allocated %u entries in PSRAM, free heap %u psram %u", recentPacketsCapacity, + memGet.getFreeHeap(), memGet.getFreePsram()); + } else { + LOG_WARN("Packet History - PSRAM allocation failed, falling back to DRAM"); + } } - // Initialize the recent packets array to zero - memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + if (!recentPackets) { + // Fall back to DRAM if PSRAM is unavailable or exhausted. + recentPackets = new PacketRecord[recentPacketsCapacity]; + if (!recentPackets) { // No logging here, console/log probably uninitialized yet. + LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, + sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsCapacity = 0; // mark allocation fail + return; // return early + } + + // Initialize the recent packets array to zero + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); + LOG_DEBUG("Packet History - allocated %u entries in DRAM, free heap %u psram %u", recentPacketsCapacity, + memGet.getFreeHeap(), memGet.getFreePsram()); + } } PacketHistory::~PacketHistory() { - recentPacketsCapacity = 0; - delete[] recentPackets; + if (recentPackets) { + // Release via the allocator that produced the buffer. + if (recentPacketsInPsram) + psramFreeArray(recentPackets); + else + delete[] recentPackets; + } + recentPackets = NULL; + recentPacketsCapacity = 0; + recentPacketsInPsram = false; } /** Update recentPackets and return true if we have already seen this packet */ @@ -457,4 +487,4 @@ inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) inline void PacketHistory::setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit) { r.hop_limit = (r.hop_limit & ~HOP_LIMIT_OUR_TX_MASK) | ((hopLimit << HOP_LIMIT_OUR_TX_SHIFT) & HOP_LIMIT_OUR_TX_MASK); -} \ No newline at end of file +} diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 5fbad2dc94..4f06ed4e04 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -27,6 +27,7 @@ class PacketHistory uint32_t recentPacketsCapacity = 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. + bool recentPacketsInPsram = false; // Remember backing store so we free via the matching allocator. /** Find a packet record in history. * @param sender NodeNum diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 725477eaee..8d71bc0b7e 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -120,4 +120,4 @@ template class ProtobufModule : protected SinglePortModule return alterReceivedProtobuf(mp, decoded); } } -}; \ No newline at end of file +}; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 5cf8bfa7df..22d185552d 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -52,8 +52,21 @@ Allocator &packetPool = dynamicPool; (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM) +// Try to put the heavy MeshPacket pool into PSRAM. If that fails we fall back to +// heap allocation so the radio stays functional (at the cost of fewer packets). +static PsramMemoryPool psramPool; +static MemoryDynamic fallbackPool; +Allocator &packetPool = psramPool.isValid() + ? static_cast &>(psramPool) + : static_cast &>(fallbackPool); +#elif defined(CONFIG_IDF_TARGET_ESP32S3) static MemoryPool staticPool; Allocator &packetPool = staticPool; +#else +static MemoryPool staticPool; +Allocator &packetPool = staticPool; +#endif #endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index e4f65aa283..0aaca57a5c 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -1,6 +1,7 @@ #pragma once #include +#include "memGet.h" #include "mesh/generated/meshtastic/admin.pb.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/localonly.pb.h" @@ -11,11 +12,58 @@ // Tricky macro to let you find the sizeof a type member #define member_size(type, member) sizeof(((type *)0)->member) +// Minimum PSRAM the firmware expects before enabling the "expanded" queues that +// rely on off-chip RAM instead of internal DRAM. Currently set to 2MB to +// accommodate Heltec WiFi LoRa 32 V4 boards (and others) +static constexpr size_t PSRAM_LARGE_THRESHOLD_BYTES = 2 * 1024 * 1024; + +// Default RX queue size for phone delivery when PSRAM is available +// This is an arbitrary default bump from default, boards can override +// this in board.h +static constexpr int RX_TOPHONE_WITH_PSRAM_DEFAULT = 100; + +inline bool has_psram(size_t minimumBytes = PSRAM_LARGE_THRESHOLD_BYTES) +{ +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) + return memGet.getPsramSize() >= minimumBytes; +#else + (void)minimumBytes; + return false; +#endif +} + +// Runtime cap used to keep the BLE message queue from overflowing low-memory +// S3 variants if PSRAM is smaller than expected or temporarily unavailable. +inline int get_rx_tophone_limit() +{ +#if defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(BOARD_MAX_RX_TOPHONE) + return BOARD_MAX_RX_TOPHONE; +#elif defined(BOARD_HAS_PSRAM) + return RX_TOPHONE_WITH_PSRAM_DEFAULT; +#else + return 32; +#endif +#elif defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + return 8; +#else + return 32; +#endif +} + /// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options protobuf // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE -#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) +#if defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(BOARD_MAX_RX_TOPHONE) +#define MAX_RX_TOPHONE BOARD_MAX_RX_TOPHONE +#elif defined(BOARD_HAS_PSRAM) +#define MAX_RX_TOPHONE RX_TOPHONE_WITH_PSRAM_DEFAULT +#else +#define MAX_RX_TOPHONE 32 +#endif +#elif defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) #define MAX_RX_TOPHONE 8 #else #define MAX_RX_TOPHONE 32 @@ -48,19 +96,24 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increas #elif defined(ARCH_NRF52) #define MAX_NUM_NODES 80 #elif defined(CONFIG_IDF_TARGET_ESP32S3) +#if defined(BOARD_MAX_NUM_NODES) +#define MAX_NUM_NODES BOARD_MAX_NUM_NODES +#elif defined(BOARD_HAS_PSRAM) +#define MAX_NUM_NODES 3000 +#else #include "Esp.h" static inline int get_max_num_nodes() { - uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Convert Bytes to MB + uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Fallback based on flash size if (flash_size >= 15) { return 250; } else if (flash_size >= 7) { return 200; - } else { - return 100; } + return 100; } #define MAX_NUM_NODES get_max_num_nodes() +#endif #else #define MAX_NUM_NODES 100 #endif @@ -90,4 +143,4 @@ bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count); */ bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count); -#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) \ No newline at end of file +#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index d300ff53b8..a72e429d38 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -327,9 +327,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } case meshtastic_AdminMessage_set_favorite_node_tag: { LOG_INFO("Client received set_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); - if (node != NULL) { - node->is_favorite = true; + if (nodeDB->getMeshNode(r->set_favorite_node) != NULL) { + nodeDB->set_favorite(true, r->set_favorite_node); saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens @@ -338,9 +337,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } case meshtastic_AdminMessage_remove_favorite_node_tag: { LOG_INFO("Client received remove_favorite_node command"); - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); - if (node != NULL) { - node->is_favorite = false; + if (nodeDB->getMeshNode(r->remove_favorite_node) != NULL) { + nodeDB->set_favorite(false, r->remove_favorite_node); saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens