This commit is contained in:
Clive Blackledge 2025-10-24 15:14:37 +02:00 committed by GitHub
commit b1a6013919
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 816 additions and 56 deletions

View File

@ -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();

View File

@ -14,16 +14,16 @@ class NodeStatus : public Status
CallbackObserver<NodeStatus, const NodeStatus *> statusObserver =
CallbackObserver<NodeStatus, const NodeStatus *>(this, &NodeStatus::updateStatus);
uint8_t numOnline = 0;
uint8_t numTotal = 0;
uint16_t numOnline = 0;
uint16_t numTotal = 0;
uint8_t lastNumTotal = 0;
uint16_t lastNumTotal = 0;
public:
bool forceUpdate = false;
NodeStatus() { statusType = STATUS_TYPE_NODE; }
NodeStatus(uint8_t numOnline, uint8_t numTotal, bool forceUpdate = false) : Status()
NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status()
{
this->forceUpdate = forceUpdate;
this->numOnline = numOnline;
@ -34,11 +34,11 @@ class NodeStatus : public Status
void observe(Observable<const NodeStatus *> *source) { statusObserver.observe(source); }
uint8_t getNumOnline() const { return numOnline; }
uint16_t getNumOnline() const { return numOnline; }
uint8_t getNumTotal() const { return numTotal; }
uint16_t getNumTotal() const { return numTotal; }
uint8_t getLastNumTotal() const { return lastNumTotal; }
uint16_t getLastNumTotal() const { return lastNumTotal; }
bool matches(const NodeStatus *newStatus) const
{

View File

@ -287,7 +287,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
float easternmost = lngCenter;
float westernmost = lngCenter;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
@ -475,7 +475,7 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
bool InkHUD::MapApplet::enoughMarkers()
{
uint8_t count = 0;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Count nodes

View File

@ -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<ScanI2CTwoWire> &i2cScanner, Scan
}
#endif
#ifndef PIO_UNIT_TESTING
#if !defined(PIO_UNIT_TESTING) || !(PIO_UNIT_TESTING)
void loop()
{
runASAP = false;

View File

@ -8,6 +8,10 @@
#include "PointerQueue.h"
#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
#if defined(ARCH_ESP32)
#include <esp_heap_caps.h>
#endif
template <class T> class Allocator
{
@ -159,3 +163,92 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
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 T, int MaxSize> class PsramMemoryPool : public Allocator<T>
{
private:
T *pool;
bool used[MaxSize];
public:
PsramMemoryPool() : pool(nullptr), used{}
{
pool = static_cast<T *>(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<int>(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 <typename T> inline T *psramAllocArray(size_t count)
{
return static_cast<T *>(heap_caps_malloc(sizeof(T) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
}
template <typename T> inline void psramFreeArray(T *ptr)
{
if (ptr)
heap_caps_free(ptr);
}
#else
template <typename T> inline T *psramAllocArray(size_t count)
{
(void)count;
return nullptr;
}
template <typename T> inline void psramFreeArray(T *ptr)
{
(void)ptr;
}
#endif

View File

@ -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;

View File

@ -24,6 +24,7 @@
#include "modules/NeighborInfoModule.h"
#include <ErriezCRC32.h>
#include <algorithm>
#include <limits>
#include <pb_decode.h>
#include <pb_encode.h>
#include <vector>
@ -37,6 +38,12 @@
#include <Preferences.h>
#include <esp_efuse.h>
#include <esp_efuse_table.h>
#if __has_include(<esp_ptr.h>)
#include <esp_ptr.h>
#define NODEDB_HAS_ESP_PTR 1
#else
#define NODEDB_HAS_ESP_PTR 0
#endif
#include <nvs_flash.h>
#include <soc/efuse_reg.h>
#include <soc/soc.h>
@ -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<unsigned>(count),
static_cast<unsigned>(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<unsigned>(capacity), static_cast<unsigned>(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<uint8_t>(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<size_t>::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<size_t>::max();
const meshtastic_NodeInfoLite *base = psramMeshNodes.data();
size_t idx = static_cast<size_t>(ptr - base);
if (idx >= psramMeshNodes.size())
return std::numeric_limits<size_t>::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<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
numMeshNodes = 0;
meshNodes = &nodeDatabase.nodes;
#endif
numMeshNodes = 0;
}
void NodeDB::installDefaultConfig(bool preserveKey = false)
@ -982,8 +1164,18 @@ void NodeDB::resetNodes()
{
if (!config.position.fixed_position)
clearLocalPosition();
#if defined(CONFIG_IDF_TARGET_ESP32S3)
if (psramMeshNodes.empty())
initHotCache();
numMeshNodes = std::min<pb_size_t>(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();
@ -994,7 +1186,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);
@ -1004,6 +1214,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();
}
@ -1020,6 +1231,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) {
@ -1039,6 +1273,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);
}
@ -1192,16 +1427,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),
@ -1407,10 +1667,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)
@ -1491,10 +1762,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
@ -1527,12 +1806,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<int>(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;
}
@ -1648,6 +1942,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<size_t>::max())
syncHotFromCold(idx);
}
#endif
saveNodeDatabaseToDisk();
}
@ -1710,6 +2011,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<size_t>::max())
syncHotFromCold(idx);
}
#endif
if (changed) {
updateGUIforNode = info;
notifyObservers(true); // Force an update whether or not our node counts have changed
@ -1757,6 +2066,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<size_t>::max())
syncHotFromCold(idx);
}
#endif
sortMeshDB();
}
}
@ -1766,6 +2082,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<size_t>::max())
syncHotFromCold(idx);
#endif
sortMeshDB();
saveNodeDatabaseToDisk();
}
@ -1779,12 +2100,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)
@ -1798,11 +2128,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);
@ -1826,6 +2178,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;
}
@ -1842,6 +2195,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
@ -1860,6 +2234,7 @@ void NodeDB::sortMeshDB()
changed = true;
}
}
#endif
}
LOG_INFO("Sort took %u milliseconds", millis() - lastSort);
}
@ -1867,11 +2242,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
@ -1885,11 +2269,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
@ -1901,6 +2295,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<unsigned>(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) {
@ -1947,9 +2395,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

View File

@ -4,9 +4,13 @@
#include <Arduino.h>
#include <algorithm>
#include <assert.h>
#include <new>
#include <pb_encode.h>
#include <string>
#include <vector>
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#include <esp_heap_caps.h>
#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 <class T> struct PsramAllocator {
using value_type = T;
PsramAllocator() noexcept = default;
template <class U> PsramAllocator(const PsramAllocator<U> &) 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<T *>(ptr);
}
void deallocate(T *p, std::size_t) noexcept
{
if (p)
heap_caps_free(p);
}
template <class U> bool operator==(const PsramAllocator<U> &) const noexcept { return true; }
template <class U> bool operator!=(const PsramAllocator<U> &) 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<meshtastic_NodeInfoLite, PsramAllocator<meshtastic_NodeInfoLite>>;
#else
using NodeInfoLiteVector = std::vector<meshtastic_NodeInfoLite>;
#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<meshtastic_NodeInfoLite> *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<const meshtastic::NodeStatus *> 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<NodeHotEntry> hotNodes;
std::vector<bool> hotDirty;
#endif
};
extern NodeDB *nodeDB;

View File

@ -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<PacketRecord>(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 */

View File

@ -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

View File

@ -13,7 +13,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
const pb_msgdesc_t *fields;
public:
uint8_t numOnlineNodes = 0;
uint16_t numOnlineNodes = 0;
/** Constructor
* name is for debugging output
*/

View File

@ -52,8 +52,21 @@ Allocator<meshtastic_MeshPacket> &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<meshtastic_MeshPacket, MAX_PACKETS_STATIC> psramPool;
static MemoryDynamic<meshtastic_MeshPacket> fallbackPool;
Allocator<meshtastic_MeshPacket> &packetPool = psramPool.isValid()
? static_cast<Allocator<meshtastic_MeshPacket> &>(psramPool)
: static_cast<Allocator<meshtastic_MeshPacket> &>(fallbackPool);
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool;
Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
#else
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool;
Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
#endif
#endif
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));

View File

@ -1,6 +1,7 @@
#pragma once
#include <vector>
#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

View File

@ -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