From f5c2c991501bef00c29b9aecc5ff699bc3dcaff6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 7 May 2026 08:37:56 -0500 Subject: [PATCH] 2.8: NodeDB refactor to decouple satellite entries and decrease size --- src/GPSStatus.h | 12 +- src/graphics/Screen.cpp | 4 +- src/graphics/draw/MenuHandler.cpp | 24 +- src/graphics/draw/MessageRenderer.cpp | 37 +- src/graphics/draw/NodeListRenderer.cpp | 76 +-- src/graphics/draw/NotificationRenderer.cpp | 10 +- src/graphics/draw/UIRenderer.cpp | 77 ++- src/graphics/niche/InkHUD/Applet.cpp | 4 +- .../InkHUD/Applets/Bases/Map/MapApplet.cpp | 53 +- .../Applets/Bases/NodeList/NodeListApplet.cpp | 20 +- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 8 +- .../Notification/NotificationApplet.cpp | 4 +- .../User/AllMessage/AllMessageApplet.cpp | 4 +- .../niche/InkHUD/Applets/User/DM/DMApplet.cpp | 4 +- .../User/FavoritesMap/FavoritesMapApplet.cpp | 6 +- .../InkHUD/Applets/User/Heard/HeardApplet.cpp | 16 +- src/mesh/CryptoEngine.cpp | 4 +- src/mesh/CryptoEngine.h | 7 +- src/mesh/MeshService.cpp | 25 +- src/mesh/NodeDB.cpp | 597 +++++++++++++++--- src/mesh/NodeDB.h | 101 ++- src/mesh/PhoneAPI.cpp | 450 ++++++++++++- src/mesh/PhoneAPI.h | 45 +- src/mesh/ProtobufModule.h | 2 +- src/mesh/ReliableRouter.cpp | 2 +- src/mesh/Router.cpp | 32 +- src/mesh/TypeConversions.cpp | 114 ++-- src/mesh/TypeConversions.h | 11 +- .../generated/meshtastic/deviceonly.pb.cpp | 12 + src/mesh/generated/meshtastic/deviceonly.pb.h | 171 +++-- .../meshtastic/deviceonly_legacy.pb.cpp | 15 + .../meshtastic/deviceonly_legacy.pb.h | 122 ++++ src/mesh/http/ContentHandler.cpp | 30 +- src/mesh/mesh-pb-constants.h | 40 +- src/modules/AdminModule.cpp | 25 +- src/modules/CannedMessageModule.cpp | 24 +- src/modules/KeyVerificationModule.cpp | 12 +- src/modules/PositionModule.cpp | 30 +- src/modules/RangeTestModule.cpp | 29 +- src/modules/RoutingModule.cpp | 3 +- src/modules/SerialModule.cpp | 21 +- src/modules/StatusMessageModule.cpp | 13 +- src/modules/StatusMessageModule.h | 19 +- .../Telemetry/EnvironmentTelemetry.cpp | 4 + src/modules/TraceRouteModule.cpp | 10 +- src/modules/TrafficManagementModule.cpp | 4 +- src/modules/WaypointModule.cpp | 5 +- src/mqtt/MQTT.cpp | 2 +- src/serialization/MeshPacketSerializer.cpp | 4 +- .../MeshPacketSerializer_nRF52.cpp | 4 +- test/test_crypto/test_main.cpp | 2 +- test/test_traffic_management/test_main.cpp | 8 +- test/test_type_conversions/test_main.cpp | 406 ++++++++++++ 53 files changed, 2241 insertions(+), 523 deletions(-) create mode 100644 src/mesh/generated/meshtastic/deviceonly_legacy.pb.cpp create mode 100644 src/mesh/generated/meshtastic/deviceonly_legacy.pb.h create mode 100644 test/test_type_conversions/test_main.cpp diff --git a/src/GPSStatus.h b/src/GPSStatus.h index a1a9f2c56..981eaa073 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -53,8 +53,8 @@ class GPSStatus : public Status int32_t getLatitude() const { if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.latitude_i; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(nodeDB->getNodeNum()); + return pos ? pos->latitude_i : 0; } else { return p.latitude_i; } @@ -63,8 +63,8 @@ class GPSStatus : public Status int32_t getLongitude() const { if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.longitude_i; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(nodeDB->getNodeNum()); + return pos ? pos->longitude_i : 0; } else { return p.longitude_i; } @@ -73,8 +73,8 @@ class GPSStatus : public Status int32_t getAltitude() const { if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.altitude; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(nodeDB->getNodeNum()); + return pos ? pos->altitude : 0; } else { return p.altitude; } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d02938df9..205e71bcf 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1285,7 +1285,7 @@ void Screen::setFrames(FrameFocus focus) for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) { favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode); } } @@ -1670,7 +1670,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_Channel channel = channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); - const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + const char *longName = nodeInfoLiteHasUser(node) ? node->long_name : nullptr; const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f31cb405b..e1783bf88 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -847,8 +847,8 @@ void menuHandler::messageViewModeMenu() uint32_t peer = uniquePeers[i]; auto node = nodeDB->getMeshNode(peer); std::string name; - if (node && node->has_user) - name = sanitizeString(node->user.long_name).substr(0, 15); + if (nodeInfoLiteHasUser(node)) + name = sanitizeString(node->long_name).substr(0, 15); else { char buf[20]; snprintf(buf, sizeof(buf), "Node %08X", peer); @@ -1355,7 +1355,7 @@ void menuHandler::manageNodeMenu() static int optionsEnumArray[enumEnd] = {Back}; int options = 1; - if (node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { optionsArray[options] = "Unfavorite"; } else { optionsArray[options] = "Favorite"; @@ -1376,7 +1376,7 @@ void menuHandler::manageNodeMenu() optionsArray[options] = "Key Verification"; optionsEnumArray[options++] = KeyVerification; - if (node->is_ignored) { + if (nodeInfoLiteIsIgnored(node)) { optionsArray[options] = "Unignore Node"; } else { optionsArray[options] = "Ignore Node"; @@ -1386,8 +1386,8 @@ void menuHandler::manageNodeMenu() BannerOverlayOptions bannerOptions; std::string title = ""; - if (node->has_user && node->user.long_name && node->user.long_name[0]) { - title += sanitizeString(node->user.long_name).substr(0, 15); + if (nodeInfoLiteHasUser(node) && node->long_name[0]) { + title += sanitizeString(node->long_name).substr(0, 15); } else { char buf[20]; snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); @@ -1409,7 +1409,7 @@ void menuHandler::manageNodeMenu() if (!n) { return; } - if (n->is_favorite) { + if (nodeInfoLiteIsFavorite(n)) { LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); nodeDB->set_favorite(false, menuHandler::pickedNodeNum); } else { @@ -1461,11 +1461,11 @@ void menuHandler::manageNodeMenu() return; } - if (n->is_ignored) { - n->is_ignored = false; + if (nodeInfoLiteIsIgnored(n)) { + nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false); LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); } else { - n->is_ignored = true; + nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true); LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); } nodeDB->notifyObservers(true); @@ -2080,8 +2080,8 @@ void menuHandler::removeFavoriteMenu() BannerOverlayOptions bannerOptions; std::string message = "Unfavorite This Node?\n"; auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); - if (node && node->has_user) { - message += sanitizeString(node->user.long_name).substr(0, 15); + if (nodeInfoLiteHasUser(node)) { + message += sanitizeString(node->long_name).substr(0, 15); } bannerOptions.message = message.c_str(); bannerOptions.optionsArrayPtr = optionsArray; diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 2260c57df..a42ef1e08 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -462,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } case ThreadMode::DIRECT: { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); - if (node && node->has_user && node->user.short_name[0]) { - snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); + if (nodeInfoLiteHasUser(node) && node->short_name[0]) { + snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name); } else { snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); } @@ -585,11 +585,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); char senderName[64] = ""; - if (node && node->has_user) { - if (node->user.long_name[0]) { - strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); - } else if (node->user.short_name[0]) { - strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); + if (nodeInfoLiteHasUser(node)) { + if (node->long_name[0]) { + strncpy(senderName, node->long_name, sizeof(senderName) - 1); + } else if (node->short_name[0]) { + strncpy(senderName, node->short_name, sizeof(senderName) - 1); } senderName[sizeof(senderName) - 1] = '\0'; } @@ -599,18 +599,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // If this is *our own* message, override senderName to who the recipient was bool mine = (m.sender == nodeDB->getNodeNum()); - if (mine && node_recipient && node_recipient->has_user) { - if (node_recipient->user.long_name[0]) { - strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); + if (mine && nodeInfoLiteHasUser(node_recipient)) { + if (node_recipient->long_name[0]) { + strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1); senderName[sizeof(senderName) - 1] = '\0'; - } else if (node_recipient->user.short_name[0]) { - strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); + } else if (node_recipient->short_name[0]) { + strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1); senderName[sizeof(senderName) - 1] = '\0'; } } // If recipient info is missing/empty, prefer a recipient identifier for outbound messages. - if (mine && (!node_recipient || !node_recipient->has_user || - (!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) { + if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) { snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); } @@ -1073,12 +1072,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht // Banner logic const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); char longName[64] = "?"; - if (node && node->has_user) { - if (node->user.long_name[0]) { - strncpy(longName, node->user.long_name, sizeof(longName) - 1); + if (nodeInfoLiteHasUser(node)) { + if (node->long_name[0]) { + strncpy(longName, node->long_name, sizeof(longName) - 1); longName[sizeof(longName) - 1] = '\0'; - } else if (node->user.short_name[0]) { - strncpy(longName, node->user.short_name, sizeof(longName) - 1); + } else if (node->short_name[0]) { + strncpy(longName, node->short_name, sizeof(longName) - 1); longName[sizeof(longName) - 1] = '\0'; } } diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 00ae74b58..eed99b020 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -98,29 +98,23 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, // 1) Choose target candidate (long vs short) only if present const char *raw = nullptr; -#if !MESHTASTIC_EXCLUDE_STATUS +#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB // If long-name mode is enabled, and we have a recent status for this node, - // prefer "(short_name) statusText" as the raw candidate. + // prefer "(short_name) statusText" as the raw candidate. Pull straight out + // of NodeDB's per-NodeNum cache instead of scanning a FIFO. std::string composedFromStatus; - if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) { - const auto &recent = statusMessageModule->getRecentReceived(); - const StatusMessageModule::RecentStatus *found = nullptr; - for (auto it = recent.rbegin(); it != recent.rend(); ++it) { - if (it->fromNodeId == node->num && !it->statusText.empty()) { - found = &(*it); - break; - } - } - - if (found) { - const char *shortName = node->user.short_name; - composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size()); + if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) { + const meshtastic_StatusMessage *cachedStatus = nodeDB->getNodeStatus(node->num); + if (cachedStatus && cachedStatus->status[0]) { + const char *shortName = node->short_name; + const size_t statusLen = std::strlen(cachedStatus->status); + composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen); composedFromStatus += "("; if (shortName && *shortName) { composedFromStatus += shortName; } composedFromStatus += ") "; - composedFromStatus += found->statusText; + composedFromStatus += cachedStatus->status; raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string } @@ -129,8 +123,8 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, // If we didn't compose from status, use normal long/short selection if (!raw) { - if (node && node->has_user) { - raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; + if (nodeInfoLiteHasUser(node)) { + raw = config.display.use_long_node_name ? node->long_name : node->short_name; } } @@ -218,7 +212,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName, int16_t nameX, int16_t y, int nameMaxWidth) { - if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) { + if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) { return; } @@ -279,14 +273,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); - if (node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } - if (node->is_ignored || isMuted) { + if (nodeInfoLiteIsIgnored(node) || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { @@ -327,14 +321,14 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); - if (node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } - if (node->is_ignored || isMuted) { + if (nodeInfoLiteIsIgnored(node) || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { @@ -405,11 +399,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; + const meshtastic_PositionLite *ourPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + const meshtastic_PositionLite *theirPos = nodeDB->getNodePosition(node->num); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && ourPos && theirPos) { + double lat1 = ourPos->latitude_i * 1e-7; + double lon1 = ourPos->longitude_i * 1e-7; + double lat2 = theirPos->latitude_i * 1e-7; + double lon2 = theirPos->longitude_i * 1e-7; double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; @@ -455,14 +451,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); - if (node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } - if (node->is_ignored || isMuted) { + if (nodeInfoLiteIsIgnored(node) || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { @@ -514,14 +510,14 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); - if (node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } - if (node->is_ignored || isMuted) { + if (nodeInfoLiteIsIgnored(node) || isMuted) { if (currentResolution == ScreenResolution::High) { display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); } else { @@ -542,8 +538,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; - double nodeLat = node->position.latitude_i * 1e-7; - double nodeLon = node->position.longitude_i * 1e-7; + const meshtastic_PositionLite *nodePos = nodeDB->getNodePosition(node->num); + if (!nodePos) + return; + double nodeLat = nodePos->latitude_i * 1e-7; + double nodeLon = nodePos->longitude_i * 1e-7; float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian); float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing); @@ -652,7 +651,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t continue; if (n->num == nodeDB->getNodeNum()) continue; - if (locationScreen && !n->has_position) + if (locationScreen && !nodeDB->hasNodePosition(n->num)) continue; drawList.push_back(n->num); @@ -884,13 +883,14 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, { float headingRadian = 0.0f; auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { + const meshtastic_PositionLite *ourSelfPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !ourSelfPos) { drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0); return; } - double lat = DegD(ourNode->position.latitude_i); - double lon = DegD(ourNode->position.longitude_i); + double lat = DegD(ourSelfPos->latitude_i); + double lon = DegD(ourSelfPos->longitude_i); #if defined(M5STACK_UNITC6L) display->clear(); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index cca60d1e2..8d031cf73 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -317,12 +317,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { char tempName[48] = {0}; meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); - if (node && node->has_user) { + if (nodeInfoLiteHasUser(node)) { const char *rawName = nullptr; - if (node->user.long_name[0]) { - rawName = node->user.long_name; - } else if (node->user.short_name[0]) { - rawName = node->user.short_name; + if (node->long_name[0]) { + rawName = node->long_name; + } else if (node->short_name[0]) { + rawName = node->short_name; } if (rawName) { const int arrowWidth = (currentResolution == ScreenResolution::High) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index b75bcd17b..c7e6eaf11 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -464,11 +464,11 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset, return true; } -static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const std::string &statusText) +static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const char *statusText) { // Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down. char rawStatus[96]; - snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str()); + snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText ? statusText : ""); char clippedStatus[96]; UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth()); @@ -495,7 +495,7 @@ void graphics::UIRenderer::rebuildFavoritedNodes() meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (!n || n->num == nodeDB->getNodeNum()) continue; - if (n->is_favorite) + if (nodeInfoLiteIsFavorite(n)) favoritedNodes.push_back(n); } @@ -750,7 +750,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat return; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; - if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node)) return; display->clear(); #if defined(M5STACK_UNITC6L) @@ -763,7 +763,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat #endif currentFavoriteNodeNum = node->num; // === Create the shortName and title string === - const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; + const char *shortName = (nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "Node"; char titlestr[40]; snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); @@ -781,9 +781,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat // === 1. Long Name (always try to show first) === const char *username; if (currentResolution == ScreenResolution::UltraLow) { - username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->short_name : nullptr; } else { - username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : nullptr; } // Print node's long name (e.g. "Backpack Node") @@ -796,23 +796,14 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); } -#if !MESHTASTIC_EXCLUDE_STATUS +#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB // === Optional: Last received StatusMessage line for this node === - // Display it directly under the username line (if we have one). - if (statusMessageModule) { - const auto &recent = statusMessageModule->getRecentReceived(); - const StatusMessageModule::RecentStatus *found = nullptr; - - // Search newest-to-oldest - for (auto it = recent.rbegin(); it != recent.rend(); ++it) { - if (it->fromNodeId == node->num && !it->statusText.empty()) { - found = &(*it); - break; - } - } - - if (found) { - drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], found->statusText); + // Display it directly under the username line (if we have one). The cache + // lives on NodeDB now, keyed by NodeNum, so this is an O(1) lookup. + if (nodeDB) { + const meshtastic_StatusMessage *cachedStatus = nodeDB->getNodeStatus(node->num); + if (cachedStatus && cachedStatus->status[0]) { + drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus->status); } } #endif @@ -971,10 +962,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat #if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; - if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + const meshtastic_DeviceMetrics *nodeMetrics = nodeDB->getNodeTelemetry(node->num); + if (nodeMetrics && nodeMetrics->has_uptime_seconds) { char upPrefix[12]; // enough for leftSideSpacing + "Up:" snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); - getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); + getUptimeStr(nodeMetrics->uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); } if (uptimeStr[0]) { display->drawString(x, getTextPositions(display)[line++], uptimeStr); @@ -985,11 +977,12 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat char distStr[24] = ""; // Make buffer big enough for any string bool haveDistance = false; - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + const meshtastic_PositionLite *nodePos = nodeDB->getNodePosition(node->num); + const meshtastic_PositionLite *ourPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && nodePos && ourPos) { // Use shared meter conversion, then format display units with lightweight integer rounding. - const float distanceMeters = - GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i), - DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i)); + const float distanceMeters = GeoCoord::latLongToMeter(DegD(nodePos->latitude_i), DegD(nodePos->longitude_i), + DegD(ourPos->latitude_i), DegD(ourPos->longitude_i)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { const int feet = static_cast((distanceMeters * METERS_TO_FEET) + 0.5f); if (feet > 0 && feet < 1000) { @@ -1024,19 +1017,19 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat char batLine[32] = ""; bool haveBatLine = false; - if (node->has_device_metrics) { - bool hasPct = node->device_metrics.has_battery_level; - bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; + if (nodeMetrics) { + bool hasPct = nodeMetrics->has_battery_level; + bool hasVolt = nodeMetrics->has_voltage && nodeMetrics->voltage > 0.001f; int pct = 0; float volt = 0.0f; if (hasPct) { - pct = (int)node->device_metrics.battery_level; + pct = (int)nodeMetrics->battery_level; } if (hasVolt) { - volt = node->device_metrics.voltage; + volt = nodeMetrics->voltage; } if (hasPct && pct > 0 && pct <= 100) { @@ -1076,11 +1069,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat const bool hasNodePositionFix = nodeDB->hasValidPosition(node); const char *statusLine1 = nullptr; const char *statusLine2 = nullptr; - if (hasOwnPositionFix && hasNodePositionFix) { - const auto &op = ourNode->position; + if (hasOwnPositionFix && hasNodePositionFix && ourPos && nodePos) { + const auto &op = *ourPos; showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); if (showCompass) { - const auto &p = node->position; + const auto &p = *nodePos; bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading); } else { @@ -1270,7 +1263,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int textWidth = 0; int nameX = 0; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; - const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; + const char *longName = (nodeInfoLiteHasUser(ourNode) && ourNode->long_name[0]) ? ourNode->long_name : ""; const char *shortName = owner.short_name ? owner.short_name : ""; char combinedName[96]; if (longName[0] && shortName[0]) { @@ -1613,9 +1606,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU headingLat = DegD(gpsStatus->getLatitude()); headingLon = DegD(gpsStatus->getLongitude()); } else if (hasOwnPositionFix) { - const auto &op = ourNode->position; - headingLat = DegD(op.latitude_i); - headingLon = DegD(op.longitude_i); + const meshtastic_PositionLite *ownPos = nodeDB->getNodePosition(ourNode->num); + if (ownPos) { + headingLat = DegD(ownPos->latitude_i); + headingLon = DegD(ownPos->longitude_i); + } } validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading); } diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index 0a9cd3add..94039d519 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -362,8 +362,8 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) assert(node); // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) - if (node->has_user) { - std::string parsed = parse(node->user.short_name); + if (nodeInfoLiteHasUser(node)) { + std::string parsed = parse(node->short_name); if (isPrintable(parsed)) return parsed; } diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index 63ccaa216..7e07c06c2 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -137,8 +137,9 @@ void InkHUD::MapApplet::onRender(bool full) // Draw our node LAST with full white fill + outline meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); + const meshtastic_PositionLite *ourSelfPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + if (ourNode && nodeDB->hasValidPosition(ourNode) && ourSelfPos) { + Marker self = calculateMarker(ourSelfPos->latitude_i * 1e-7, ourSelfPos->longitude_i * 1e-7, false, 0); int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); @@ -169,9 +170,10 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) { // If we have a valid position for our own node, use that as the anchor meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && nodeDB->hasValidPosition(ourNode)) { - *lat = ourNode->position.latitude_i * 1e-7; - *lng = ourNode->position.longitude_i * 1e-7; + const meshtastic_PositionLite *ourSelfPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + if (ourNode && nodeDB->hasValidPosition(ourNode) && ourSelfPos) { + *lat = ourSelfPos->latitude_i * 1e-7; + *lng = ourSelfPos->longitude_i * 1e-7; } else { // Find mean lat long coords // ============================ @@ -201,9 +203,13 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) if (!shouldDrawNode(node)) continue; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(node->num); + if (!pos) + continue; + // Latitude and Longitude of node, in radians - float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; - float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; + float latRad = pos->latitude_i * (1e-7) * DEG_TO_RAD; + float lngRad = pos->longitude_i * (1e-7) * DEG_TO_RAD; // Convert to cartesian points, with center of earth at 0, 0, 0 // Exact distance from center is irrelevant, as we're only interested in the vector @@ -300,13 +306,17 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) if (!shouldDrawNode(node)) continue; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(node->num); + if (!pos) + continue; + // Check for a new top or bottom latitude - float latNode = node->position.latitude_i * 1e-7; + float latNode = pos->latitude_i * 1e-7; northernmost = max(northernmost, latNode); southernmost = min(southernmost, latNode); // Longitude is trickier - float lngNode = node->position.longitude_i * 1e-7; + float lngNode = pos->longitude_i * 1e-7; float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node if (degEastward < degWestward) @@ -372,10 +382,12 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) { // Find x and y position based on node's position in nodeDB assert(nodeDB->hasValidPosition(node)); - Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(node->num); + assert(pos); + Marker m = calculateMarker(pos->latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + pos->longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away ); // Convert to pixel coords @@ -516,13 +528,16 @@ void InkHUD::MapApplet::calculateAllMarkers() if (node->num == nodeDB->getNodeNum()) continue; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(node->num); + if (!pos) + continue; + // Calculate marker and store it - markers.push_back( - calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style - node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style - node->has_hops_away, // Is the hopsAway number valid - node->hops_away // Hops away - )); + markers.push_back(calculateMarker(pos->latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style + pos->longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style + node->has_hops_away, // Is the hopsAway number valid + node->hops_away // Hops away + )); } } diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 607fd4ef7..1b11ba200 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -57,14 +57,16 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke c.hopsAway = node->hops_away; if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; + const meshtastic_PositionLite *ourPos = nodeDB->getNodePosition(ourNode->num); + const meshtastic_PositionLite *theirPos = nodeDB->getNodePosition(node->num); + if (ourPos && theirPos) { + float ourLat = ourPos->latitude_i * 1e-7; + float ourLong = ourPos->longitude_i * 1e-7; + float theirLat = theirPos->latitude_i * 1e-7; + float theirLong = theirPos->longitude_i * 1e-7; - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } } } @@ -179,8 +181,8 @@ void InkHUD::NodeListApplet::onRender(bool full) // -- Longname -- // Parse special chars in long name // Use node id if unknown - if (node && node->has_user) - longName = parse(node->user.long_name); // Found in nodeDB + if (nodeInfoLiteHasUser(node)) + longName = parse(node->long_name); // Found in nodeDB else { // Not found in nodeDB, show a hex nodeid instead longName = hexifyNodeNum(nodeNum); diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index dfef4d085..5be8a9725 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -2090,7 +2090,7 @@ void InkHUD::MenuApplet::populateRecipientPage() // Count favorites for (uint32_t i = 0; i < nodeCount; i++) { - if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + if (nodeInfoLiteIsFavorite(nodeDB->getMeshNodeByIndex(i))) favoriteCount++; } @@ -2101,7 +2101,7 @@ void InkHUD::MenuApplet::populateRecipientPage() meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Skip node if not a favorite - if (!node->is_favorite) + if (!nodeInfoLiteIsFavorite(node)) continue; CannedMessages::RecipientItem r; @@ -2111,8 +2111,8 @@ void InkHUD::MenuApplet::populateRecipientPage() // Set a label for the menu item r.label = "DM: "; - if (node->has_user) - r.label += parse(node->user.long_name); + if (nodeInfoLiteHasUser(node)) + r.label += parse(node->long_name); else r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 6c8069c8b..44d0a8089 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -241,7 +241,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila text += msgIsBroadcast ? "From:" : "DM: "; // Sender id - if (node && node->has_user) + if (nodeInfoLiteHasUser(node)) text += parseShortName(node); else text += hexifyNodeNum(message->sender); @@ -255,7 +255,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila text += msgIsBroadcast ? "Msg from " : "DM from "; // Sender id - if (node && node->has_user) + if (nodeInfoLiteHasUser(node)) text += parseShortName(node); else text += hexifyNodeNum(message->sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 96c519599..bbbe826b1 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -70,10 +70,10 @@ void InkHUD::AllMessageApplet::onRender(bool full) // - short name and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); - if (sender && sender->has_user) { + if (nodeInfoLiteHasUser(sender)) { header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += parse(sender->user.long_name); + header += parse(sender->long_name); header += ")"; } else header += hexifyNodeNum(message->sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index 189a56cab..c15152d3a 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -66,10 +66,10 @@ void InkHUD::DMApplet::onRender(bool full) // - shortname and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); - if (sender && sender->has_user) { + if (nodeInfoLiteHasUser(sender)) { header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += parse(sender->user.long_name); + header += parse(sender->long_name); header += ")"; } else header += hexifyNodeNum(latestMessage->dm.sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp index 520070d72..290546a6f 100644 --- a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp @@ -8,7 +8,7 @@ using namespace NicheGraphics; bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) { // Keep our own node available as map anchor/center; all others must be favorited. - return node && (node->num == nodeDB->getNodeNum() || node->is_favorite); + return node && (node->num == nodeDB->getNodeNum() || nodeInfoLiteIsFavorite(node)); } void InkHUD::FavoritesMapApplet::onRender(bool full) @@ -25,7 +25,7 @@ void InkHUD::FavoritesMapApplet::onRender(bool full) // Draw our latest "node of interest" as a special marker. meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); - if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers()) + if (node && nodeInfoLiteIsFavorite(node) && nodeDB->hasValidPosition(node) && enoughMarkers()) drawLabeledMarker(node); } @@ -74,7 +74,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP } else { // For non-local packets, this applet only reacts to favorited nodes. const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); - if (!sender || !sender->is_favorite) + if (!nodeInfoLiteIsFavorite(sender)) return ProcessMessage::CONTINUE; // Check if this position is from someone different than our previous position packet. diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index a7fd094e6..45ff84126 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -89,14 +89,16 @@ void InkHUD::HeardApplet::populateFromNodeDB() c.hopsAway = node->hops_away; if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { - // Get lat and long as float - // Meshtastic stores these as integers internally - float ourLat = ourNode->position.latitude_i * 1e-7; - float ourLong = ourNode->position.longitude_i * 1e-7; - float theirLat = node->position.latitude_i * 1e-7; - float theirLong = node->position.longitude_i * 1e-7; + const meshtastic_PositionLite *ourPos = nodeDB->getNodePosition(ourNode->num); + const meshtastic_PositionLite *theirPos = nodeDB->getNodePosition(node->num); + if (ourPos && theirPos) { + float ourLat = ourPos->latitude_i * 1e-7; + float ourLong = ourPos->longitude_i * 1e-7; + float theirLat = theirPos->latitude_i * 1e-7; + float theirLong = theirPos->longitude_i * 1e-7; - c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); + } } // Insert into the card collection (member of base class) diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index daa7a3a75..59c6701c9 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -112,7 +112,7 @@ bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, mes * @param bytes Buffer containing plaintext input. * @param bytesOut Output buffer to be populated with encrypted ciphertext. */ -bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, +bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { uint8_t *auth; @@ -152,7 +152,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas * @param bytes Buffer containing ciphertext input. * @param bytesOut Output buffer to be populated with decrypted plaintext. */ -bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, +bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) { const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index f40400331..9410b63e7 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -40,9 +40,12 @@ class CryptoEngine #endif void setDHPrivateKey(uint8_t *_private_key); - virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, + // The remotePublic key parameter takes the public_key bytes container from + // a stored node header. NodeInfoLite is the on-device storage type since + // the slim refactor flattened UserLite into it. + virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); - virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, + virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); virtual bool setDHPublicKey(uint8_t *publicKey); virtual void hash(uint8_t *bytes, size_t numBytes); diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 952a6d2be..7ce1aac04 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -94,8 +94,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); // ignore our request for its NodeInfo - } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && - nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { + } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster && + !nodeDB->isFull()) { if (airTime->isTxAllowedChannelUtil(true)) { const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { @@ -392,19 +393,17 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); assert(node); - // We might not have a position yet for our local node, in that case, at least try to send the time - if (!node->has_position) { - memset(&node->position, 0, sizeof(node->position)); - node->has_position = true; - } - - meshtastic_PositionLite &position = node->position; - // Update our local node info with our time (even if we don't decide to update anyone else) node->last_heard = getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid - position.time = getValidTime(RTCQualityFromNet); +#if !MESHTASTIC_EXCLUDE_POSITIONDB + // Make sure our own NodeNum has a slot in the position map so subsequent + // updates (and the bundled NodeInfo emission to the phone) have somewhere + // to read from. Insert a default-zero entry on first call. + auto &mySlot = nodeDB->nodePositions[node->num]; + mySlot.time = getValidTime(RTCQualityFromNet); +#endif if (powerStatus->getHasBattery() == 1) { updateBatteryLevel(powerStatus->getBatteryChargePercent()); @@ -432,7 +431,9 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus) // Used fixed position if configured regardless of GPS lock if (config.position.fixed_position) { LOG_WARN("Use fixed position"); - pos = TypeConversions::ConvertToPosition(node->position); + const meshtastic_PositionLite *fixedSlot = nodeDB->getNodePosition(node->num); + if (fixedSlot) + pos = TypeConversions::ConvertToPosition(*fixedSlot); } // Add a fresh timestamp diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ac6880ade..c61b780a1 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -22,6 +22,7 @@ #include "error.h" #include "main.h" #include "mesh-pb-constants.h" +#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h" #include "meshUtils.h" #include "modules/NeighborInfoModule.h" #include @@ -152,20 +153,119 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_ #endif bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) +{ + switch (field->tag) { + case meshtastic_NodeDatabase_nodes_tag: { + if (ostream) { + const auto *vec = static_cast *>(field->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeInfoLite node; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) + vec->push_back(node); + } + return true; + } + case meshtastic_NodeDatabase_positions_tag: { + if (ostream) { + const auto *vec = static_cast *>(field->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodePositionEntry entry; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry)) + vec->push_back(entry); + } + return true; + } + case meshtastic_NodeDatabase_telemetry_tag: { + if (ostream) { + const auto *vec = static_cast *>(field->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeTelemetryEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeTelemetryEntry entry; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry)) + vec->push_back(entry); + } + return true; + } + case meshtastic_NodeDatabase_status_tag: { + if (ostream) { + const auto *vec = static_cast *>(field->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeStatusEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeStatusEntry entry; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry)) + vec->push_back(entry); + } + return true; + } + case meshtastic_NodeDatabase_environment_tag: { + if (ostream) { + const auto *vec = static_cast *>(field->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, field)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeEnvironmentEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeEnvironmentEntry entry; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry)) + vec->push_back(entry); + } + return true; + } + default: + return true; + } +} + +// Migration-only callback for the legacy NodeDatabase descriptor. +bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) { if (ostream) { - std::vector const *vec = (std::vector *)field->pData; + const auto *vec = static_cast *>(field->pData); for (auto item : *vec) { if (!pb_encode_tag_for_field(ostream, field)) return false; - pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); + if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_Legacy_fields, &item)) + return false; } } if (istream) { - meshtastic_NodeInfoLite node; // this gets good data - std::vector *vec = (std::vector *)field->pData; - - if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) + meshtastic_NodeInfoLite_Legacy node; + auto *vec = static_cast *>(field->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_Legacy_fields, &node)) vec->push_back(node); } return true; @@ -298,8 +398,7 @@ NodeDB::NodeDB() #endif // Include our owner in the node db under our nodenum meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); - info->user = TypeConversions::ConvertToUserLite(owner); - info->has_user = true; + TypeConversions::CopyUserToNodeInfoLite(info, owner); // If node database has not been saved for the first time, save it now #ifdef FSCom @@ -442,8 +541,9 @@ NodeDB::NodeDB() #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; config.has_position = true; - info->has_position = true; - info->position = TypeConversions::ConvertToPositionLite(fixedGPS); +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS); +#endif nodeDB->setLocalPosition(fixedGPS); config.position.fixed_position = true; #endif @@ -548,6 +648,18 @@ void NodeDB::installDefaultNodeDatabase() nodeDatabase.nodes = std::vector(MAX_NUM_NODES); numMeshNodes = 0; meshNodes = &nodeDatabase.nodes; +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodePositions.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + nodeTelemetry.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + nodeEnvironment.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_STATUSDB + nodeStatus.clear(); +#endif } void NodeDB::installDefaultConfig(bool preserveKey = false) @@ -1019,12 +1131,14 @@ void NodeDB::resetNodes(bool keepFavorites) { if (!config.position.fixed_position) clearLocalPosition(); + NodeNum ourNum = getNodeNum(); numMeshNodes = 1; if (keepFavorites) { LOG_INFO("Clearing node database - preserving favorites"); for (size_t i = 0; i < meshNodes->size(); i++) { meshtastic_NodeInfoLite &node = meshNodes->at(i); - if (i > 0 && !node.is_favorite) { + if (i > 0 && !(node.bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK)) { + eraseNodeSatellites(node.num); node = meshtastic_NodeInfoLite(); } else { numMeshNodes += 1; @@ -1032,8 +1146,14 @@ void NodeDB::resetNodes(bool keepFavorites) }; } else { LOG_INFO("Clearing node database - removing favorites"); + for (size_t i = 1; i < meshNodes->size(); i++) { + const NodeNum gone = meshNodes->at(i).num; + if (gone) + eraseNodeSatellites(gone); + } std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); } + (void)ourNum; devicestate.has_rx_text_message = false; devicestate.has_rx_waypoint = false; saveNodeDatabaseToDisk(); @@ -1054,36 +1174,111 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum) numMeshNodes -= removed; std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, meshtastic_NodeInfoLite()); + if (removed) + eraseNodeSatellites(nodeNum); LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); saveNodeDatabaseToDisk(); } void NodeDB::clearLocalPosition() { - meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); - node->position.latitude_i = 0; - node->position.longitude_i = 0; - node->position.altitude = 0; - node->position.time = 0; +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodePositions.erase(getNodeNum()); +#endif setLocalPosition(meshtastic_Position_init_default); localPositionUpdatedSinceBoot = false; } +const meshtastic_PositionLite *NodeDB::getNodePosition(NodeNum n) const +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + (void)n; + return nullptr; +#else + auto it = nodePositions.find(n); + return (it == nodePositions.end()) ? nullptr : &it->second; +#endif +} + +const meshtastic_DeviceMetrics *NodeDB::getNodeTelemetry(NodeNum n) const +{ +#if MESHTASTIC_EXCLUDE_TELEMETRYDB + (void)n; + return nullptr; +#else + auto it = nodeTelemetry.find(n); + return (it == nodeTelemetry.end()) ? nullptr : &it->second; +#endif +} + +const meshtastic_EnvironmentMetrics *NodeDB::getNodeEnvironment(NodeNum n) const +{ +#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB + (void)n; + return nullptr; +#else + auto it = nodeEnvironment.find(n); + return (it == nodeEnvironment.end()) ? nullptr : &it->second; +#endif +} + +const meshtastic_StatusMessage *NodeDB::getNodeStatus(NodeNum n) const +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + (void)n; + return nullptr; +#else + auto it = nodeStatus.find(n); + return (it == nodeStatus.end()) ? nullptr : &it->second; +#endif +} + +void NodeDB::setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status) +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + (void)n; + (void)status; +#else + nodeStatus[n] = status; +#endif +} + +void NodeDB::eraseNodeSatellites(NodeNum n) +{ +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodePositions.erase(n); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + nodeTelemetry.erase(n); +#endif +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + nodeEnvironment.erase(n); +#endif +#if !MESHTASTIC_EXCLUDE_STATUSDB + nodeStatus.erase(n); +#endif +} + void NodeDB::cleanupMeshDB() { int newPos = 0, removed = 0; for (int i = 0; i < numMeshNodes; i++) { - if (meshNodes->at(i).has_user) { - if (meshNodes->at(i).user.public_key.size > 0) { - if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { - meshNodes->at(i).user.public_key.size = 0; + meshtastic_NodeInfoLite &n = meshNodes->at(i); + if (n.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) { + if (n.public_key.size > 0) { + if (memfll(n.public_key.bytes, 0, n.public_key.size)) { + n.public_key.size = 0; } } if (newPos != i) - meshNodes->at(newPos++) = meshNodes->at(i); + meshNodes->at(newPos++) = n; else newPos++; } else { + // No user info - drop this node and its satellites + const NodeNum gone = n.num; + if (gone) + eraseNodeSatellites(gone); removed++; } } @@ -1141,14 +1336,22 @@ void NodeDB::pickNewNodeNum() nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; } + // Identity check via public key (or "empty slot?" when no keys yet); + // macaddr no longer lives on the slim header. + auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool { + if (!n) + return false; + if (owner.public_key.size == 32 && n->public_key.size == 32) + return memcmp(n->public_key.bytes, owner.public_key.bytes, 32) == 0; + return !(n->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK); + }; + meshtastic_NodeInfoLite *found; - while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || + while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) || (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice if (found) - LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " - "trying for 0x%x", - nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate); + LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate); nodeNum = candidate; } LOG_DEBUG("Use nodenum 0x%x ", nodeNum); @@ -1242,9 +1445,133 @@ void NodeDB::loadFromDisk() if (nodeDatabase.version < DEVICESTATE_MIN_VER) { LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); installDefaultNodeDatabase(); + } else if (nodeDatabase.version < DEVICESTATE_CUR_VER) { + // The v25 read above dropped tag-3/6 (now reserved); reload via the + // legacy descriptor to recover position/device_metrics into the maps. + LOG_WARN("NodeDatabase v%u: migrating to v%u", nodeDatabase.version, DEVICESTATE_CUR_VER); + // _init_zero brace-inits the embedded std::vector via its explicit + // (size_type, allocator) ctor, so default-construct instead. + meshtastic_NodeDatabase_Legacy legacyDb{}; + legacyDb.version = 0; + meshtastic_NodeDatabase_Legacy legacyEmpty{}; + legacyEmpty.version = DEVICESTATE_CUR_VER; + size_t legacyEmptyEncoded = 0; + pb_get_encoded_size(&legacyEmptyEncoded, meshtastic_NodeDatabase_Legacy_fields, &legacyEmpty); + const size_t legacyBufSize = legacyEmptyEncoded + (MAX_NUM_NODES * meshtastic_NodeInfoLite_Legacy_size); + auto legacyState = loadProto(nodeDatabaseFileName, legacyBufSize, sizeof(meshtastic_NodeDatabase_Legacy), + &meshtastic_NodeDatabase_Legacy_msg, &legacyDb); + if (legacyState == LoadFileResult::LOAD_SUCCESS) { + nodeDatabase.nodes.clear(); + const size_t maxToMigrate = std::min(legacyDb.nodes.size(), MAX_NUM_NODES); + nodeDatabase.nodes.reserve(maxToMigrate); + for (size_t i = 0; i < maxToMigrate; ++i) { + const auto &legacy = legacyDb.nodes[i]; + meshtastic_NodeInfoLite slim = meshtastic_NodeInfoLite_init_default; + slim.num = legacy.num; + slim.snr = legacy.snr; + slim.last_heard = legacy.last_heard; + slim.channel = legacy.channel; + slim.has_hops_away = legacy.has_hops_away; + slim.hops_away = legacy.hops_away; + slim.next_hop = legacy.next_hop; + slim.bitfield = legacy.bitfield; + if (legacy.via_mqtt) + slim.bitfield |= NODEINFO_BITFIELD_VIA_MQTT_MASK; + if (legacy.is_favorite) + slim.bitfield |= NODEINFO_BITFIELD_IS_FAVORITE_MASK; + if (legacy.is_ignored) + slim.bitfield |= NODEINFO_BITFIELD_IS_IGNORED_MASK; + if (legacy.has_user) { + slim.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK; + strncpy(slim.long_name, legacy.user.long_name, sizeof(slim.long_name)); + slim.long_name[sizeof(slim.long_name) - 1] = '\0'; + strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name)); + slim.short_name[sizeof(slim.short_name) - 1] = '\0'; + slim.hw_model = legacy.user.hw_model; + slim.role = legacy.user.role; + if (legacy.user.is_licensed) + slim.bitfield |= NODEINFO_BITFIELD_IS_LICENSED_MASK; + slim.public_key.size = legacy.user.public_key.size; + memcpy(slim.public_key.bytes, legacy.user.public_key.bytes, sizeof(slim.public_key.bytes)); + if (legacy.user.has_is_unmessagable) { + slim.bitfield |= NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK; + if (legacy.user.is_unmessagable) + slim.bitfield |= NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK; + } + // macaddr deprecated since 1.2.11 - dropped from slim header. + } + nodeDatabase.nodes.push_back(slim); +#if !MESHTASTIC_EXCLUDE_POSITIONDB + if (legacy.has_position) + nodePositions[legacy.num] = legacy.position; +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + if (legacy.has_device_metrics) + nodeTelemetry[legacy.num] = legacy.device_metrics; +#endif + } + nodeDatabase.version = DEVICESTATE_CUR_VER; + meshNodes = &nodeDatabase.nodes; + numMeshNodes = nodeDatabase.nodes.size(); + size_t posCount = 0, telCount = 0; +#if !MESHTASTIC_EXCLUDE_POSITIONDB + posCount = nodePositions.size(); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + telCount = nodeTelemetry.size(); +#endif + LOG_INFO("Migrated %u nodes from legacy -> v%u (positions: %u, telemetry: %u)", (unsigned)numMeshNodes, + DEVICESTATE_CUR_VER, (unsigned)posCount, (unsigned)telCount); + saveNodeDatabaseToDisk(); + } else { + LOG_ERROR("Failed to load NodeDatabase via legacy descriptor; installing default"); + installDefaultNodeDatabase(); + } } else { meshNodes = &nodeDatabase.nodes; numMeshNodes = nodeDatabase.nodes.size(); + // Hydrate the satellite maps; the on-disk vectors stay empty in steady + // state and are repopulated only at save time. +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodePositions.clear(); + nodePositions.reserve(nodeDatabase.positions.size()); + for (const auto &entry : nodeDatabase.positions) { + if (entry.has_position) + nodePositions[entry.num] = entry.position; + } + nodeDatabase.positions.clear(); + nodeDatabase.positions.shrink_to_fit(); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + nodeTelemetry.clear(); + nodeTelemetry.reserve(nodeDatabase.telemetry.size()); + for (const auto &entry : nodeDatabase.telemetry) { + if (entry.has_device_metrics) + nodeTelemetry[entry.num] = entry.device_metrics; + } + nodeDatabase.telemetry.clear(); + nodeDatabase.telemetry.shrink_to_fit(); +#endif +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + nodeEnvironment.clear(); + nodeEnvironment.reserve(nodeDatabase.environment.size()); + for (const auto &entry : nodeDatabase.environment) { + if (entry.has_environment_metrics) + nodeEnvironment[entry.num] = entry.environment_metrics; + } + nodeDatabase.environment.clear(); + nodeDatabase.environment.shrink_to_fit(); +#endif +#if !MESHTASTIC_EXCLUDE_STATUSDB + nodeStatus.clear(); + nodeStatus.reserve(nodeDatabase.status.size()); + for (const auto &entry : nodeDatabase.status) { + if (entry.has_status) + nodeStatus[entry.num] = entry.status; + } + nodeDatabase.status.clear(); + nodeDatabase.status.shrink_to_fit(); +#endif LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); } @@ -1272,16 +1599,16 @@ void NodeDB::loadFromDisk() // Attempt recovery of owner fields from our own NodeDB entry if available. meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); - if (us && us->has_user) { + if (us && (us->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK)) { LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", us->num); - memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); + memcpy(owner.long_name, us->long_name, sizeof(owner.long_name)); owner.long_name[sizeof(owner.long_name) - 1] = '\0'; - memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); + memcpy(owner.short_name, us->short_name, sizeof(owner.short_name)); owner.short_name[sizeof(owner.short_name) - 1] = '\0'; - owner.is_licensed = us->user.is_licensed; - owner.has_is_unmessagable = us->user.has_is_unmessagable; - owner.is_unmessagable = us->user.is_unmessagable; + owner.is_licensed = (us->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0; + owner.has_is_unmessagable = (us->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0; + owner.is_unmessagable = (us->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0; // Save the recovered owner to device state on disk saveToDisk(SEGMENT_DEVICESTATE); @@ -1528,9 +1855,74 @@ bool NodeDB::saveNodeDatabaseToDisk() FSCom.mkdir("/prefs"); spiLock->unlock(); #endif + // Project the maps into the on-disk vectors just before encoding; cleared + // again on the way out so we don't carry duplicate state. +#if !MESHTASTIC_EXCLUDE_POSITIONDB + nodeDatabase.positions.clear(); + nodeDatabase.positions.reserve(nodePositions.size()); + for (const auto &kv : nodePositions) { + meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_default; + entry.num = kv.first; + entry.has_position = true; + entry.position = kv.second; + nodeDatabase.positions.push_back(entry); + } +#else + nodeDatabase.positions.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + nodeDatabase.telemetry.clear(); + nodeDatabase.telemetry.reserve(nodeTelemetry.size()); + for (const auto &kv : nodeTelemetry) { + meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_default; + entry.num = kv.first; + entry.has_device_metrics = true; + entry.device_metrics = kv.second; + nodeDatabase.telemetry.push_back(entry); + } +#else + nodeDatabase.telemetry.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + nodeDatabase.environment.clear(); + nodeDatabase.environment.reserve(nodeEnvironment.size()); + for (const auto &kv : nodeEnvironment) { + meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_default; + entry.num = kv.first; + entry.has_environment_metrics = true; + entry.environment_metrics = kv.second; + nodeDatabase.environment.push_back(entry); + } +#else + nodeDatabase.environment.clear(); +#endif +#if !MESHTASTIC_EXCLUDE_STATUSDB + nodeDatabase.status.clear(); + nodeDatabase.status.reserve(nodeStatus.size()); + for (const auto &kv : nodeStatus) { + meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_default; + entry.num = kv.first; + entry.has_status = true; + entry.status = kv.second; + nodeDatabase.status.push_back(entry); + } +#else + nodeDatabase.status.clear(); +#endif + size_t nodeDatabaseSize; pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); - return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); + bool ok = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); + + nodeDatabase.positions.clear(); + nodeDatabase.positions.shrink_to_fit(); + nodeDatabase.telemetry.clear(); + nodeDatabase.telemetry.shrink_to_fit(); + nodeDatabase.environment.clear(); + nodeDatabase.environment.shrink_to_fit(); + nodeDatabase.status.clear(); + nodeDatabase.status.shrink_to_fit(); + return ok; } bool NodeDB::saveToDiskNoRetry(int saveWhat) @@ -1699,7 +2091,7 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) // FIXME this implementation is kinda expensive for (int i = 0; i < numMeshNodes; i++) { - if (localOnly && meshNodes->at(i).via_mqtt) + if (localOnly && (meshNodes->at(i).bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK)) continue; if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) numseen++; @@ -1736,18 +2128,32 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou return; } +#if MESHTASTIC_EXCLUDE_POSITIONDB + // Build flag opted out: header still tracks last_heard via updateFrom; we + // simply don't cache the position payload anywhere on this device. + if (src == RX_SRC_LOCAL) { + LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i, + p.altitude); + setLocalPosition(p); + } + (void)nodeId; + updateGUIforNode = info; + notifyObservers(true); +#else + meshtastic_PositionLite &slot = nodePositions[nodeId]; // creates default-zero entry if missing + if (src == RX_SRC_LOCAL) { // Local packet, fully authoritative LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i, p.altitude); setLocalPosition(p); - info->position = TypeConversions::ConvertToPositionLite(p); + slot = TypeConversions::ConvertToPositionLite(p); } else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) { // FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO // (stop-gap fix for issue #900) LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time); - info->position.time = p.time; + slot.time = p.time; } else { // Be careful to only update fields that have been set by the REMOTE sender // A lot of position reports don't have time populated. In that case, be careful to not blow away the time we @@ -1757,41 +2163,46 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i); // First, back up fields that we want to protect from overwrite - uint32_t tmp_time = info->position.time; + uint32_t tmp_time = slot.time; // Next, update atomically - info->position = TypeConversions::ConvertToPositionLite(p); + slot = TypeConversions::ConvertToPositionLite(p); // Last, restore any fields that may have been overwritten - if (!info->position.time) - info->position.time = tmp_time; + if (!slot.time) + slot.time = tmp_time; } - info->has_position = true; updateGUIforNode = info; notifyObservers(true); // Force an update whether or not our node counts have changed +#endif } -/** Update telemetry info for this node based on received metrics - * We only care about device telemetry here +/** Update telemetry info for this node based on received metrics. Stores + * device_metrics and environment_metrics into their respective satellite + * maps; other variants (air_quality, power, local_stats, health) are + * intentionally not retained per-node. */ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) { meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); - // Environment metrics should never go to NodeDb but we'll safegaurd anyway - if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) { + if (!info) return; - } - if (src == RX_SRC_LOCAL) { - // Local packet, fully authoritative - LOG_DEBUG("updateTelemetry LOCAL"); + if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) { + LOG_DEBUG(src == RX_SRC_LOCAL ? "updateTelemetry LOCAL device" : "updateTelemetry REMOTE device node=0x%x", nodeId); +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + nodeTelemetry[nodeId] = t.variant.device_metrics; +#endif + } else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) { + LOG_DEBUG(src == RX_SRC_LOCAL ? "updateTelemetry LOCAL env" : "updateTelemetry REMOTE env node=0x%x", nodeId); +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + nodeEnvironment[nodeId] = t.variant.environment_metrics; +#endif } else { - LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); + return; // air_quality / power / local_stats / health: not stored per-node } - info->device_metrics = t.variant.device_metrics; - info->has_device_metrics = true; updateGUIforNode = info; - notifyObservers(true); // Force an update whether or not our node counts have changed + notifyObservers(true); } /** @@ -1807,23 +2218,21 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) // and the client does not, do not allow the client to update the // saved public key. if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { - if (contact.user.public_key.size != info->user.public_key.size || - memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { + if (contact.user.public_key.size != info->public_key.size || + memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) { return; } } info->num = contact.node_num; - info->has_user = true; - info->user = TypeConversions::ConvertToUserLite(contact.user); + TypeConversions::CopyUserToNodeInfoLite(info, contact.user); if (contact.should_ignore) { // If should_ignore is set, // we need to clear the public key and other cruft, in addition to setting the node as ignored - info->is_ignored = true; - info->is_favorite = false; - info->has_device_metrics = false; - info->has_position = false; - info->user.public_key.size = 0; - memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes)); + nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true); + nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false); + eraseNodeSatellites(contact.node_num); + info->public_key.size = 0; + memset(info->public_key.bytes, 0, sizeof(info->public_key.bytes)); } else { /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! @@ -1842,7 +2251,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) } else { // Normal case: set is_favorite to prevent expiration. // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). - info->is_favorite = true; + nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); } // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified @@ -1887,9 +2296,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde return false; } } - if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one + if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one // if the key doesn't match, don't update nodeDB at all. - if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { + if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) { LOG_WARN("Public Key mismatch, dropping NodeInfo"); return false; } @@ -1902,19 +2311,22 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde // Always ensure user.id is derived from nodeId, regardless of what was received snprintf(p.id, sizeof(p.id), "!%08x", nodeId); - // Both of info->user and p start as filled with zero so I think this is okay - auto lite = TypeConversions::ConvertToUserLite(p); - bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); + meshtastic_NodeInfoLite before = *info; + TypeConversions::CopyUserToNodeInfoLite(info, p); + bool changed = + (memcmp(before.long_name, info->long_name, sizeof(info->long_name)) != 0) || + (memcmp(before.short_name, info->short_name, sizeof(info->short_name)) != 0) || (before.hw_model != info->hw_model) || + (before.role != info->role) || (before.public_key.size != info->public_key.size) || + (info->public_key.size > 0 && memcmp(before.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) || + (before.bitfield != info->bitfield) || (info->channel != channelIndex); - info->user = lite; - if (info->user.public_key.size == 32) { - printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32); + if (info->public_key.size == 32) { + printBytes("Saved Pubkey: ", info->public_key.bytes, 32); } if (nodeId != getNodeNum()) info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) - LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, + LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->long_name, info->short_name, nodeId, info->channel); - info->has_user = true; if (changed) { updateGUIforNode = info; @@ -1956,7 +2368,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) if (mp.rx_snr) info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. - info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT + nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK, + mp.via_mqtt); // Store if we received this packet via MQTT // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway const int8_t hopsAway = getHopsAway(mp); @@ -1971,8 +2384,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) 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 (lite && nodeInfoLiteIsFavorite(lite) != is_favorite) { + nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite); sortMeshDB(); saveNodeDatabaseToDisk(); } @@ -1989,7 +2402,7 @@ bool NodeDB::isFavorite(uint32_t nodeId) meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite) { - return lite->is_favorite; + return nodeInfoLiteIsFavorite(lite); } return false; } @@ -2014,14 +2427,14 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) lite = &meshNodes->at(i); if (lite->num == p.from) { - if (lite->is_favorite) + if (nodeInfoLiteIsFavorite(lite)) return true; seenFrom = true; } if (lite->num == p.to) { - if (lite->is_favorite) + if (nodeInfoLiteIsFavorite(lite)) return true; seenTo = true; @@ -2057,10 +2470,12 @@ void NodeDB::sortMeshDB() // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash std::swap(meshNodes->at(i), meshNodes->at(i - 1)); changed = true; - } else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { + } else if ((meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) && + !(meshNodes->at(i - 1).bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK)) { std::swap(meshNodes->at(i), meshNodes->at(i - 1)); changed = true; - } else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { + } else if (!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) && + (meshNodes->at(i - 1).bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK)) { // noop } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { std::swap(meshNodes->at(i), meshNodes->at(i - 1)); @@ -2120,15 +2535,17 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) int oldestIndex = -1; int oldestBoringIndex = -1; for (int i = 1; i < numMeshNodes; i++) { + const uint32_t bf = meshNodes->at(i).bitfield; + const bool isFavorite = (bf & NODEINFO_BITFIELD_IS_FAVORITE_MASK) != 0; + const bool isIgnored = (bf & NODEINFO_BITFIELD_IS_IGNORED_MASK) != 0; + const bool isVerified = (bf & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) != 0; // Simply the oldest non-favorite, non-ignored, non-verified node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && - !(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && - meshNodes->at(i).last_heard < oldest) { + if (!isFavorite && !isIgnored && !isVerified && meshNodes->at(i).last_heard < oldest) { oldest = meshNodes->at(i).last_heard; oldestIndex = i; } // The oldest "boring" node - if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && + if (!isFavorite && !isIgnored && meshNodes->at(i).public_key.size == 0 && meshNodes->at(i).last_heard < oldestBoring) { oldestBoring = meshNodes->at(i).last_heard; oldestBoringIndex = i; @@ -2140,6 +2557,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) } if (oldestIndex != -1) { + eraseNodeSatellites(meshNodes->at(oldestIndex).num); // Shove the remaining nodes down the chain for (int i = oldestIndex; i < numMeshNodes - 1; i++) { meshNodes->at(i) = meshNodes->at(i + 1); @@ -2163,7 +2581,10 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) /// valid lat/lon bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) { - return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); + if (!n) + return false; + const meshtastic_PositionLite *pos = getNodePosition(n->num); + return pos && (pos->latitude_i != 0 || pos->longitude_i != 0); } /// If we have a node / user and they report is_licensed = true @@ -2171,10 +2592,10 @@ bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) { meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); - if (!info || !info->has_user) { + if (!info || !(info->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK)) { return UserLicenseStatus::NotKnown; } - return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; + return (info->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } #if !defined(MESHTASTIC_EXCLUDE_PKI) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index f6be963c1..6313e1e86 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "MeshTypes.h" @@ -82,7 +83,9 @@ DeviceState versions used to be defined in the .proto file but really only this #define SEGMENT_CHANNELS 8 #define SEGMENT_NODEDATABASE 16 -#define DEVICESTATE_CUR_VER 24 +#define DEVICESTATE_CUR_VER 25 +// Lowest on-disk version we still know how to load. v24 saves are migrated +// at boot via the parallel deviceonly_legacy descriptor and re-saved as v25. #define DEVICESTATE_MIN_VER 24 extern meshtastic_DeviceState devicestate; @@ -166,6 +169,21 @@ class NodeDB Observable newStatus; pb_size_t numMeshNodes; + // Satellite per-NodeNum maps for data we used to inline into NodeInfoLite, + // gated by MESHTASTIC_EXCLUDE_*DB so STM32WL can omit them. +#if !MESHTASTIC_EXCLUDE_POSITIONDB + std::unordered_map nodePositions; +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + std::unordered_map nodeTelemetry; +#endif +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + std::unordered_map nodeEnvironment; +#endif +#if !MESHTASTIC_EXCLUDE_STATUSDB + std::unordered_map nodeStatus; +#endif + bool keyIsLowEntropy = false; bool hasWarned = false; @@ -277,6 +295,21 @@ class NodeDB virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); size_t getNumMeshNodes() { return numMeshNodes; } + // nullptr if absent or the corresponding DB is compiled out. + const meshtastic_PositionLite *getNodePosition(NodeNum n) const; + const meshtastic_DeviceMetrics *getNodeTelemetry(NodeNum n) const; + const meshtastic_EnvironmentMetrics *getNodeEnvironment(NodeNum n) const; + const meshtastic_StatusMessage *getNodeStatus(NodeNum n) const; + + void setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status); + + bool hasNodePosition(NodeNum n) const { return getNodePosition(n) != nullptr; } + bool hasNodeTelemetry(NodeNum n) const { return getNodeTelemetry(n) != nullptr; } + bool hasNodeEnvironment(NodeNum n) const { return getNodeEnvironment(n) != nullptr; } + bool hasNodeStatus(NodeNum n) const { return getNodeStatus(n) != nullptr; } + + void eraseNodeSatellites(NodeNum n); + UserLicenseStatus getLicenseStatus(uint32_t nodeNum); size_t getMaxNodesAllocatedSize() @@ -285,7 +318,11 @@ class NodeDB emptyNodeDatabase.version = DEVICESTATE_CUR_VER; size_t nodeDatabaseSize; pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); - return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); + // Always include satellite slots so backups from higher-cap peers + // decode without truncation, even when our build excludes the DBs. + return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size) + + (MAX_NUM_NODES * meshtastic_NodePositionEntry_size) + (MAX_NUM_NODES * meshtastic_NodeTelemetryEntry_size) + + (MAX_NUM_NODES * meshtastic_NodeEnvironmentEntry_size) + (MAX_NUM_NODES * meshtastic_NodeStatusEntry_size); } // returns true if the maximum number of nodes is reached or we are running low on memory @@ -397,10 +434,66 @@ extern meshtastic_CriticalErrorCode error_code; * A numeric error address (nonzero if available) */ extern uint32_t error_address; +// Bit assignments for meshtastic_NodeInfoLite.bitfield. #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 -#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) +#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1u << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) #define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1 -#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT) +#define NODEINFO_BITFIELD_IS_MUTED_MASK (1u << NODEINFO_BITFIELD_IS_MUTED_SHIFT) +#define NODEINFO_BITFIELD_VIA_MQTT_SHIFT 2 +#define NODEINFO_BITFIELD_VIA_MQTT_MASK (1u << NODEINFO_BITFIELD_VIA_MQTT_SHIFT) +#define NODEINFO_BITFIELD_IS_FAVORITE_SHIFT 3 +#define NODEINFO_BITFIELD_IS_FAVORITE_MASK (1u << NODEINFO_BITFIELD_IS_FAVORITE_SHIFT) +#define NODEINFO_BITFIELD_IS_IGNORED_SHIFT 4 +#define NODEINFO_BITFIELD_IS_IGNORED_MASK (1u << NODEINFO_BITFIELD_IS_IGNORED_SHIFT) +#define NODEINFO_BITFIELD_HAS_USER_SHIFT 5 +#define NODEINFO_BITFIELD_HAS_USER_MASK (1u << NODEINFO_BITFIELD_HAS_USER_SHIFT) +#define NODEINFO_BITFIELD_IS_LICENSED_SHIFT 6 +#define NODEINFO_BITFIELD_IS_LICENSED_MASK (1u << NODEINFO_BITFIELD_IS_LICENSED_SHIFT) +#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT 7 +#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT) +#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT 8 +#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT) +// Bits 9..31 reserved for future single-bit flags. + +// Convenience accessors so call sites read like the old struct fields. +inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK); +} +inline bool nodeInfoLiteViaMqtt(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK); +} +inline bool nodeInfoLiteIsFavorite(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK); +} +inline bool nodeInfoLiteIsIgnored(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK); +} +inline bool nodeInfoLiteIsLicensed(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK); +} +inline bool nodeInfoLiteHasIsUnmessagable(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK); +} +inline bool nodeInfoLiteIsUnmessagable(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK); +} + +inline void nodeInfoLiteSetBit(meshtastic_NodeInfoLite *n, uint32_t mask, bool value) +{ + if (!n) + return; + if (value) + n->bitfield |= mask; + else + n->bitfield &= ~mask; +} #define Module_Config_size \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 5c3ab486a..fd54790ba 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig() onConfigStart(); // even if we were already connected - restart our state machine - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes state = STATE_SEND_OWN_NODEINFO; LOG_INFO("Client only wants node info, skipping other config"); @@ -81,6 +81,15 @@ void PhoneAPI::handleStartConfig() concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone = {}; nodeInfoQueue.clear(); + replayQueue.clear(); + replayPositionOrder.clear(); + replayTelemetryOrder.clear(); + replayEnvironmentOrder.clear(); + replayStatusOrder.clear(); + replayPositionIndex = 0; + replayTelemetryIndex = 0; + replayEnvironmentIndex = 0; + replayStatusIndex = 0; } resetReadIndex(); } @@ -120,6 +129,15 @@ void PhoneAPI::close() concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoForPhone = {}; nodeInfoQueue.clear(); + replayQueue.clear(); + replayPositionOrder.clear(); + replayTelemetryOrder.clear(); + replayEnvironmentOrder.clear(); + replayStatusOrder.clear(); + replayPositionIndex = 0; + replayTelemetryIndex = 0; + replayEnvironmentIndex = 0; + replayStatusIndex = 0; } packetForPhone = NULL; filesManifest.clear(); @@ -302,7 +320,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) nodeInfoForPhone.num = 0; } } - if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { + if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes state = STATE_SEND_OTHER_NODEINFOS; onNowHasData(0); @@ -529,8 +547,124 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoQueue.clear(); + // Replay states no-op for legacy clients / excluded DBs. + state = STATE_REPLAY_POSITIONS; + return getFromRadio(buf); + } + break; + } + + case STATE_REPLAY_POSITIONS: { + if (replayPositionOrder.empty() && replayPositionIndex == 0) + beginReplayPositions(); + prefetchReplayPositions(); + + meshtastic_MeshPacket pkt = {}; + bool havePkt = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (!replayQueue.empty()) { + pkt = replayQueue.front(); + replayQueue.pop_front(); + havePkt = true; + } + } + + if (havePkt) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = pkt; + } else { + LOG_DEBUG("Done replaying positions count=%u millis=%u", (unsigned)replayPositionIndex, millis()); + state = STATE_REPLAY_TELEMETRY; + return getFromRadio(buf); + } + break; + } + + case STATE_REPLAY_TELEMETRY: { + if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0) + beginReplayTelemetry(); + prefetchReplayTelemetry(); + + meshtastic_MeshPacket pkt = {}; + bool havePkt = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (!replayQueue.empty()) { + pkt = replayQueue.front(); + replayQueue.pop_front(); + havePkt = true; + } + } + + if (havePkt) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = pkt; + } else { + LOG_DEBUG("Done replaying telemetry count=%u millis=%u", (unsigned)replayTelemetryIndex, millis()); + state = STATE_REPLAY_ENVIRONMENT; + return getFromRadio(buf); + } + break; + } + + case STATE_REPLAY_ENVIRONMENT: { + if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0) + beginReplayEnvironment(); + prefetchReplayEnvironment(); + + meshtastic_MeshPacket pkt = {}; + bool havePkt = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (!replayQueue.empty()) { + pkt = replayQueue.front(); + replayQueue.pop_front(); + havePkt = true; + } + } + + if (havePkt) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = pkt; + } else { + LOG_DEBUG("Done replaying environment count=%u millis=%u", (unsigned)replayEnvironmentIndex, millis()); + state = STATE_REPLAY_STATUS; + return getFromRadio(buf); + } + break; + } + + case STATE_REPLAY_STATUS: { + if (replayStatusOrder.empty() && replayStatusIndex == 0) + beginReplayStatus(); + prefetchReplayStatus(); + + meshtastic_MeshPacket pkt = {}; + bool havePkt = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (!replayQueue.empty()) { + pkt = replayQueue.front(); + replayQueue.pop_front(); + havePkt = true; + } + } + + if (havePkt) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag; + fromRadioScratch.packet = pkt; + } else { + LOG_DEBUG("Done replaying status count=%u millis=%u", (unsigned)replayStatusIndex, millis()); + replayPositionOrder.clear(); + replayPositionOrder.shrink_to_fit(); + replayTelemetryOrder.clear(); + replayTelemetryOrder.shrink_to_fit(); + replayEnvironmentOrder.clear(); + replayEnvironmentOrder.shrink_to_fit(); + replayStatusOrder.clear(); + replayStatusOrder.shrink_to_fit(); state = STATE_SEND_FILEMANIFEST; - // Go ahead and send that ID right now return getFromRadio(buf); } break; @@ -538,9 +672,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) case STATE_SEND_FILEMANIFEST: { LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); - // last element - if (config_state == filesManifest.size() || - config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest + // ONLY_NODES variants skip the manifest. + if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES || + config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) { config_state = 0; filesManifest.clear(); // Skip to complete packet @@ -653,6 +787,7 @@ void PhoneAPI::prefetchNodeInfos() { bool added = false; bool wasEmpty = false; + const bool gradient = clientWantsGradientSync(); // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. { concurrency::LockGuard guard(&nodeInfoMutex); @@ -662,7 +797,8 @@ void PhoneAPI::prefetchNodeInfos() if (!nextNode) break; - auto info = TypeConversions::ConvertToNodeInfo(nextNode); + auto info = + gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : 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; @@ -682,6 +818,281 @@ void PhoneAPI::prefetchNodeInfos() onNowHasData(0); } +meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos) +{ + meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; + pkt.from = num; + pkt.to = nodeDB->getNodeNum(); + pkt.id = generatePacketId(); + pkt.rx_time = pos.time; + pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + pkt.decoded.portnum = meshtastic_PortNum_POSITION_APP; + meshtastic_Position fullPos = TypeConversions::ConvertToPosition(pos); + size_t len = + pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Position_msg, &fullPos); + pkt.decoded.payload.size = (pb_size_t)len; + return pkt; +} + +meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const meshtastic_DeviceMetrics &metrics) +{ + meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; + pkt.from = num; + pkt.to = nodeDB->getNodeNum(); + pkt.id = generatePacketId(); + // No native timestamp on telemetry packets here; use last_heard. + const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); + pkt.rx_time = header ? header->last_heard : 0; + pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; + meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default; + fullTel.time = pkt.rx_time; + fullTel.which_variant = meshtastic_Telemetry_device_metrics_tag; + fullTel.variant.device_metrics = metrics; + size_t len = + pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel); + pkt.decoded.payload.size = (pb_size_t)len; + return pkt; +} + +void PhoneAPI::beginReplayPositions() +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + // Build excluded entirely - leave the order list empty so the state arm + // immediately drains and advances. + replayPositionOrder.clear(); + replayPositionIndex = 0; +#else + if (!clientWantsGradientSync()) { + replayPositionOrder.clear(); + replayPositionIndex = 0; + return; + } + // Snapshot the keyset at phase start so concurrent inserts/erases on the + // map don't invalidate iteration. Skip our own node - the phone already + // got our position bundled in STATE_SEND_OWN_NODEINFO. + replayPositionOrder.clear(); + NodeNum ourNum = nodeDB->getNodeNum(); + replayPositionOrder.reserve(nodeDB->nodePositions.size()); + for (const auto &kv : nodeDB->nodePositions) { + if (kv.first != ourNum) + replayPositionOrder.push_back(kv.first); + } + replayPositionIndex = 0; + LOG_INFO("Begin position replay: %u entries millis=%u", (unsigned)replayPositionOrder.size(), millis()); +#endif +} + +void PhoneAPI::prefetchReplayPositions() +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + return; +#else + if (!clientWantsGradientSync()) + return; + bool added = false; + bool wasEmpty = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + wasEmpty = replayQueue.empty(); + while (replayQueue.size() < kReplayPrefetchDepth && replayPositionIndex < replayPositionOrder.size()) { + NodeNum num = replayPositionOrder[replayPositionIndex++]; + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(num); + if (!pos) + continue; // entry was evicted between snapshot and now + replayQueue.push_back(makeReplayPositionPacket(num, *pos)); + added = true; + } + } + if (added && wasEmpty) + onNowHasData(0); +#endif +} + +void PhoneAPI::beginReplayTelemetry() +{ +#if MESHTASTIC_EXCLUDE_TELEMETRYDB + replayTelemetryOrder.clear(); + replayTelemetryIndex = 0; +#else + if (!clientWantsGradientSync()) { + replayTelemetryOrder.clear(); + replayTelemetryIndex = 0; + return; + } + replayTelemetryOrder.clear(); + NodeNum ourNum = nodeDB->getNodeNum(); + replayTelemetryOrder.reserve(nodeDB->nodeTelemetry.size()); + for (const auto &kv : nodeDB->nodeTelemetry) { + if (kv.first != ourNum) + replayTelemetryOrder.push_back(kv.first); + } + replayTelemetryIndex = 0; + LOG_INFO("Begin telemetry replay: %u entries millis=%u", (unsigned)replayTelemetryOrder.size(), millis()); +#endif +} + +void PhoneAPI::prefetchReplayTelemetry() +{ +#if MESHTASTIC_EXCLUDE_TELEMETRYDB + return; +#else + if (!clientWantsGradientSync()) + return; + bool added = false; + bool wasEmpty = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + wasEmpty = replayQueue.empty(); + while (replayQueue.size() < kReplayPrefetchDepth && replayTelemetryIndex < replayTelemetryOrder.size()) { + NodeNum num = replayTelemetryOrder[replayTelemetryIndex++]; + const meshtastic_DeviceMetrics *dm = nodeDB->getNodeTelemetry(num); + if (!dm) + continue; + replayQueue.push_back(makeReplayTelemetryPacket(num, *dm)); + added = true; + } + } + if (added && wasEmpty) + onNowHasData(0); +#endif +} + +meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env) +{ + meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; + pkt.from = num; + pkt.to = nodeDB->getNodeNum(); + pkt.id = generatePacketId(); + const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); + pkt.rx_time = header ? header->last_heard : 0; + pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; + meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default; + fullTel.time = pkt.rx_time; + fullTel.which_variant = meshtastic_Telemetry_environment_metrics_tag; + fullTel.variant.environment_metrics = env; + size_t len = + pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel); + pkt.decoded.payload.size = (pb_size_t)len; + return pkt; +} + +void PhoneAPI::beginReplayEnvironment() +{ +#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB + replayEnvironmentOrder.clear(); + replayEnvironmentIndex = 0; +#else + if (!clientWantsGradientSync()) { + replayEnvironmentOrder.clear(); + replayEnvironmentIndex = 0; + return; + } + replayEnvironmentOrder.clear(); + NodeNum ourNum = nodeDB->getNodeNum(); + replayEnvironmentOrder.reserve(nodeDB->nodeEnvironment.size()); + for (const auto &kv : nodeDB->nodeEnvironment) { + if (kv.first != ourNum) + replayEnvironmentOrder.push_back(kv.first); + } + replayEnvironmentIndex = 0; + LOG_INFO("Begin environment replay: %u entries millis=%u", (unsigned)replayEnvironmentOrder.size(), millis()); +#endif +} + +void PhoneAPI::prefetchReplayEnvironment() +{ +#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB + return; +#else + if (!clientWantsGradientSync()) + return; + bool added = false; + bool wasEmpty = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + wasEmpty = replayQueue.empty(); + while (replayQueue.size() < kReplayPrefetchDepth && replayEnvironmentIndex < replayEnvironmentOrder.size()) { + NodeNum num = replayEnvironmentOrder[replayEnvironmentIndex++]; + const meshtastic_EnvironmentMetrics *env = nodeDB->getNodeEnvironment(num); + if (!env) + continue; + replayQueue.push_back(makeReplayEnvironmentPacket(num, *env)); + added = true; + } + } + if (added && wasEmpty) + onNowHasData(0); +#endif +} + +meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status) +{ + meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; + pkt.from = num; + pkt.to = nodeDB->getNodeNum(); + pkt.id = generatePacketId(); + // StatusMessage has no native timestamp; use last_heard. + const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); + pkt.rx_time = header ? header->last_heard : 0; + pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP; + size_t len = + pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_StatusMessage_msg, &status); + pkt.decoded.payload.size = (pb_size_t)len; + return pkt; +} + +void PhoneAPI::beginReplayStatus() +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + replayStatusOrder.clear(); + replayStatusIndex = 0; +#else + if (!clientWantsGradientSync()) { + replayStatusOrder.clear(); + replayStatusIndex = 0; + return; + } + replayStatusOrder.clear(); + NodeNum ourNum = nodeDB->getNodeNum(); + replayStatusOrder.reserve(nodeDB->nodeStatus.size()); + for (const auto &kv : nodeDB->nodeStatus) { + if (kv.first != ourNum) + replayStatusOrder.push_back(kv.first); + } + replayStatusIndex = 0; + LOG_INFO("Begin status replay: %u entries millis=%u", (unsigned)replayStatusOrder.size(), millis()); +#endif +} + +void PhoneAPI::prefetchReplayStatus() +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + return; +#else + if (!clientWantsGradientSync()) + return; + bool added = false; + bool wasEmpty = false; + { + concurrency::LockGuard guard(&nodeInfoMutex); + wasEmpty = replayQueue.empty(); + while (replayQueue.size() < kReplayPrefetchDepth && replayStatusIndex < replayStatusOrder.size()) { + NodeNum num = replayStatusOrder[replayStatusIndex++]; + const meshtastic_StatusMessage *status = nodeDB->getNodeStatus(num); + if (!status || status->status[0] == '\0') + continue; + replayQueue.push_back(makeReplayStatusPacket(num, *status)); + added = true; + } + } + if (added && wasEmpty) + onNowHasData(0); +#endif +} + void PhoneAPI::releaseMqttClientProxyPhonePacket() { if (mqttClientProxyMessageForPhone) { @@ -728,6 +1139,31 @@ bool PhoneAPI::available() PREFETCH_NODEINFO: prefetchNodeInfos(); return true; + case STATE_REPLAY_POSITIONS: { + // Prime the iterator if we haven't yet, then top up the queue. + if (replayPositionOrder.empty() && replayPositionIndex == 0) + beginReplayPositions(); + prefetchReplayPositions(); + return true; // Always advance state machine; arm itself transitions when drained + } + case STATE_REPLAY_TELEMETRY: { + if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0) + beginReplayTelemetry(); + prefetchReplayTelemetry(); + return true; + } + case STATE_REPLAY_ENVIRONMENT: { + if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0) + beginReplayEnvironment(); + prefetchReplayEnvironment(); + return true; + } + case STATE_REPLAY_STATUS: { + if (replayStatusOrder.empty() && replayStatusIndex == 0) + beginReplayStatus(); + prefetchReplayStatus(); + return true; + } case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) queueStatusPacketForPhone = service->getQueueStatusForPhone(); diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 7f79b5792..642fdd7e0 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -4,12 +4,16 @@ #include "concurrency/Lock.h" #include "mesh-pb-constants.h" #include "meshtastic/portnums.pb.h" +#include #include #include #include #include #include +// NodeNum stored as raw uint32_t below; including MeshTypes.h here breaks the +// LOG_WARN include order via Arduino.h/MemoryPool.h. + // Make sure that we never let our packets grow too large for one BLE packet #define MAX_TO_FROM_RADIO_SIZE 512 @@ -22,6 +26,9 @@ #define SPECIAL_NONCE_ONLY_CONFIG 69420 #define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°) +// Gradient sync: phone sends one of these to opt into thin-header + replay. +#define SPECIAL_NONCE_GRADIENT_SYNC 69422 +#define SPECIAL_NONCE_GRADIENT_ONLY_NODES 69423 /** * Provides our protobuf based API which phone/PC clients can use to talk to our device @@ -45,7 +52,13 @@ class PhoneAPI STATE_SEND_CONFIG, // Replacement for the old Radioconfig STATE_SEND_MODULECONFIG, // Send Module specific config STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client - STATE_SEND_FILEMANIFEST, // Send file manifest + // Drain satellite DBs as synthetic POSITION_APP / TELEMETRY_APP / + // NODE_STATUS_APP packets when the phone opted into gradient sync. + STATE_REPLAY_POSITIONS, + STATE_REPLAY_TELEMETRY, + STATE_REPLAY_ENVIRONMENT, + STATE_REPLAY_STATUS, + STATE_SEND_FILEMANIFEST, // Send file manifest STATE_SEND_COMPLETE_ID, STATE_SEND_PACKETS // send packets or debug strings }; @@ -88,6 +101,20 @@ class PhoneAPI // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. concurrency::Lock nodeInfoMutex; + // Synthetic-packet replay queue (paced via prefetch). + std::deque replayQueue; + static constexpr size_t kReplayPrefetchDepth = 4; + // NodeNum snapshots taken at each replay phase start so concurrent + // satellite-map mutations don't invalidate iteration. + std::vector replayPositionOrder; + std::vector replayTelemetryOrder; + std::vector replayEnvironmentOrder; + std::vector replayStatusOrder; + size_t replayPositionIndex = 0; + size_t replayTelemetryIndex = 0; + size_t replayEnvironmentIndex = 0; + size_t replayStatusIndex = 0; + meshtastic_ToRadio toRadioScratch = { 0}; // this is a static scratch object, any data must be copied elsewhere before returning @@ -137,6 +164,10 @@ class PhoneAPI bool isConnected() { return state != STATE_SEND_NOTHING; } bool isSendingPackets() { return state == STATE_SEND_PACKETS; } + bool clientWantsGradientSync() const + { + return config_nonce == SPECIAL_NONCE_GRADIENT_SYNC || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES; + } protected: /// Our fromradio packet while it is being assembled @@ -185,6 +216,18 @@ class PhoneAPI void releaseQueueStatusPhonePacket(); void prefetchNodeInfos(); + void beginReplayPositions(); + void prefetchReplayPositions(); + void beginReplayTelemetry(); + void prefetchReplayTelemetry(); + void beginReplayEnvironment(); + void prefetchReplayEnvironment(); + void beginReplayStatus(); + void prefetchReplayStatus(); + meshtastic_MeshPacket makeReplayPositionPacket(uint32_t num, const meshtastic_PositionLite &pos); + meshtastic_MeshPacket makeReplayTelemetryPacket(uint32_t num, const meshtastic_DeviceMetrics &metrics); + meshtastic_MeshPacket makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env); + meshtastic_MeshPacket makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status); void releaseMqttClientProxyPhonePacket(); diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 27e653efe..eb71da987 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -58,7 +58,7 @@ template class ProtobufModule : protected SinglePortModule const char *getSenderShortName(const meshtastic_MeshPacket &mp) { auto node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node) ? node->user.short_name : "???"; + const char *sender = (node) ? node->short_name : "???"; return sender; } diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 42c24c783..a1cfb1200 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -115,7 +115,7 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); } } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && - (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { + (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->public_key.size == 0)) { LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(*p)); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index ffeb7c539..59e06649b 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -120,17 +120,17 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) if (!node) continue; - // Check 1: is_favorite (cheapest - single bool) - if (!node->is_favorite) + // Check 1: is_favorite (cheapest - single bit test) + if (!nodeInfoLiteIsFavorite(node)) continue; - // Check 2: has_user (cheap - single bool) - if (!node->has_user) + // Check 2: has_user (cheap - single bit test) + if (!nodeInfoLiteHasUser(node)) continue; // Check 3: role check (moderate cost - multiple comparisons) - if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { + if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } @@ -433,7 +433,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) concurrency::LockGuard g(cryptLock); if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && - (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { + !nodeInfoLiteHasUser(nodeDB->getMeshNode(p->from))) { LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); return DecodeState::DECODE_FAILURE; } @@ -451,11 +451,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) #if !(MESHTASTIC_EXCLUDE_PKI) // Attempt PKI decryption first if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && - nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && - rawSize > MESHTASTIC_PKC_OVERHEAD) { + nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr && + nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) { LOG_DEBUG("Attempt PKI decryption"); - if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, + if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes, bytes)) { LOG_INFO("PKI Decryption worked!"); @@ -467,7 +467,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) decrypted = true; LOG_INFO("Packet decrypted using PKI!"); p->pki_encrypted = true; - memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); + memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32); p->public_key.size = 32; p->decoded = decodedtmp; p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded @@ -665,18 +665,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) return meshtastic_Routing_Error_TOO_LARGE; // Check for a known public key for the destination - if (node == nullptr || node->user.public_key.size != 32) { + if (node == nullptr || node->public_key.size != 32) { LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to, p->decoded.portnum); return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; } if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && - memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { + memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) { LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, - *node->user.public_key.bytes); + *node->public_key.bytes); return meshtastic_Routing_Error_PKI_FAILED; } - crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); + crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes); numbytes += MESHTASTIC_PKC_OVERHEAD; p->channel = 0; p->pki_encrypted = true; @@ -860,7 +860,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) } meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); - if (node != NULL && node->is_ignored) { + if (nodeInfoLiteIsIgnored(node)) { LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); packetPool.release(p); return; diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index 3798daf28..392de47f7 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -3,7 +3,9 @@ #include "mesh/generated/meshtastic/mesh.pb.h" #include "meshUtils.h" -meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) +meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite, + const meshtastic_PositionLite *position, + const meshtastic_DeviceMetrics *deviceMetrics) { meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; @@ -11,42 +13,54 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo info.snr = lite->snr; info.last_heard = lite->last_heard; info.channel = lite->channel; - info.via_mqtt = lite->via_mqtt; - info.is_favorite = lite->is_favorite; - info.is_ignored = lite->is_ignored; - info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; + info.via_mqtt = (lite->bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK) != 0; + info.is_favorite = (lite->bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) != 0; + info.is_ignored = (lite->bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK) != 0; + info.is_key_manually_verified = (lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) != 0; + info.is_muted = (lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; if (lite->has_hops_away) { info.has_hops_away = true; info.hops_away = lite->hops_away; } - if (lite->has_position) { + if (position) { info.has_position = true; - if (lite->position.latitude_i != 0) + if (position->latitude_i != 0) info.position.has_latitude_i = true; - info.position.latitude_i = lite->position.latitude_i; - if (lite->position.longitude_i != 0) + info.position.latitude_i = position->latitude_i; + if (position->longitude_i != 0) info.position.has_longitude_i = true; - info.position.longitude_i = lite->position.longitude_i; - if (lite->position.altitude != 0) + info.position.longitude_i = position->longitude_i; + if (position->altitude != 0) info.position.has_altitude = true; - info.position.altitude = lite->position.altitude; - info.position.location_source = lite->position.location_source; - info.position.time = lite->position.time; + info.position.altitude = position->altitude; + info.position.location_source = position->location_source; + info.position.time = position->time; } - if (lite->has_user) { + if (lite->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) { info.has_user = true; - info.user = ConvertToUser(lite->num, lite->user); + info.user = ConvertToUser(lite); } - if (lite->has_device_metrics) { + if (deviceMetrics) { info.has_device_metrics = true; - info.device_metrics = lite->device_metrics; + info.device_metrics = *deviceMetrics; } return info; } +meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) +{ + const meshtastic_PositionLite *pos = nodeDB ? nodeDB->getNodePosition(lite->num) : nullptr; + const meshtastic_DeviceMetrics *dm = nodeDB ? nodeDB->getNodeTelemetry(lite->num) : nullptr; + return ConvertToNodeInfo(lite, pos, dm); +} + +meshtastic_NodeInfo TypeConversions::ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite) +{ + return ConvertToNodeInfo(lite, nullptr, nullptr); +} + meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) { meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; @@ -77,46 +91,50 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l return position; } -meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) +void TypeConversions::CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user) { - meshtastic_UserLite lite = meshtastic_UserLite_init_default; + if (!lite) + return; - strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); - lite.long_name[sizeof(lite.long_name) - 1] = '\0'; - sanitizeUtf8(lite.long_name, sizeof(lite.long_name)); - strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); - lite.short_name[sizeof(lite.short_name) - 1] = '\0'; - sanitizeUtf8(lite.short_name, sizeof(lite.short_name)); - lite.hw_model = user.hw_model; - lite.role = user.role; - lite.is_licensed = user.is_licensed; - memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); - memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); - lite.public_key.size = user.public_key.size; - lite.has_is_unmessagable = user.has_is_unmessagable; - lite.is_unmessagable = user.is_unmessagable; - return lite; + strncpy(lite->long_name, user.long_name, sizeof(lite->long_name)); + lite->long_name[sizeof(lite->long_name) - 1] = '\0'; + sanitizeUtf8(lite->long_name, sizeof(lite->long_name)); + strncpy(lite->short_name, user.short_name, sizeof(lite->short_name)); + lite->short_name[sizeof(lite->short_name) - 1] = '\0'; + sanitizeUtf8(lite->short_name, sizeof(lite->short_name)); + lite->hw_model = user.hw_model; + lite->role = user.role; + memcpy(lite->public_key.bytes, user.public_key.bytes, sizeof(lite->public_key.bytes)); + lite->public_key.size = user.public_key.size; + + nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_LICENSED_MASK, user.is_licensed); + nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable); + nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK, user.is_unmessagable); + nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_USER_MASK, true); } -meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) +meshtastic_User TypeConversions::ConvertToUser(const meshtastic_NodeInfoLite *lite) { meshtastic_User user = meshtastic_User_init_default; + if (!lite) + return user; - snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); - strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); + snprintf(user.id, sizeof(user.id), "!%08x", lite->num); + strncpy(user.long_name, lite->long_name, sizeof(user.long_name)); user.long_name[sizeof(user.long_name) - 1] = '\0'; sanitizeUtf8(user.long_name, sizeof(user.long_name)); - strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); + strncpy(user.short_name, lite->short_name, sizeof(user.short_name)); user.short_name[sizeof(user.short_name) - 1] = '\0'; sanitizeUtf8(user.short_name, sizeof(user.short_name)); - user.hw_model = lite.hw_model; - user.role = lite.role; - user.is_licensed = lite.is_licensed; - memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); - memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); - user.public_key.size = lite.public_key.size; - user.has_is_unmessagable = lite.has_is_unmessagable; - user.is_unmessagable = lite.is_unmessagable; + user.hw_model = lite->hw_model; + user.role = lite->role; + user.is_licensed = (lite->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0; + memcpy(user.public_key.bytes, lite->public_key.bytes, sizeof(user.public_key.bytes)); + user.public_key.size = lite->public_key.size; + user.has_is_unmessagable = (lite->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0; + user.is_unmessagable = (lite->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0; + // macaddr is gone from the slim header; zero-fill for old clients that read it. + memset(user.macaddr, 0, sizeof(user.macaddr)); return user; } \ No newline at end of file diff --git a/src/mesh/TypeConversions.h b/src/mesh/TypeConversions.h index 19e471f98..f9792b7c3 100644 --- a/src/mesh/TypeConversions.h +++ b/src/mesh/TypeConversions.h @@ -7,9 +7,16 @@ class TypeConversions { public: + // Either pointer may be null; the corresponding has_* fields stay unset. + static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite, const meshtastic_PositionLite *position, + const meshtastic_DeviceMetrics *deviceMetrics); static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); + // Identity + link-state only; satellite payloads are replayed afterward. + static meshtastic_NodeInfo ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite); + static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); - static meshtastic_UserLite ConvertToUserLite(meshtastic_User user); - static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); + + static void CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user); + static meshtastic_User ConvertToUser(const meshtastic_NodeInfoLite *lite); }; diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.cpp b/src/mesh/generated/meshtastic/deviceonly.pb.cpp index 5a9695702..558086637 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.cpp +++ b/src/mesh/generated/meshtastic/deviceonly.pb.cpp @@ -18,6 +18,18 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO) PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2) +PB_BIND(meshtastic_NodePositionEntry, meshtastic_NodePositionEntry, AUTO) + + +PB_BIND(meshtastic_NodeTelemetryEntry, meshtastic_NodeTelemetryEntry, AUTO) + + +PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, AUTO) + + +PB_BIND(meshtastic_NodeStatusEntry, meshtastic_NodeStatusEntry, AUTO) + + PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO) diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 6d03dc643..5e0b844f1 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -63,43 +63,35 @@ typedef struct _meshtastic_UserLite { bool is_unmessagable; } meshtastic_UserLite; +typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t; typedef struct _meshtastic_NodeInfoLite { /* The node number */ uint32_t num; - /* The user info for this node */ - bool has_user; - meshtastic_UserLite user; - /* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. - Position.time now indicates the last time we received a POSITION from that node. */ - bool has_position; - meshtastic_PositionLite position; /* Returns the Signal-to-noise ratio (SNR) of the last received message, as measured by the receiver. Return SNR of the last received message in dB */ float snr; /* Set to indicate the last time we received a packet from this node */ uint32_t last_heard; - /* The latest device metrics for the node. */ - bool has_device_metrics; - meshtastic_DeviceMetrics device_metrics; /* local channel index we heard that node on. Only populated if its not the default channel. */ uint8_t channel; - /* True if we witnessed the node over MQTT instead of LoRA transport */ - bool via_mqtt; /* Number of hops away from us this node is (0 if direct neighbor) */ bool has_hops_away; uint8_t hops_away; - /* True if node is in our favorites list - Persists between NodeDB internal clean ups */ - bool is_favorite; - /* True if node is in our ignored list - Persists between NodeDB internal clean ups */ - bool is_ignored; /* Last byte of the node number of the node that should be used as the next hop to reach this node. */ uint8_t next_hop; - /* Bitfield for storing booleans. - LSB 0 is_key_manually_verified - LSB 1 is_muted */ + /* Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */ uint32_t bitfield; + /* A full name for this user, i.e. "Kevin Hester". */ + char long_name[25]; + /* A VERY short name, ideally two characters or an emoji. + Suitable for a tiny OLED screen. */ + char short_name[5]; + /* Hardware model the user's device is running. */ + meshtastic_HardwareModel hw_model; + /* The user's role in the mesh. */ + meshtastic_Config_DeviceConfig_Role role; + /* The public key of the user's device, for PKI-based encrypted DMs. */ + meshtastic_NodeInfoLite_public_key_t public_key; } meshtastic_NodeInfoLite; /* This message is never sent over the wire, but it is used for serializing DB @@ -143,6 +135,30 @@ typedef struct _meshtastic_DeviceState { meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12]; } meshtastic_DeviceState; +typedef struct _meshtastic_NodePositionEntry { + uint32_t num; + bool has_position; + meshtastic_PositionLite position; +} meshtastic_NodePositionEntry; + +typedef struct _meshtastic_NodeTelemetryEntry { + uint32_t num; + bool has_device_metrics; + meshtastic_DeviceMetrics device_metrics; +} meshtastic_NodeTelemetryEntry; + +typedef struct _meshtastic_NodeEnvironmentEntry { + uint32_t num; + bool has_environment_metrics; + meshtastic_EnvironmentMetrics environment_metrics; +} meshtastic_NodeEnvironmentEntry; + +typedef struct _meshtastic_NodeStatusEntry { + uint32_t num; + bool has_status; + meshtastic_StatusMessage status; +} meshtastic_NodeStatusEntry; + typedef struct _meshtastic_NodeDatabase { /* A version integer used to invalidate old save files when we make incompatible changes This integer is set at build time and is private to @@ -150,6 +166,12 @@ typedef struct _meshtastic_NodeDatabase { uint32_t version; /* New lite version of NodeDB to decrease memory footprint */ std::vector nodes; + /* Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit + these via MESHTASTIC_EXCLUDE_*DB build flags. */ + std::vector positions; + std::vector telemetry; + std::vector status; + std::vector environment; } meshtastic_NodeDatabase; /* The on-disk saved channels */ @@ -191,16 +213,24 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0} -#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0} +#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} #define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} -#define meshtastic_NodeDatabase_init_default {0, {0}} +#define meshtastic_NodePositionEntry_init_default {0, false, meshtastic_PositionLite_init_default} +#define meshtastic_NodeTelemetryEntry_init_default {0, false, meshtastic_DeviceMetrics_init_default} +#define meshtastic_NodeEnvironmentEntry_init_default {0, false, meshtastic_EnvironmentMetrics_init_default} +#define meshtastic_NodeStatusEntry_init_default {0, false, meshtastic_StatusMessage_init_default} +#define meshtastic_NodeDatabase_init_default {0, {0}, {0}, {0}, {0}, {0}} #define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0} #define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default} #define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0} -#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0} +#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}} #define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} -#define meshtastic_NodeDatabase_init_zero {0, {0}} +#define meshtastic_NodePositionEntry_init_zero {0, false, meshtastic_PositionLite_init_zero} +#define meshtastic_NodeTelemetryEntry_init_zero {0, false, meshtastic_DeviceMetrics_init_zero} +#define meshtastic_NodeEnvironmentEntry_init_zero {0, false, meshtastic_EnvironmentMetrics_init_zero} +#define meshtastic_NodeStatusEntry_init_zero {0, false, meshtastic_StatusMessage_init_zero} +#define meshtastic_NodeDatabase_init_zero {0, {0}, {0}, {0}, {0}, {0}} #define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0} #define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero} @@ -219,18 +249,17 @@ extern "C" { #define meshtastic_UserLite_public_key_tag 7 #define meshtastic_UserLite_is_unmessagable_tag 9 #define meshtastic_NodeInfoLite_num_tag 1 -#define meshtastic_NodeInfoLite_user_tag 2 -#define meshtastic_NodeInfoLite_position_tag 3 #define meshtastic_NodeInfoLite_snr_tag 4 #define meshtastic_NodeInfoLite_last_heard_tag 5 -#define meshtastic_NodeInfoLite_device_metrics_tag 6 #define meshtastic_NodeInfoLite_channel_tag 7 -#define meshtastic_NodeInfoLite_via_mqtt_tag 8 #define meshtastic_NodeInfoLite_hops_away_tag 9 -#define meshtastic_NodeInfoLite_is_favorite_tag 10 -#define meshtastic_NodeInfoLite_is_ignored_tag 11 #define meshtastic_NodeInfoLite_next_hop_tag 12 #define meshtastic_NodeInfoLite_bitfield_tag 13 +#define meshtastic_NodeInfoLite_long_name_tag 14 +#define meshtastic_NodeInfoLite_short_name_tag 15 +#define meshtastic_NodeInfoLite_hw_model_tag 16 +#define meshtastic_NodeInfoLite_role_tag 17 +#define meshtastic_NodeInfoLite_public_key_tag 18 #define meshtastic_DeviceState_my_node_tag 2 #define meshtastic_DeviceState_owner_tag 3 #define meshtastic_DeviceState_receive_queue_tag 5 @@ -240,8 +269,20 @@ extern "C" { #define meshtastic_DeviceState_did_gps_reset_tag 11 #define meshtastic_DeviceState_rx_waypoint_tag 12 #define meshtastic_DeviceState_node_remote_hardware_pins_tag 13 +#define meshtastic_NodePositionEntry_num_tag 1 +#define meshtastic_NodePositionEntry_position_tag 2 +#define meshtastic_NodeTelemetryEntry_num_tag 1 +#define meshtastic_NodeTelemetryEntry_device_metrics_tag 2 +#define meshtastic_NodeEnvironmentEntry_num_tag 1 +#define meshtastic_NodeEnvironmentEntry_environment_metrics_tag 2 +#define meshtastic_NodeStatusEntry_num_tag 1 +#define meshtastic_NodeStatusEntry_status_tag 2 #define meshtastic_NodeDatabase_version_tag 1 #define meshtastic_NodeDatabase_nodes_tag 2 +#define meshtastic_NodeDatabase_positions_tag 3 +#define meshtastic_NodeDatabase_telemetry_tag 4 +#define meshtastic_NodeDatabase_status_tag 5 +#define meshtastic_NodeDatabase_environment_tag 6 #define meshtastic_ChannelFile_channels_tag 1 #define meshtastic_ChannelFile_version_tag 2 #define meshtastic_BackupPreferences_version_tag 1 @@ -275,23 +316,19 @@ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9) #define meshtastic_NodeInfoLite_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, num, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ -X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \ X(a, STATIC, SINGULAR, FLOAT, snr, 4) \ X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ -X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \ X(a, STATIC, SINGULAR, UINT32, channel, 7) \ -X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ -X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \ -X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \ -X(a, STATIC, SINGULAR, UINT32, bitfield, 13) +X(a, STATIC, SINGULAR, UINT32, bitfield, 13) \ +X(a, STATIC, SINGULAR, STRING, long_name, 14) \ +X(a, STATIC, SINGULAR, STRING, short_name, 15) \ +X(a, STATIC, SINGULAR, UENUM, hw_model, 16) \ +X(a, STATIC, SINGULAR, UENUM, role, 17) \ +X(a, STATIC, SINGULAR, BYTES, public_key, 18) #define meshtastic_NodeInfoLite_CALLBACK NULL #define meshtastic_NodeInfoLite_DEFAULT NULL -#define meshtastic_NodeInfoLite_user_MSGTYPE meshtastic_UserLite -#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite -#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics #define meshtastic_DeviceState_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \ @@ -312,13 +349,49 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13) #define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket #define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin +#define meshtastic_NodePositionEntry_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, position, 2) +#define meshtastic_NodePositionEntry_CALLBACK NULL +#define meshtastic_NodePositionEntry_DEFAULT NULL +#define meshtastic_NodePositionEntry_position_MSGTYPE meshtastic_PositionLite + +#define meshtastic_NodeTelemetryEntry_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 2) +#define meshtastic_NodeTelemetryEntry_CALLBACK NULL +#define meshtastic_NodeTelemetryEntry_DEFAULT NULL +#define meshtastic_NodeTelemetryEntry_device_metrics_MSGTYPE meshtastic_DeviceMetrics + +#define meshtastic_NodeEnvironmentEntry_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, environment_metrics, 2) +#define meshtastic_NodeEnvironmentEntry_CALLBACK NULL +#define meshtastic_NodeEnvironmentEntry_DEFAULT NULL +#define meshtastic_NodeEnvironmentEntry_environment_metrics_MSGTYPE meshtastic_EnvironmentMetrics + +#define meshtastic_NodeStatusEntry_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, status, 2) +#define meshtastic_NodeStatusEntry_CALLBACK NULL +#define meshtastic_NodeStatusEntry_DEFAULT NULL +#define meshtastic_NodeStatusEntry_status_MSGTYPE meshtastic_StatusMessage + #define meshtastic_NodeDatabase_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, version, 1) \ -X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) +X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) \ +X(a, CALLBACK, REPEATED, MESSAGE, positions, 3) \ +X(a, CALLBACK, REPEATED, MESSAGE, telemetry, 4) \ +X(a, CALLBACK, REPEATED, MESSAGE, status, 5) \ +X(a, CALLBACK, REPEATED, MESSAGE, environment, 6) extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); #define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback #define meshtastic_NodeDatabase_DEFAULT NULL #define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite +#define meshtastic_NodeDatabase_positions_MSGTYPE meshtastic_NodePositionEntry +#define meshtastic_NodeDatabase_telemetry_MSGTYPE meshtastic_NodeTelemetryEntry +#define meshtastic_NodeDatabase_status_MSGTYPE meshtastic_NodeStatusEntry +#define meshtastic_NodeDatabase_environment_MSGTYPE meshtastic_NodeEnvironmentEntry #define meshtastic_ChannelFile_FIELDLIST(X, a) \ X(a, STATIC, REPEATED, MESSAGE, channels, 1) \ @@ -345,6 +418,10 @@ extern const pb_msgdesc_t meshtastic_PositionLite_msg; extern const pb_msgdesc_t meshtastic_UserLite_msg; extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg; extern const pb_msgdesc_t meshtastic_DeviceState_msg; +extern const pb_msgdesc_t meshtastic_NodePositionEntry_msg; +extern const pb_msgdesc_t meshtastic_NodeTelemetryEntry_msg; +extern const pb_msgdesc_t meshtastic_NodeEnvironmentEntry_msg; +extern const pb_msgdesc_t meshtastic_NodeStatusEntry_msg; extern const pb_msgdesc_t meshtastic_NodeDatabase_msg; extern const pb_msgdesc_t meshtastic_ChannelFile_msg; extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; @@ -354,6 +431,10 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; #define meshtastic_UserLite_fields &meshtastic_UserLite_msg #define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg #define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg +#define meshtastic_NodePositionEntry_fields &meshtastic_NodePositionEntry_msg +#define meshtastic_NodeTelemetryEntry_fields &meshtastic_NodeTelemetryEntry_msg +#define meshtastic_NodeEnvironmentEntry_fields &meshtastic_NodeEnvironmentEntry_msg +#define meshtastic_NodeStatusEntry_fields &meshtastic_NodeStatusEntry_msg #define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg #define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg #define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg @@ -364,7 +445,11 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; #define meshtastic_BackupPreferences_size 2432 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 -#define meshtastic_NodeInfoLite_size 196 +#define meshtastic_NodeEnvironmentEntry_size 170 +#define meshtastic_NodeInfoLite_size 105 +#define meshtastic_NodePositionEntry_size 36 +#define meshtastic_NodeStatusEntry_size 89 +#define meshtastic_NodeTelemetryEntry_size 35 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 98 diff --git a/src/mesh/generated/meshtastic/deviceonly_legacy.pb.cpp b/src/mesh/generated/meshtastic/deviceonly_legacy.pb.cpp new file mode 100644 index 000000000..04c3a98d9 --- /dev/null +++ b/src/mesh/generated/meshtastic/deviceonly_legacy.pb.cpp @@ -0,0 +1,15 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.9.1 */ + +#include "meshtastic/deviceonly_legacy.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(meshtastic_NodeInfoLite_Legacy, meshtastic_NodeInfoLite_Legacy, AUTO) + + +PB_BIND(meshtastic_NodeDatabase_Legacy, meshtastic_NodeDatabase_Legacy, AUTO) + + + diff --git a/src/mesh/generated/meshtastic/deviceonly_legacy.pb.h b/src/mesh/generated/meshtastic/deviceonly_legacy.pb.h new file mode 100644 index 000000000..b0364c211 --- /dev/null +++ b/src/mesh/generated/meshtastic/deviceonly_legacy.pb.h @@ -0,0 +1,122 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.9.1 */ + +#ifndef PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED +#define PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED +#include +#include +#include "meshtastic/deviceonly.pb.h" +#include "meshtastic/telemetry.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +/* Legacy NodeInfoLite descriptor used only to decode pre-split + /prefs/nodes.proto saves during the v24 -> v25 migration boot. + Field tags here mirror the original NodeInfoLite tags 3 and 6 so that + old wire bytes parse cleanly. Steady-state code does not use this + struct; it is dropped after migration completes. This file should be + removed once DEVICESTATE_MIN_VER advances past 24. */ +typedef struct _meshtastic_NodeInfoLite_Legacy { + uint32_t num; + bool has_user; + meshtastic_UserLite user; + bool has_position; + meshtastic_PositionLite position; + float snr; + uint32_t last_heard; + bool has_device_metrics; + meshtastic_DeviceMetrics device_metrics; + uint8_t channel; + bool via_mqtt; + bool has_hops_away; + uint8_t hops_away; + bool is_favorite; + bool is_ignored; + uint8_t next_hop; + uint32_t bitfield; +} meshtastic_NodeInfoLite_Legacy; + +/* Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy + with no satellite position/telemetry arrays. */ +typedef struct _meshtastic_NodeDatabase_Legacy { + uint32_t version; + std::vector nodes; +} meshtastic_NodeDatabase_Legacy; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define meshtastic_NodeInfoLite_Legacy_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0} +#define meshtastic_NodeDatabase_Legacy_init_default {0, {0}} +#define meshtastic_NodeInfoLite_Legacy_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0} +#define meshtastic_NodeDatabase_Legacy_init_zero {0, {0}} + +/* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_NodeInfoLite_Legacy_num_tag 1 +#define meshtastic_NodeInfoLite_Legacy_user_tag 2 +#define meshtastic_NodeInfoLite_Legacy_position_tag 3 +#define meshtastic_NodeInfoLite_Legacy_snr_tag 4 +#define meshtastic_NodeInfoLite_Legacy_last_heard_tag 5 +#define meshtastic_NodeInfoLite_Legacy_device_metrics_tag 6 +#define meshtastic_NodeInfoLite_Legacy_channel_tag 7 +#define meshtastic_NodeInfoLite_Legacy_via_mqtt_tag 8 +#define meshtastic_NodeInfoLite_Legacy_hops_away_tag 9 +#define meshtastic_NodeInfoLite_Legacy_is_favorite_tag 10 +#define meshtastic_NodeInfoLite_Legacy_is_ignored_tag 11 +#define meshtastic_NodeInfoLite_Legacy_next_hop_tag 12 +#define meshtastic_NodeInfoLite_Legacy_bitfield_tag 13 +#define meshtastic_NodeDatabase_Legacy_version_tag 1 +#define meshtastic_NodeDatabase_Legacy_nodes_tag 2 + +/* Struct field encoding specification for nanopb */ +#define meshtastic_NodeInfoLite_Legacy_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, num, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \ +X(a, STATIC, SINGULAR, FLOAT, snr, 4) \ +X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ +X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \ +X(a, STATIC, SINGULAR, UINT32, channel, 7) \ +X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \ +X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ +X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \ +X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \ +X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \ +X(a, STATIC, SINGULAR, UINT32, bitfield, 13) +#define meshtastic_NodeInfoLite_Legacy_CALLBACK NULL +#define meshtastic_NodeInfoLite_Legacy_DEFAULT NULL +#define meshtastic_NodeInfoLite_Legacy_user_MSGTYPE meshtastic_UserLite +#define meshtastic_NodeInfoLite_Legacy_position_MSGTYPE meshtastic_PositionLite +#define meshtastic_NodeInfoLite_Legacy_device_metrics_MSGTYPE meshtastic_DeviceMetrics + +#define meshtastic_NodeDatabase_Legacy_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, version, 1) \ +X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) +extern bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); +#define meshtastic_NodeDatabase_Legacy_CALLBACK meshtastic_NodeDatabase_Legacy_callback +#define meshtastic_NodeDatabase_Legacy_DEFAULT NULL +#define meshtastic_NodeDatabase_Legacy_nodes_MSGTYPE meshtastic_NodeInfoLite_Legacy + +extern const pb_msgdesc_t meshtastic_NodeInfoLite_Legacy_msg; +extern const pb_msgdesc_t meshtastic_NodeDatabase_Legacy_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define meshtastic_NodeInfoLite_Legacy_fields &meshtastic_NodeInfoLite_Legacy_msg +#define meshtastic_NodeDatabase_Legacy_fields &meshtastic_NodeDatabase_Legacy_msg + +/* Maximum encoded size of messages (where known) */ +/* meshtastic_NodeDatabase_Legacy_size depends on runtime parameters */ +#define MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_MAX_SIZE meshtastic_NodeInfoLite_Legacy_size +#define meshtastic_NodeInfoLite_Legacy_size 196 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 6b3aa4859..16ef2a844 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -729,7 +729,7 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) uint32_t readIndex = 0; const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); while (tempNodeInfo != NULL) { - if (tempNodeInfo->has_user) { + if (nodeInfoLiteHasUser(tempNodeInfo)) { JSONObject node; char id[16]; @@ -737,26 +737,26 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) node["id"] = new JSONValue(id); node["snr"] = new JSONValue(tempNodeInfo->snr); - node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt)); + node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo))); node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard); node["position"] = new JSONValue(); if (nodeDB->hasValidPosition(tempNodeInfo)) { - JSONObject position; - position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7); - position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7); - position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude); - node["position"] = new JSONValue(position); + const meshtastic_PositionLite *posLite = nodeDB->getNodePosition(tempNodeInfo->num); + if (posLite) { + JSONObject position; + position["latitude"] = new JSONValue((float)posLite->latitude_i * 1e-7); + position["longitude"] = new JSONValue((float)posLite->longitude_i * 1e-7); + position["altitude"] = new JSONValue((int)posLite->altitude); + node["position"] = new JSONValue(position); + } } - node["long_name"] = new JSONValue(tempNodeInfo->user.long_name); - node["short_name"] = new JSONValue(tempNodeInfo->user.short_name); - char macStr[18]; - snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0], - tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3], - tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]); - node["mac_address"] = new JSONValue(macStr); - node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model); + node["long_name"] = new JSONValue(tempNodeInfo->long_name); + node["short_name"] = new JSONValue(tempNodeInfo->short_name); + // mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros. + node["mac_address"] = new JSONValue("00:00:00:00:00:00"); + node["hw_model"] = new JSONValue(tempNodeInfo->hw_model); nodesArray.push_back(new JSONValue(node)); } diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index eea7d4efc..2fbb355b4 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -37,9 +37,43 @@ #define MAX_RX_NOTIFICATION_TOPHONE 2 #endif -/// Verify baseline assumption of node size. If it increases, we need to reevaluate -/// the impact of its memory footprint, notably on MAX_NUM_NODES. -static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); +/// Tighten this when the slim header shrinks; loosen only with deliberate +/// awareness of MAX_NUM_NODES impact per platform. +static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); + +// Compile satellite NodeDBs out on STM32WL (and the status DB also follows +// MESHTASTIC_EXCLUDE_STATUS). +#ifndef MESHTASTIC_EXCLUDE_POSITIONDB +#if defined(ARCH_STM32WL) +#define MESHTASTIC_EXCLUDE_POSITIONDB 1 +#else +#define MESHTASTIC_EXCLUDE_POSITIONDB 0 +#endif +#endif + +#ifndef MESHTASTIC_EXCLUDE_TELEMETRYDB +#if defined(ARCH_STM32WL) +#define MESHTASTIC_EXCLUDE_TELEMETRYDB 1 +#else +#define MESHTASTIC_EXCLUDE_TELEMETRYDB 0 +#endif +#endif + +#ifndef MESHTASTIC_EXCLUDE_ENVIRONMENTDB +#if defined(ARCH_STM32WL) +#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 1 +#else +#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 0 +#endif +#endif + +#ifndef MESHTASTIC_EXCLUDE_STATUSDB +#if defined(ARCH_STM32WL) || defined(MESHTASTIC_EXCLUDE_STATUS) +#define MESHTASTIC_EXCLUDE_STATUSDB 1 +#else +#define MESHTASTIC_EXCLUDE_STATUSDB 0 +#endif +#endif /// max number of nodes allowed in the nodeDB #ifndef MAX_NUM_NODES diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 7b249f656..31cf377ee 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta // Automatically favorite the node that is using the admin key auto remoteNode = nodeDB->getMeshNode(mp.from); - if (remoteNode && !remoteNode->is_favorite) { + if (remoteNode && !nodeInfoLiteIsFavorite(remoteNode)) { if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it // without the user doing so deliberately. LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); } else { LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); - remoteNode->is_favorite = true; + nodeInfoLiteSetBit(remoteNode, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); } } } else { @@ -393,7 +393,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received set_favorite_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); if (node != NULL) { - node->is_favorite = true; + nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens @@ -404,7 +404,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received remove_favorite_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); if (node != NULL) { - node->is_favorite = false; + nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false); saveChanges(SEGMENT_NODEDATABASE, false); if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens @@ -415,11 +415,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received set_ignored_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); if (node != NULL) { - node->is_ignored = true; - node->has_device_metrics = false; - node->has_position = false; - node->user.public_key.size = 0; - memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes)); + nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true); + nodeDB->eraseNodeSatellites(node->num); + node->public_key.size = 0; + memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes)); saveChanges(SEGMENT_NODEDATABASE, false); } break; @@ -428,7 +427,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received remove_ignored_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); if (node != NULL) { - node->is_ignored = false; + nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, false); saveChanges(SEGMENT_NODEDATABASE, false); } break; @@ -446,8 +445,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta case meshtastic_AdminMessage_set_fixed_position_tag: { LOG_INFO("Client received set_fixed_position command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - node->has_position = true; - node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); + // Route the fixed position through updatePosition so it lands in the + // satellite map (or, on builds with PositionDB excluded, just sets + // localPosition for the local broadcast path). + nodeDB->updatePosition(node->num, r->set_fixed_position, RX_SRC_LOCAL); nodeDB->setLocalPosition(r->set_fixed_position); config.position.fixed_position = true; saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 65e903134..a29b9fa58 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -141,7 +141,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t static bool returnToCannedList = false; bool hasKeyForNode(const meshtastic_NodeInfoLite *node) { - return node && node->has_user && node->user.public_key.size > 0; + return nodeInfoLiteHasUser(node) && node->public_key.size > 0; } /** * @brief Items in array this->messages will be set to be pointing on the right @@ -262,10 +262,10 @@ void CannedMessageModule::updateDestinationSelectionList() for (size_t i = 0; i < numMeshNodes; ++i) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) + if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32) continue; - const String &nodeName = node->user.long_name; + const String &nodeName = node->long_name; if (searchQuery.length() == 0) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); @@ -1054,7 +1054,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha } NodeNum myNodeNum = nodeDB->getNodeNum(); - if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { + if (node && node->num != myNodeNum && nodeInfoLiteHasUser(node) && node->public_key.size == 32) { p->pki_encrypted = true; p->channel = 0; // force PKI } @@ -1415,8 +1415,8 @@ const char *CannedMessageModule::getNodeName(NodeNum node) return "Broadcast"; meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user && strlen(info->user.long_name) > 0) { - return info->user.long_name; + if (nodeInfoLiteHasUser(info) && strlen(info->long_name) > 0) { + return info->long_name; } static char fallback[12]; @@ -1723,17 +1723,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node) { if (display->getWidth() <= 64) { - entryText = node->user.short_name; - } else if (node->user.long_name[0]) { - entryText = node->user.long_name; + entryText = node->short_name; + } else if (node->long_name[0]) { + entryText = node->long_name; } else { - entryText = node->user.short_name; + entryText = node->short_name; } } int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - - ((node && node->is_favorite) ? 10 : 0); + ((nodeInfoLiteIsFavorite(node)) ? 10 : 0); if (availWidth < 0) availWidth = 0; char truncatedEntry[96]; @@ -1742,7 +1742,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O entryText = truncatedEntry; // Prepend "* " if this is a favorite - if (node && node->is_favorite) { + if (nodeInfoLiteIsFavorite(node)) { entryText = "* " + entryText; } graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 6d0255d53..e2f7b4ca3 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -73,7 +73,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; cn->payload_variant.key_verification_number_request.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + nodeDB->getMeshNode(currentRemoteNode)->long_name, sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); service->sendClientNotification(cn); LOG_INFO("Received hash2"); @@ -105,7 +105,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + nodeDB->getMeshNode(currentRemoteNode)->long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); cn->payload_variant.key_verification_final.isSender = false; service->sendClientNotification(cn); @@ -203,7 +203,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply() cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; cn->payload_variant.key_verification_number_inform.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + nodeDB->getMeshNode(currentRemoteNode)->long_name, sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; service->sendClientNotification(cn); @@ -219,7 +219,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) LOG_WARN("received security number: %u", incomingNumber); meshtastic_NodeInfoLite *remoteNodePtr = nullptr; remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { + if (!nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) { currentState = KEY_VERIFICATION_IDLE; return; // should we throw an error here? } @@ -232,7 +232,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); hash.update(owner.public_key.bytes, owner.public_key.size); - hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size); hash.finalize(hash1, 32); hash.reset(); @@ -266,7 +266,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc - nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + nodeDB->getMeshNode(currentRemoteNode)->long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname)); cn->payload_variant.key_verification_final.isSender = true; service->sendClientNotification(cn); diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index ac81e9c57..0c6b04fc7 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -184,7 +184,6 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() } meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position - assert(node->has_position); // configuration of POSITION packet // consider making this a function argument? @@ -194,7 +193,9 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure // if localPosition is totally empty, put our last saved position (lite) in there if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); + const meshtastic_PositionLite *cachedSelf = nodeDB->getNodePosition(node->num); + if (cachedSelf) + nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(*cachedSelf)); } localPosition.seq_number++; @@ -445,8 +446,11 @@ int32_t PositionModule::runOnce() } else if (nodeDB->hasValidPosition(node)) { lastGpsSend = now; - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + const meshtastic_PositionLite *selfPos = nodeDB->getNodePosition(node->num); + if (selfPos) { + lastGpsLatitude = selfPos->latitude_i; + lastGpsLongitude = selfPos->longitude_i; + } if (transmitHistory) transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); @@ -461,7 +465,10 @@ int32_t PositionModule::runOnce() if (nodeDB->hasValidPosition(node2)) { // The minimum time (in seconds) that would pass before we are able to send a new position packet. - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + const meshtastic_PositionLite *selfPos = nodeDB->getNodePosition(node->num); + if (!selfPos) + return RUNONCE_INTERVAL; // Defensive: hasValidPosition should imply this is non-null + auto smartPosition = getDistanceTraveledSinceLastSend(*selfPos); msSinceLastSend = now - lastGpsSend; if (smartPosition.hasTraveledOverThreshold && @@ -479,8 +486,8 @@ int32_t PositionModule::runOnce() msSinceLastSend, minimumTimeThreshold); // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + lastGpsLatitude = selfPos->latitude_i; + lastGpsLongitude = selfPos->longitude_i; } } } @@ -567,7 +574,10 @@ void PositionModule::handleNewPosition() const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position // We limit our GPS broadcasts to a max rate if (nodeDB->hasValidPosition(node2)) { - auto smartPosition = getDistanceTraveledSinceLastSend(node->position); + const meshtastic_PositionLite *selfPos = nodeDB->getNodePosition(node->num); + if (!selfPos) + return; + auto smartPosition = getDistanceTraveledSinceLastSend(*selfPos); uint32_t msSinceLastSend = millis() - lastGpsSend; if (smartPosition.hasTraveledOverThreshold && Throttle::execute( @@ -583,8 +593,8 @@ void PositionModule::handleNewPosition() minimumTimeThreshold); // Set the current coords as our last ones, after we've compared distance with current and decided to send - lastGpsLatitude = node->position.latitude_i; - lastGpsLongitude = node->position.longitude_i; + lastGpsLatitude = selfPos->latitude_i; + lastGpsLongitude = selfPos->longitude_i; } } } diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 026b3028d..ac8e87954 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -268,26 +268,35 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) fileToAppend.printf("??:??:??,"); // Time } - fileToAppend.printf("%d,", getFrom(&mp)); // From - fileToAppend.printf("%s,", n->user.long_name); // Long Name - fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat - fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long + fileToAppend.printf("%d,", getFrom(&mp)); // From + fileToAppend.printf("%s,", n ? n->long_name : ""); // Long Name + const meshtastic_PositionLite *senderPos = nodeDB->getNodePosition(n->num); + if (senderPos) { + fileToAppend.printf("%f,", senderPos->latitude_i * 1e-7); // Sender Lat + fileToAppend.printf("%f,", senderPos->longitude_i * 1e-7); // Sender Long + } else { + fileToAppend.printf("0.0,0.0,"); + } if (gpsStatus->getIsConnected() || config.position.fixed_position) { fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude } else { // When the phone API is in use, the node info will be updated with position - meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); - fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat - fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long - fileToAppend.printf("%d,", us->position.altitude); // RX Altitude + const meshtastic_PositionLite *usPos = nodeDB->getNodePosition(nodeDB->getNodeNum()); + if (usPos) { + fileToAppend.printf("%f,", usPos->latitude_i * 1e-7); // RX Lat + fileToAppend.printf("%f,", usPos->longitude_i * 1e-7); // RX Long + fileToAppend.printf("%d,", usPos->altitude); // RX Altitude + } else { + fileToAppend.printf("0.0,0.0,0,"); + } } fileToAppend.printf("%f,", mp.rx_snr); // RX SNR - if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { - float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, + if (senderPos && senderPos->latitude_i && senderPos->longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { + float distance = GeoCoord::latLongToMeter(senderPos->latitude_i * 1e-7, senderPos->longitude_i * 1e-7, gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); fileToAppend.printf("%f,", distance); // Distance in meters } else { diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 85e7f8c06..a46323ac6 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -17,8 +17,7 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { if (!maybePKI) return false; - if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && - (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) + if (!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.from)) && !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.to))) return false; } else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) || (nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) { diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 20d4d7d8c..e46583686 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -250,9 +250,12 @@ int32_t SerialModule::runOnce() uint32_t readIndex = 0; const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); while (tempNodeInfo != NULL) { - if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { - printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); - serialPrint->printf("%s", outbuf); + if (nodeInfoLiteHasUser(tempNodeInfo) && nodeDB->hasValidPosition(tempNodeInfo)) { + const meshtastic_PositionLite *pos = nodeDB->getNodePosition(tempNodeInfo->num); + if (pos) { + printWPL(outbuf, sizeof(outbuf), *pos, tempNodeInfo->long_name, true); + serialPrint->printf("%s", outbuf); + } } tempNodeInfo = nodeDB->readNextMeshNode(readIndex); } @@ -401,7 +404,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp serialPrint->write(p.payload.bytes, p.payload.size); } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - const char *sender = (node && node->has_user) ? node->user.short_name : "???"; + const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???"; serialPrint->println(); serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->println(); @@ -417,9 +420,13 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp decoded = &scratch; } // send position packet as WPL to the serial port - printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, - moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); - serialPrint->printf("%s", outbuf); + { + meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp)); + const char *senderName = senderNode ? senderNode->long_name : ""; + printWPL(outbuf, sizeof(outbuf), *decoded, senderName, + moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); + serialPrint->printf("%s", outbuf); + } } } } diff --git a/src/modules/StatusMessageModule.cpp b/src/modules/StatusMessageModule.cpp index 0707a4f7d..2f05517f3 100644 --- a/src/modules/StatusMessageModule.cpp +++ b/src/modules/StatusMessageModule.cpp @@ -2,6 +2,7 @@ #include "StatusMessageModule.h" #include "MeshService.h" +#include "NodeDB.h" #include "ProtobufModule.h" StatusMessageModule *statusMessageModule; @@ -36,16 +37,8 @@ ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket & LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); - RecentStatus entry; - entry.fromNodeId = mp.from; - entry.statusText = incomingMessage.status; - - recentReceived.push_back(std::move(entry)); - - // Keep only last MAX_RECENT_STATUSMESSAGES - if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) { - recentReceived.erase(recentReceived.begin()); // drop oldest - } + if (nodeDB) + nodeDB->setNodeStatus(mp.from, incomingMessage); } } return ProcessMessage::CONTINUE; diff --git a/src/modules/StatusMessageModule.h b/src/modules/StatusMessageModule.h index 5090066e6..ba32b23e8 100644 --- a/src/modules/StatusMessageModule.h +++ b/src/modules/StatusMessageModule.h @@ -2,8 +2,6 @@ #if !MESHTASTIC_EXCLUDE_STATUS #include "SinglePortModule.h" #include "configuration.h" -#include -#include class StatusMessageModule : public SinglePortModule, private concurrency::OSThread { @@ -20,28 +18,15 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre this->setInterval(1000 * 12 * 60 * 60); } // TODO: If we have a string, set the initial delay (15 minutes maybe) - - // Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES - recentReceived.reserve(MAX_RECENT_STATUSMESSAGES); } virtual int32_t runOnce() override; - struct RecentStatus { - uint32_t fromNodeId; // mp.from - std::string statusText; // incomingMessage.status - }; - - const std::vector &getRecentReceived() const { return recentReceived; } - protected: - /** Called to handle a particular incoming message + /** Called to handle a particular incoming message. The cached most-recent + * status for each node lives on NodeDB; use nodeDB->getNodeStatus(num). */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - - private: - static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5; - std::vector recentReceived; }; extern StatusMessageModule *statusMessageModule; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 684d408a1..04a5370de 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -515,6 +515,10 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac packetPool.release(lastMeasurementPacket); lastMeasurementPacket = packetPool.allocCopy(mp); + + // Cache the latest env metrics per node on NodeDB so the phone can + // pull last-known values across reboots and replays. + nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); } return false; // Let others look at this message also if they want diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 3371c405d..b6cb5d041 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -492,12 +492,12 @@ TraceRouteModule::TraceRouteModule() const char *TraceRouteModule::getNodeName(NodeNum node) { meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info && info->has_user) { - if (strlen(info->user.short_name) > 0) { - return info->user.short_name; + if (nodeInfoLiteHasUser(info)) { + if (strlen(info->short_name) > 0) { + return info->short_name; } - if (strlen(info->user.long_name) > 0) { - return info->user.long_name; + if (strlen(info->long_name) > 0) { + return info->long_name; } } diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index 1ecb68c4b..4c73c2947 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -1226,9 +1226,9 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke // Fallback only when PSRAM cache is unavailable on this target. // In this mode we use the node-wide table maintained by NodeInfoModule. const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); - if (!node || !node->has_user) + if (!nodeInfoLiteHasUser(node)) return false; - cachedUser = TypeConversions::ConvertToUser(node->num, node->user); + cachedUser = TypeConversions::ConvertToUser(node); } if (!sendResponse) diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 632727b92..0437f0dd5 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -147,8 +147,9 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, const char *statusLine2 = nullptr; // Distance only needs our own position fix; compass/bearing additionally needs heading. - if (hasOwnPositionFix) { - const meshtastic_PositionLite &op = ourNode->position; + const meshtastic_PositionLite *ownPos = ourNode ? nodeDB->getNodePosition(ourNode->num) : nullptr; + if (hasOwnPositionFix && ownPos) { + const meshtastic_PositionLite &op = *ownPos; const float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 97636b601..649cd7d01 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -141,7 +141,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's // likely they discovered each other via a channel we have downlink enabled for - if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) + if (isToUs(p.get()) || (nodeInfoLiteHasUser(tx) && nodeInfoLiteHasUser(rx))) router->enqueueReceivedMessage(p.release()); } else if (router && perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 819ba3da5..021730c20 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -320,9 +320,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, auto addToRoute = [](JSONArray *route, NodeNum num) { char long_name[40] = "Unknown"; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; + bool name_known = nodeInfoLiteHasUser(node); if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); + memcpy(long_name, node->long_name, sizeof(long_name)); route->push_back(new JSONValue(long_name)); }; addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index bd0a29c51..4656cd8f7 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -292,9 +292,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, auto addToRoute = [](JsonArray *route, NodeNum num) { char long_name[40] = "Unknown"; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); - bool name_known = node ? node->has_user : false; + bool name_known = nodeInfoLiteHasUser(node); if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); + memcpy(long_name, node->long_name, sizeof(long_name)); route->add(long_name); }; diff --git a/test/test_crypto/test_main.cpp b/test/test_crypto/test_main.cpp index 36dc37b9d..15f372398 100644 --- a/test/test_crypto/test_main.cpp +++ b/test/test_crypto/test_main.cpp @@ -113,7 +113,7 @@ void test_DH25519(void) void test_PKC(void) { uint8_t private_key[32]; - meshtastic_UserLite_public_key_t public_key; + meshtastic_NodeInfoLite_public_key_t public_key; uint8_t expected_shared[32]; uint8_t expected_decrypted[32]; uint8_t radioBytes[128] __attribute__((__aligned__)); diff --git a/test/test_traffic_management/test_main.cpp b/test/test_traffic_management/test_main.cpp index ec54f2312..734b4a970 100644 --- a/test/test_traffic_management/test_main.cpp +++ b/test/test_traffic_management/test_main.cpp @@ -50,7 +50,7 @@ class MockNodeDB : public NodeDB hasCachedNode = true; cachedNodeNum = n; cachedNode.num = n; - cachedNode.has_user = true; + cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK; } private: @@ -494,9 +494,9 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void) TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); TEST_ASSERT_NOT_NULL(requestor); - TEST_ASSERT_TRUE(requestor->has_user); - TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); - TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); + TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0); + TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name); + TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name); TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); } diff --git a/test/test_type_conversions/test_main.cpp b/test/test_type_conversions/test_main.cpp new file mode 100644 index 000000000..a02d9bdf8 --- /dev/null +++ b/test/test_type_conversions/test_main.cpp @@ -0,0 +1,406 @@ +// Tests for src/mesh/TypeConversions.cpp covering: +// - bitfield bit collapse on store + extraction round-trip +// - long_name / short_name truncation at the new max_size:25 / 5 boundaries +// - public_key / hw_model / role pass-through +// - thin vs bundled NodeInfo emission +// +// All exercised via the explicit-args overload of ConvertToNodeInfo so we don't +// touch the global nodeDB pointer (which isn't initialized in this test env). + +#include "NodeDB.h" +#include "TestUtil.h" +#include "TypeConversions.h" +#include +#include +#include + +void setUp(void) {} +void tearDown(void) {} + +// ---------- helpers ----------------------------------------------------------- + +static meshtastic_User makeUser(const char *longName, const char *shortName) +{ + meshtastic_User u = meshtastic_User_init_zero; + if (longName) + strncpy(u.long_name, longName, sizeof(u.long_name) - 1); + if (shortName) + strncpy(u.short_name, shortName, sizeof(u.short_name) - 1); + u.hw_model = meshtastic_HardwareModel_TBEAM; + u.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + return u; +} + +// ---------- has_user / id / macaddr ------------------------------------------ + +void test_copy_user_sets_has_user_bit(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser("Kevin Hester", "kh"); + TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0); + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0); +} + +void test_convert_to_user_id_derived_from_nodenum(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0x12345678; + meshtastic_User u = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_EQUAL_STRING("!12345678", u.id); +} + +void test_convert_to_user_zero_fills_macaddr(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xCAFEBABE; + meshtastic_User u = TypeConversions::ConvertToUser(&lite); + uint8_t zeros[sizeof(u.macaddr)] = {0}; + TEST_ASSERT_EQUAL_MEMORY(zeros, u.macaddr, sizeof(u.macaddr)); +} + +// ---------- long_name truncation --------------------------------------------- + +void test_long_name_short_passes_through(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser("Kevin Hester", "kh"); // 12 chars, fits + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_EQUAL_STRING("Kevin Hester", lite.long_name); +} + +void test_long_name_exact_24_fits(void) +{ + // 24 chars -> stored as 24 chars + NUL inside char[25]. + const char *exact24 = "abcdefghijklmnopqrstuvwx"; + TEST_ASSERT_EQUAL_INT(24, (int)strlen(exact24)); + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser(exact24, "ex"); + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_EQUAL_STRING(exact24, lite.long_name); + TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name)); +} + +void test_long_name_truncates_when_too_long(void) +{ + // 33 chars in, must fit in 24 + NUL. + const char *tooLong = "North-County Search & Rescue Base"; + TEST_ASSERT_EQUAL_INT(33, (int)strlen(tooLong)); + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser(tooLong, "nc"); + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name)); + TEST_ASSERT_EQUAL_STRING_LEN(tooLong, lite.long_name, 24); + TEST_ASSERT_EQUAL_INT('\0', lite.long_name[24]); +} + +void test_long_name_round_trip_to_wire(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("Mountain Repeater Site E", "mr"); // exactly 24 + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_EQUAL_STRING(in.long_name, out.long_name); +} + +void test_long_name_truncated_utf8_boundary_sanitized(void) +{ + // Suffix the 24th byte with the start of a 4-byte emoji; truncation should + // leave the dangling bytes for sanitizeUtf8 to replace with '?'. + char input[40] = {0}; + memset(input, 'a', 22); // 22 ASCII + input[22] = static_cast(0xF0); // emoji lead byte at position 22 + input[23] = static_cast(0x9F); // continuation + input[24] = static_cast(0xA4); // continuation - past the cap + input[25] = static_cast(0x96); // continuation - past the cap + input[26] = '\0'; + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser(input, "u8"); + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + // 24 bytes survive plus NUL. sanitizeUtf8 should have turned the dangling + // multi-byte head into '?' since its continuation bytes were chopped. + TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name)); + for (int i = 0; i < 22; ++i) + TEST_ASSERT_EQUAL_INT('a', lite.long_name[i]); + // The 4-byte sequence got truncated mid-codepoint; sanitizeUtf8 replaces + // any invalid lead/continuation byte with '?'. + TEST_ASSERT_EQUAL_INT('?', lite.long_name[22]); + TEST_ASSERT_EQUAL_INT('?', lite.long_name[23]); +} + +// ---------- short_name truncation -------------------------------------------- + +void test_short_name_passes_through(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser("Test", "abcd"); // 4 chars, fits short_name[5] + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_EQUAL_STRING("abcd", lite.short_name); +} + +void test_short_name_truncates_when_too_long(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User u = makeUser("Test", "abcdefgh"); + TypeConversions::CopyUserToNodeInfoLite(&lite, u); + TEST_ASSERT_EQUAL_INT(4, (int)strlen(lite.short_name)); + TEST_ASSERT_EQUAL_INT('\0', lite.short_name[4]); +} + +// ---------- bitfield collapse + round-trip per bool -------------------------- + +void test_bitfield_is_licensed_round_trip(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.is_licensed = true; + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_TRUE(out.is_licensed); + + in.is_licensed = false; + meshtastic_NodeInfoLite lite2 = meshtastic_NodeInfoLite_init_default; + TypeConversions::CopyUserToNodeInfoLite(&lite2, in); + TEST_ASSERT_FALSE((lite2.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0); + meshtastic_User out2 = TypeConversions::ConvertToUser(&lite2); + TEST_ASSERT_FALSE(out2.is_licensed); +} + +void test_bitfield_unmessagable_present_and_true(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.has_is_unmessagable = true; + in.is_unmessagable = true; + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_TRUE(out.has_is_unmessagable); + TEST_ASSERT_TRUE(out.is_unmessagable); +} + +void test_bitfield_unmessagable_present_but_false(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.has_is_unmessagable = true; + in.is_unmessagable = false; + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0); + TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_TRUE(out.has_is_unmessagable); + TEST_ASSERT_FALSE(out.is_unmessagable); +} + +void test_bitfield_unmessagable_absent(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.has_is_unmessagable = false; + in.is_unmessagable = true; // explicitly true to make sure absence still wins + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0); + TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_FALSE(out.has_is_unmessagable); + TEST_ASSERT_FALSE(out.is_unmessagable); +} + +void test_copy_user_preserves_unrelated_bits(void) +{ + // Pre-set is_muted and is_key_manually_verified - CopyUserToNodeInfoLite + // must not stomp them. + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.bitfield = NODEINFO_BITFIELD_IS_MUTED_MASK | NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK | + NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK; + meshtastic_User in = makeUser("a", "a"); + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) != 0); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK) != 0); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) != 0); + TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK) != 0); +} + +void test_bitfield_bits_are_independent(void) +{ + // Set just is_licensed, verify only that bit (and HAS_USER) is set. + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.is_licensed = true; + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + const uint32_t expected = NODEINFO_BITFIELD_HAS_USER_MASK | NODEINFO_BITFIELD_IS_LICENSED_MASK; + TEST_ASSERT_EQUAL_HEX32(expected, lite.bitfield); +} + +// ---------- public_key / hw_model / role pass-through ------------------------ + +void test_public_key_round_trip(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.public_key.size = 32; + for (int i = 0; i < 32; ++i) + in.public_key.bytes[i] = (uint8_t)(i ^ 0xA5); + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_EQUAL_INT(32, lite.public_key.size); + TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, lite.public_key.bytes, 32); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_EQUAL_INT(32, out.public_key.size); + TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, out.public_key.bytes, 32); +} + +void test_hw_model_and_role_round_trip(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + meshtastic_User in = makeUser("a", "a"); + in.hw_model = meshtastic_HardwareModel_HELTEC_V3; + in.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + TypeConversions::CopyUserToNodeInfoLite(&lite, in); + TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, lite.hw_model); + TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, lite.role); + meshtastic_User out = TypeConversions::ConvertToUser(&lite); + TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, out.hw_model); + TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, out.role); +} + +// ---------- ConvertToNodeInfo (3-arg) ---------------------------------------- + +void test_convert_to_node_info_thin_omits_position_and_metrics(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xAA; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfoThin(&lite); + TEST_ASSERT_FALSE(info.has_position); + TEST_ASSERT_FALSE(info.has_device_metrics); +} + +void test_convert_to_node_info_3arg_with_position(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xAA; + meshtastic_PositionLite pos = meshtastic_PositionLite_init_default; + pos.latitude_i = 374200000; + pos.longitude_i = -1221000000; + pos.altitude = 30; + pos.time = 12345; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, &pos, nullptr); + TEST_ASSERT_TRUE(info.has_position); + TEST_ASSERT_FALSE(info.has_device_metrics); + TEST_ASSERT_EQUAL_INT32(374200000, info.position.latitude_i); + TEST_ASSERT_EQUAL_INT32(-1221000000, info.position.longitude_i); + TEST_ASSERT_EQUAL_INT32(30, info.position.altitude); + TEST_ASSERT_EQUAL_UINT32(12345, info.position.time); +} + +void test_convert_to_node_info_3arg_with_metrics(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xAA; + meshtastic_DeviceMetrics dm = meshtastic_DeviceMetrics_init_default; + dm.battery_level = 88; + dm.has_battery_level = true; + dm.voltage = 3.71f; + dm.has_voltage = true; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, &dm); + TEST_ASSERT_FALSE(info.has_position); + TEST_ASSERT_TRUE(info.has_device_metrics); + TEST_ASSERT_EQUAL_INT(88, info.device_metrics.battery_level); + TEST_ASSERT_TRUE(info.device_metrics.has_battery_level); + TEST_ASSERT_EQUAL_FLOAT(3.71f, info.device_metrics.voltage); +} + +void test_convert_to_node_info_3arg_null_inputs(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xAA; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr); + TEST_ASSERT_FALSE(info.has_position); + TEST_ASSERT_FALSE(info.has_device_metrics); +} + +void test_convert_to_node_info_extracts_bitfield_bools(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xBB; + lite.bitfield = NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK | + NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK | NODEINFO_BITFIELD_IS_MUTED_MASK; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr); + TEST_ASSERT_TRUE(info.via_mqtt); + TEST_ASSERT_TRUE(info.is_favorite); + TEST_ASSERT_TRUE(info.is_ignored); + TEST_ASSERT_TRUE(info.is_key_manually_verified); + TEST_ASSERT_TRUE(info.is_muted); +} + +void test_convert_to_node_info_extracts_bitfield_bools_none_set(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0xBB; + lite.bitfield = 0; + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr); + TEST_ASSERT_FALSE(info.via_mqtt); + TEST_ASSERT_FALSE(info.is_favorite); + TEST_ASSERT_FALSE(info.is_ignored); + TEST_ASSERT_FALSE(info.is_key_manually_verified); + TEST_ASSERT_FALSE(info.is_muted); +} + +void test_convert_to_node_info_user_only_when_has_user_bit_set(void) +{ + meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default; + lite.num = 0x01; + strcpy(lite.long_name, "Alpha"); + strcpy(lite.short_name, "A"); + // No HAS_USER bit -> user fields ignored on emit. + meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr); + TEST_ASSERT_FALSE(info.has_user); + + lite.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK; + meshtastic_NodeInfo info2 = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr); + TEST_ASSERT_TRUE(info2.has_user); + TEST_ASSERT_EQUAL_STRING("Alpha", info2.user.long_name); + TEST_ASSERT_EQUAL_STRING("A", info2.user.short_name); + TEST_ASSERT_EQUAL_STRING("!00000001", info2.user.id); +} + +// ---------- entry point ------------------------------------------------------- + +void setup() +{ + delay(10); + delay(2000); + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_copy_user_sets_has_user_bit); + RUN_TEST(test_convert_to_user_id_derived_from_nodenum); + RUN_TEST(test_convert_to_user_zero_fills_macaddr); + RUN_TEST(test_long_name_short_passes_through); + RUN_TEST(test_long_name_exact_24_fits); + RUN_TEST(test_long_name_truncates_when_too_long); + RUN_TEST(test_long_name_round_trip_to_wire); + RUN_TEST(test_long_name_truncated_utf8_boundary_sanitized); + RUN_TEST(test_short_name_passes_through); + RUN_TEST(test_short_name_truncates_when_too_long); + RUN_TEST(test_bitfield_is_licensed_round_trip); + RUN_TEST(test_bitfield_unmessagable_present_and_true); + RUN_TEST(test_bitfield_unmessagable_present_but_false); + RUN_TEST(test_bitfield_unmessagable_absent); + RUN_TEST(test_copy_user_preserves_unrelated_bits); + RUN_TEST(test_bitfield_bits_are_independent); + RUN_TEST(test_public_key_round_trip); + RUN_TEST(test_hw_model_and_role_round_trip); + RUN_TEST(test_convert_to_node_info_thin_omits_position_and_metrics); + RUN_TEST(test_convert_to_node_info_3arg_with_position); + RUN_TEST(test_convert_to_node_info_3arg_with_metrics); + RUN_TEST(test_convert_to_node_info_3arg_null_inputs); + RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools); + RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools_none_set); + RUN_TEST(test_convert_to_node_info_user_only_when_has_user_bit_set); + exit(UNITY_END()); +} + +void loop() {}