diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fe9af4359..c63e24018 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -135,6 +135,95 @@ On top of authorization, any remote admin message that **mutates** state (not a - **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast. - **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo. +## NodeDB Layout (v25) + +`DEVICESTATE_CUR_VER = 25`, `DEVICESTATE_MIN_VER = 24`. The on-device NodeDB was split in v25 into a slim header table plus four optional satellite stores. Older v24 saves auto-migrate at boot. Old training-data instincts (`node->user.long_name`, `node->position.latitude_i`, `node->is_favorite`, `node->device_metrics.battery_level`) are wrong now — the fields aren't there. Read this section before touching anything that walks `nodeDB->meshNodes`. + +### Slim `NodeInfoLite` + +`UserLite` is flattened onto `NodeInfoLite` (no nested sub-message); `position` and `device_metrics` are removed entirely (tags reserved). MAC address is dropped. Long names are capped at 25 chars (`max_size:25` in `deviceonly.options`); `hw_model` and `role` are `int_size:8`. Encoded size dropped from ~166 B → ~105 B per node. + +Booleans are bit-packed into `NodeInfoLite.bitfield`. **Do not read or write the bits directly** — use the inline helpers in `src/mesh/NodeDB.h`: + +```cpp +nodeInfoLiteHasUser(n) // bit 5 — user fields populated +nodeInfoLiteIsFavorite(n) // bit 3 +nodeInfoLiteIsIgnored(n) // bit 4 +nodeInfoLiteIsMuted(n) // bit 1 +nodeInfoLiteIsLicensed(n) // bit 6 — Ham mode peer +nodeInfoLiteIsKeyManuallyVerified(n) // bit 0 +nodeInfoLiteHasIsUnmessagable(n) // bit 8 — "is_unmessagable was sent" +nodeInfoLiteIsUnmessagable(n) // bit 7 +// via_mqtt is bit 2 (mask exposed; predicate uses the mask directly) + +nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); // setter +``` + +### Satellite stores + +Four `std::unordered_map` members on `NodeDB`, each gated by its own build flag: + +| Map | Value type | Build flag | +| ----------------- | ------------------------------- | ---------------------------------- | +| `nodePositions` | `meshtastic_PositionLite` | `MESHTASTIC_EXCLUDE_POSITIONDB` | +| `nodeTelemetry` | `meshtastic_DeviceMetrics` | `MESHTASTIC_EXCLUDE_TELEMETRYDB` | +| `nodeEnvironment` | `meshtastic_EnvironmentMetrics` | `MESHTASTIC_EXCLUDE_ENVIRONMENTDB` | +| `nodeStatus` | `meshtastic_StatusMessage` | `MESHTASTIC_EXCLUDE_STATUSDB` | + +Defaults are ON (i.e., maps **excluded**) for STM32WL only — see `src/mesh/mesh-pb-constants.h`. On every other arch all four maps are present. When excluded, the map member is absent and the corresponding accessors return `false`. + +All four maps are guarded by **`mutable concurrency::Lock satelliteMutex`** — concurrent access from receive threads, the phone API state machine, and the renderer is the rule, not the exception. + +### Accessor convention + +**Never hand out pointers into the maps.** Use the copy-out accessors on `NodeDB`: + +```cpp +bool copyNodePosition(NodeNum, meshtastic_PositionLite &out) const; +bool copyNodeTelemetry(NodeNum, meshtastic_DeviceMetrics &out) const; +bool copyNodeEnvironment(NodeNum, meshtastic_EnvironmentMetrics &out) const; +bool copyNodeStatus(NodeNum, meshtastic_StatusMessage &out) const; +``` + +Each takes the lock, copies the value if present, returns `false` if the entry is absent or the DB is excluded. Pass-by-out-param is deliberate — pointer-style accessors would invite UAF and lock-leak bugs across the renderer. The "has any X" convenience predicates (`hasValidPosition` etc.) are implemented in terms of these. + +Writers go through `setNodeStatus`, `updatePosition`, `updateTelemetry` (which dispatches on `which_variant` for device vs environment metrics) — these own the lock and the eviction hooks. + +### Eviction + +Every code path that drops a node from the header table must also evict the satellites. The single chokepoint is `eraseNodeSatellites(NodeNum)`; it's already called from `getOrCreateMeshNode`'s oldest-boring eviction, `removeNodeByNum`, both branches of `resetNodes`, `cleanupMeshDB`, `addFromContact`'s ignored-branch, and `AdminModule`'s `set_ignored_node`. Add new eviction sites here, not by calling `.erase()` directly. + +### Gradient sync (opt-in via special nonces) + +`client_capabilities` is **not** a thing in this branch. Phone clients opt into the new sync flow by sending one of two values in the `ToRadio.want_config_id`: + +- `SPECIAL_NONCE_GRADIENT_SYNC` (69422) — full config + thin NodeInfo + replay phases. +- `SPECIAL_NONCE_GRADIENT_ONLY_NODES` (69423) — skip config segments, NodeInfo + replay only. + +`PhoneAPI::clientWantsGradientSync()` is the single switch. When true, `STATE_SEND_OTHER_NODEINFOS` is followed by: + +```text +STATE_REPLAY_POSITIONS → STATE_REPLAY_TELEMETRY → STATE_REPLAY_ENVIRONMENT → STATE_REPLAY_STATUS +``` + +Each replay phase walks the corresponding satellite map and emits synthetic `MeshPacket`s on the matching portnum (`POSITION_APP`, `TELEMETRY_APP` for both device + environment variants, `STATUS_MESSAGE_APP`). Legacy clients (no special nonce) get the bundled-NodeInfo path with position/device_metrics joined back in by `ConvertToNodeInfo(lite, pos*, dm*)` — wire bytes are byte-identical to pre-v25 for them. + +`ConvertToNodeInfoThin(lite)` is the gradient-sync emitter (no position/telemetry). + +### v24 → v25 migration + +The legacy migration code lives in **`src/mesh/NodeDBLegacyMigration.cpp`**, not in `NodeDB.cpp`. It owns the `meshtastic_NodeDatabase_Legacy` callback and `NodeDB::migrateLegacyNodeDatabase()`. The legacy proto descriptor is `protobufs/meshtastic/deviceonly_legacy.proto` (only included by the migration TU). The boot path peeks the file's leading version tag, runs the migration if `version < 25`, then re-saves in v25 layout. The legacy descriptor is scheduled for removal once `DEVICESTATE_MIN_VER` is bumped. + +### Read-site rules of thumb + +- Never `node->position.X` / `node->device_metrics.X` — those fields no longer exist. Pull from the satellite map via `copyNodePosition` / `copyNodeTelemetry`. +- Never `node->user.long_name` — `long_name`, `short_name`, `public_key`, `hw_model`, `role`, `macaddr` (gone), `is_licensed`, `is_unmessagable` are flat on `NodeInfoLite`. +- Never `node->is_favorite` / `node->is_ignored` / `node->via_mqtt` / `node->is_key_manually_verified` — use the bitfield helpers. +- Never assume `nodeDB->getMeshNode(num)->position.time` — call `copyNodePosition` and check the return. +- Don't lock `satelliteMutex` yourself in renderer code; the copy-out accessors already do. + +Unit tests for the conversion layer live in `test/test_type_conversions/test_main.cpp` (Unity) — bitfield round-trips, `long_name` truncation, thin-vs-full conversions. Add cases there when extending the schema. + ## Project Structure ``` diff --git a/protobufs b/protobufs index 1d6f1a71f..149586802 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 1d6f1a71ff329fa52ad8bb7899951e96f8280a1f +Subproject commit 149586802fa59233783d536132d8cf2fa8057193 diff --git a/src/GPSStatus.h b/src/GPSStatus.h index a1a9f2c56..25c7b1039 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -53,8 +53,7 @@ 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; + return localPosition.latitude_i; } else { return p.latitude_i; } @@ -63,8 +62,7 @@ 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; + return localPosition.longitude_i; } else { return p.longitude_i; } @@ -73,8 +71,7 @@ class GPSStatus : public Status int32_t getAltitude() const { if (config.position.fixed_position) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); - return node->position.altitude; + return localPosition.altitude; } 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..382b89f2f 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -845,10 +845,10 @@ void menuHandler::messageViewModeMenu() // Encode peers for (size_t i = 0; i < uniquePeers.size(); ++i) { uint32_t peer = uniquePeers[i]; - auto node = nodeDB->getMeshNode(peer); + const 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,14 +1355,14 @@ 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"; } optionsEnumArray[options++] = Favorite; - bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + bool isMuted = nodeInfoLiteIsMuted(node); if (isMuted) { optionsArray[options] = "Unmute Notifications"; } else { @@ -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 { @@ -1426,13 +1426,9 @@ void menuHandler::manageNodeMenu() return; } - if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { - n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; - LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); - } else { - n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK; - LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum); - } + const bool wasMuted = nodeInfoLiteIsMuted(n); + nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted); + LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum); nodeDB->notifyObservers(true); nodeDB->saveToDisk(); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); @@ -1461,11 +1457,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); @@ -2079,9 +2075,9 @@ void menuHandler::removeFavoriteMenu() static const char *optionsArray[] = {"Back", "Yes"}; 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); + const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); + 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..d0b027c13 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) { + meshtastic_StatusMessage cachedStatus; + if (nodeDB->copyNodeStatus(node->num, 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; } @@ -259,7 +253,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int #if GRAPHICS_TFT_COLORING_ENABLED applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); #endif - bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + bool isMuted = nodeInfoLiteIsMuted(node); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -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 { @@ -321,20 +315,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int #if GRAPHICS_TFT_COLORING_ENABLED applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); #endif - bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + bool isMuted = nodeInfoLiteIsMuted(node); 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 { @@ -401,15 +395,19 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 #if GRAPHICS_TFT_COLORING_ENABLED applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); #endif - bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + bool isMuted = nodeInfoLiteIsMuted(node); 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_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + meshtastic_PositionLite ourPos; + meshtastic_PositionLite theirPos; + const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos); + const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveOurPos && haveTheirPos) { + 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 +453,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 { @@ -509,19 +507,19 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 #if GRAPHICS_TFT_COLORING_ENABLED applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); #endif - bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; + bool isMuted = nodeInfoLiteIsMuted(node); 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 +540,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; + meshtastic_PositionLite nodePos; + if (!nodeDB->copyNodePosition(node->num, 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 +653,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); @@ -883,14 +884,15 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { float headingRadian = 0.0f; - auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { + const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + meshtastic_PositionLite ourSelfPos; + if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !nodeDB->copyNodePosition(ourNode->num, 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..cfde10124 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) { + meshtastic_StatusMessage cachedStatus; + if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) { + drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus.status); } } #endif @@ -971,25 +962,30 @@ 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) { + meshtastic_DeviceMetrics nodeMetrics; + const bool haveNodeMetrics = nodeDB->copyNodeTelemetry(node->num, nodeMetrics); + if (haveNodeMetrics && 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); } // === 5. Distance (only if both nodes have GPS position) === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); char distStr[24] = ""; // Make buffer big enough for any string bool haveDistance = false; - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + meshtastic_PositionLite nodePos; + meshtastic_PositionLite ourPos; + const bool haveNodePos = nodeDB->copyNodePosition(node->num, nodePos); + const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveNodePos && haveOurPos) { // 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 +1020,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 (haveNodeMetrics) { + 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 +1072,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 && haveOurPos && haveNodePos) { + 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 { @@ -1130,7 +1126,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); int line = 1; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // === Header === if (currentResolution == ScreenResolution::UltraLow) { @@ -1270,7 +1266,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]) { @@ -1597,7 +1593,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); const bool hasLiveGpsFix = (gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0)); @@ -1613,9 +1609,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); + meshtastic_PositionLite ownPos; + if (nodeDB->copyNodePosition(ourNode->num, 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..881371e2d 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -136,9 +136,10 @@ void InkHUD::MapApplet::onRender(bool full) printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); // 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_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + meshtastic_PositionLite ourSelfPos; + if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, 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); @@ -168,10 +169,11 @@ void InkHUD::MapApplet::onRender(bool full) 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_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + meshtastic_PositionLite ourSelfPos; + if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, 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; + meshtastic_PositionLite pos; + if (!nodeDB->copyNodePosition(node->num, 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; + meshtastic_PositionLite pos; + if (!nodeDB->copyNodePosition(node->num, 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,13 @@ 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 + meshtastic_PositionLite pos; + const bool hasPos = nodeDB->copyNodePosition(node->num, pos); + assert(hasPos); + 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 +529,16 @@ void InkHUD::MapApplet::calculateAllMarkers() if (node->num == nodeDB->getNodeNum()) continue; + meshtastic_PositionLite pos; + if (!nodeDB->copyNodePosition(node->num, 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..3da912a78 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -50,21 +50,23 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); // Assemble info: from nodeDB (needed to detect changes) - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (node) { if (node->has_hops_away) 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; + meshtastic_PositionLite ourPos; + meshtastic_PositionLite theirPos; + if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, 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..79ac1e701 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++; } @@ -2098,10 +2098,10 @@ void InkHUD::MenuApplet::populateRecipientPage() // Don't want some monstrous list that takes 100 clicks to reach exit if (favoriteCount < 20) { for (uint32_t i = 0; i < nodeCount; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + const 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..94a87d23d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -80,8 +80,8 @@ void InkHUD::HeardApplet::populateFromNodeDB() ordered.resize(maxCards()); // Create card info for these (stale) node observations - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - for (meshtastic_NodeInfoLite *node : ordered) { + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + for (const meshtastic_NodeInfoLite *node : ordered) { CardInfo c; c.nodeNum = node->num; @@ -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; + meshtastic_PositionLite ourPos; + meshtastic_PositionLite theirPos; + if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, 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) @@ -122,4 +124,4 @@ std::string InkHUD::HeardApplet::getHeaderText() return text; } -#endif \ No newline at end of file +#endif 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..151831b96 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,16 @@ 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. + nodeDB->touchNodePositionTime(node->num, getValidTime(RTCQualityFromNet)); +#endif if (powerStatus->getHasBattery() == 1) { updateBatteryLevel(powerStatus->getBatteryChargePercent()); @@ -432,7 +430,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); + meshtastic_PositionLite fixedSlot; + if (nodeDB->copyNodePosition(node->num, fixedSlot)) + pos = TypeConversions::ConvertToPosition(fixedSlot); } // Add a fresh timestamp diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ef07f68fd..46aa61b28 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 @@ -151,24 +152,103 @@ 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) +bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field) { - if (ostream) { - std::vector const *vec = (std::vector *)field->pData; - for (auto item : *vec) { - if (!pb_encode_tag_for_field(ostream, field)) - return false; - pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); + const auto *iter = reinterpret_cast(field); + switch (iter->tag) { + case meshtastic_NodeDatabase_nodes_tag: { + if (ostream) { + const auto *vec = static_cast *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item)) + return false; + } } + if (istream) { + meshtastic_NodeInfoLite node; + auto *vec = static_cast *>(iter->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) + vec->push_back(node); + } + return true; } - 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)) - vec->push_back(node); + case meshtastic_NodeDatabase_positions_tag: { + if (ostream) { + const auto *vec = static_cast *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodePositionEntry entry; + auto *vec = static_cast *>(iter->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 *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeTelemetryEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeTelemetryEntry entry; + auto *vec = static_cast *>(iter->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 *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeStatusEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeStatusEntry entry; + auto *vec = static_cast *>(iter->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 *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeEnvironmentEntry_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeEnvironmentEntry entry; + auto *vec = static_cast *>(iter->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry)) + vec->push_back(entry); + } + return true; + } + default: + return true; } - return true; } /** The current change # for radio settings. Starts at 0 on boot and any time the radio settings @@ -298,8 +378,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 +521,12 @@ 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 + { + concurrency::LockGuard guard(&satelliteMutex); + nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS); + } +#endif nodeDB->setLocalPosition(fixedGPS); config.position.fixed_position = true; #endif @@ -479,6 +562,29 @@ bool isBroadcast(uint32_t dest) return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; } +namespace +{ +template bool copySatelliteEntry(const Map &map, NodeNum n, Value &out) +{ + auto it = map.find(n); + if (it == map.end()) + return false; + out = it->second; + return true; +} + +template std::vector snapshotSatelliteNodeNums(const Map &map, NodeNum exclude) +{ + std::vector result; + result.reserve(map.size()); + for (const auto &kv : map) { + if (kv.first != exclude) + result.push_back(kv.first); + } + return result; +} +} // namespace + void NodeDB::resetRadioConfig(bool is_fresh_install) { if (is_fresh_install) { @@ -548,6 +654,19 @@ void NodeDB::installDefaultNodeDatabase() nodeDatabase.nodes = std::vector(MAX_NUM_NODES); numMeshNodes = 0; meshNodes = &nodeDatabase.nodes; + concurrency::LockGuard satelliteGuard(&satelliteMutex); +#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 +1138,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 && !nodeInfoLiteIsFavorite(&node)) { + eraseNodeSatellites(node.num); node = meshtastic_NodeInfoLite(); } else { numMeshNodes += 1; @@ -1032,8 +1153,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 +1181,173 @@ 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 + concurrency::LockGuard guard(&satelliteMutex); + nodePositions.erase(getNodeNum()); +#endif setLocalPosition(meshtastic_Position_init_default); localPositionUpdatedSinceBoot = false; } +bool NodeDB::copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + (void)n; + (void)out; + return false; +#else + concurrency::LockGuard guard(&satelliteMutex); + return copySatelliteEntry(nodePositions, n, out); +#endif +} + +bool NodeDB::copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const +{ +#if MESHTASTIC_EXCLUDE_TELEMETRYDB + (void)n; + (void)out; + return false; +#else + concurrency::LockGuard guard(&satelliteMutex); + return copySatelliteEntry(nodeTelemetry, n, out); +#endif +} + +bool NodeDB::copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const +{ +#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB + (void)n; + (void)out; + return false; +#else + concurrency::LockGuard guard(&satelliteMutex); + return copySatelliteEntry(nodeEnvironment, n, out); +#endif +} + +bool NodeDB::copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + (void)n; + (void)out; + return false; +#else + concurrency::LockGuard guard(&satelliteMutex); + return copySatelliteEntry(nodeStatus, n, out); +#endif +} + +std::vector NodeDB::snapshotPositionNodeNums(NodeNum exclude) const +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + (void)exclude; + return {}; +#else + concurrency::LockGuard guard(&satelliteMutex); + return snapshotSatelliteNodeNums(nodePositions, exclude); +#endif +} + +std::vector NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const +{ +#if MESHTASTIC_EXCLUDE_TELEMETRYDB + (void)exclude; + return {}; +#else + concurrency::LockGuard guard(&satelliteMutex); + return snapshotSatelliteNodeNums(nodeTelemetry, exclude); +#endif +} + +std::vector NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const +{ +#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB + (void)exclude; + return {}; +#else + concurrency::LockGuard guard(&satelliteMutex); + return snapshotSatelliteNodeNums(nodeEnvironment, exclude); +#endif +} + +std::vector NodeDB::snapshotStatusNodeNums(NodeNum exclude) const +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + (void)exclude; + return {}; +#else + concurrency::LockGuard guard(&satelliteMutex); + return snapshotSatelliteNodeNums(nodeStatus, exclude); +#endif +} + +void NodeDB::setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status) +{ +#if MESHTASTIC_EXCLUDE_STATUSDB + (void)n; + (void)status; +#else + concurrency::LockGuard guard(&satelliteMutex); + nodeStatus[n] = status; +#endif +} + +void NodeDB::touchNodePositionTime(NodeNum n, uint32_t time) +{ +#if MESHTASTIC_EXCLUDE_POSITIONDB + (void)n; + (void)time; +#else + concurrency::LockGuard guard(&satelliteMutex); + nodePositions[n].time = time; +#endif +} + +void NodeDB::eraseNodeSatellites(NodeNum n) +{ + concurrency::LockGuard guard(&satelliteMutex); +#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 (nodeInfoLiteHasUser(&n)) { + 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 +1405,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 !nodeInfoLiteHasUser(n); + }; + 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); @@ -1169,7 +1441,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t if (f) { LOG_INFO("Load %s", filename); pb_istream_t stream = {&readcb, &f, protoSize}; - if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object + if (fields != &meshtastic_NodeDatabase_msg && + fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members memset(dest_struct, 0, objSize); if (!pb_decode(&stream, fields, dest_struct)) { LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); @@ -1242,9 +1515,57 @@ 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) { + if (migrateLegacyNodeDatabase()) + saveNodeDatabaseToDisk(); + else + 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. + concurrency::LockGuard guard(&satelliteMutex); +#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 +1593,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 (nodeInfoLiteHasUser(us)) { 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 = nodeInfoLiteIsLicensed(us); + owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us); + owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us); // Save the recovered owner to device state on disk saveToDisk(SEGMENT_DEVICESTATE); @@ -1528,9 +1849,75 @@ 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. + concurrency::LockGuard guard(&satelliteMutex); +#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 +2086,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 && nodeInfoLiteViaMqtt(&meshNodes->at(i))) continue; if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) numseen++; @@ -1736,62 +2123,94 @@ 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) { - // 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, + 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); - info->position = 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; - } 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 - // recorded based on the packet rxTime - // - // FIXME perhaps handle RX_SRC_USER separately? - 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; - - // Next, update atomically - info->position = TypeConversions::ConvertToPositionLite(p); - - // Last, restore any fields that may have been overwritten - if (!info->position.time) - info->position.time = tmp_time; } - info->has_position = true; + (void)nodeId; + updateGUIforNode = info; + notifyObservers(true); +#else + { + concurrency::LockGuard guard(&satelliteMutex); + 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); + 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); + 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 + // recorded based on the packet rxTime + // + // FIXME perhaps handle RX_SRC_USER separately? + 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 = slot.time; + + // Next, update atomically + slot = TypeConversions::ConvertToPositionLite(p); + + // Last, restore any fields that may have been overwritten + if (!slot.time) + slot.time = tmp_time; + } + } 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) { + if (src == RX_SRC_LOCAL) { + LOG_DEBUG("updateTelemetry LOCAL device"); + } else { + LOG_DEBUG("updateTelemetry REMOTE device node=0x%x", nodeId); + } +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + concurrency::LockGuard guard(&satelliteMutex); + nodeTelemetry[nodeId] = t.variant.device_metrics; +#endif + } else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) { + if (src == RX_SRC_LOCAL) { + LOG_DEBUG("updateTelemetry LOCAL env"); + } else { + LOG_DEBUG("updateTelemetry REMOTE env node=0x%x", nodeId); + } +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB + concurrency::LockGuard guard(&satelliteMutex); + 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); } /** @@ -1806,24 +2225,22 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) // If the local node has this node marked as manually verified // 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 (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) { + 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,12 +2259,12 @@ 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 if (contact.manually_verified) { - info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK, true); } // Mark the node's key as manually verified to indicate trustworthiness. updateGUIforNode = info; @@ -1887,9 +2304,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 +2319,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 +2376,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 +2392,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(); } @@ -1986,10 +2407,10 @@ bool NodeDB::isFavorite(uint32_t nodeId) if (nodeId == NODENUM_BROADCAST) return false; - meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); if (lite) { - return lite->is_favorite; + return nodeInfoLiteIsFavorite(lite); } return false; } @@ -2014,14 +2435,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 +2478,10 @@ 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 (nodeInfoLiteIsFavorite(&meshNodes->at(i)) && !nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) { 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 (!nodeInfoLiteIsFavorite(&meshNodes->at(i)) && nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) { // noop } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { std::swap(meshNodes->at(i), meshNodes->at(i - 1)); @@ -2120,17 +2541,18 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n) int oldestIndex = -1; int oldestBoringIndex = -1; for (int i = 1; i < numMeshNodes; i++) { + const meshtastic_NodeInfoLite *cand = &meshNodes->at(i); + const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand); + const bool isIgnored = nodeInfoLiteIsIgnored(cand); + const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand); // 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) { - oldest = meshNodes->at(i).last_heard; + if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) { + oldest = cand->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 && - meshNodes->at(i).last_heard < oldestBoring) { - oldestBoring = meshNodes->at(i).last_heard; + if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) { + oldestBoring = cand->last_heard; oldestBoringIndex = i; } } @@ -2140,6 +2562,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,18 +2586,23 @@ 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; + if (n->num == getNodeNum()) { + return localPosition.latitude_i != 0 || localPosition.longitude_i != 0; + } + meshtastic_PositionLite pos; + return copyNodePosition(n->num, pos) && (pos.latitude_i != 0 || pos.longitude_i != 0); } /// If we have a node / user and they report is_licensed = true /// we consider them licensed UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) { - meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); - if (!info || !info->has_user) { + const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); + if (!nodeInfoLiteHasUser(info)) return UserLicenseStatus::NotKnown; - } - return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; + return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } #if !defined(MESHTASTIC_EXCLUDE_PKI) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index f6be963c1..090f3bb9a 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -6,10 +6,12 @@ #include #include #include +#include #include #include "MeshTypes.h" #include "NodeStatus.h" +#include "concurrency/Lock.h" #include "configuration.h" #include "mesh-pb-constants.h" #include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode @@ -82,7 +84,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 +170,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 +296,43 @@ class NodeDB virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); size_t getNumMeshNodes() { return numMeshNodes; } + // Thread-safe satellite-map accessors. Return false if absent or the + // corresponding DB is compiled out. + bool copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const; + bool copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const; + bool copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const; + bool copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const; + std::vector snapshotPositionNodeNums(NodeNum exclude) const; + std::vector snapshotTelemetryNodeNums(NodeNum exclude) const; + std::vector snapshotEnvironmentNodeNums(NodeNum exclude) const; + std::vector snapshotStatusNodeNums(NodeNum exclude) const; + + void setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status); + void touchNodePositionTime(NodeNum n, uint32_t time); + + bool hasNodePosition(NodeNum n) const + { + meshtastic_PositionLite scratch; + return copyNodePosition(n, scratch); + } + bool hasNodeTelemetry(NodeNum n) const + { + meshtastic_DeviceMetrics scratch; + return copyNodeTelemetry(n, scratch); + } + bool hasNodeEnvironment(NodeNum n) const + { + meshtastic_EnvironmentMetrics scratch; + return copyNodeEnvironment(n, scratch); + } + bool hasNodeStatus(NodeNum n) const + { + meshtastic_StatusMessage scratch; + return copyNodeStatus(n, scratch); + } + + void eraseNodeSatellites(NodeNum n); + UserLicenseStatus getLicenseStatus(uint32_t nodeNum); size_t getMaxNodesAllocatedSize() @@ -285,7 +341,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 @@ -329,6 +389,7 @@ class NodeDB } private: + mutable concurrency::Lock satelliteMutex; bool duplicateWarned = false; bool localPositionUpdatedSinceBoot = false; uint32_t lastNodeDbSave = 0; // when we last saved our db to flash @@ -363,6 +424,11 @@ class NodeDB bool saveDeviceStateToDisk(); bool saveNodeDatabaseToDisk(); void sortMeshDB(); + + // Defined in NodeDBLegacyMigration.cpp. Decodes /prefs/nodes.proto via + // the legacy descriptor and copies entries into the v25 layout. Caller + // is responsible for save / install-default on the result. + bool migrateLegacyNodeDatabase(); }; extern NodeDB *nodeDB; @@ -397,10 +463,74 @@ 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 bool nodeInfoLiteIsMuted(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK); +} +inline bool nodeInfoLiteIsKeyManuallyVerified(const meshtastic_NodeInfoLite *n) +{ + return n && (n->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_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/NodeDBLegacyMigration.cpp b/src/mesh/NodeDBLegacyMigration.cpp new file mode 100644 index 000000000..18d6daa4d --- /dev/null +++ b/src/mesh/NodeDBLegacyMigration.cpp @@ -0,0 +1,129 @@ +// Migration from the legacy (pre-v25) NodeDatabase shape to the slim header + +// satellite maps layout. Lives in its own translation unit so the bulk +// fixed-shape decode + field-by-field copy doesn't clutter NodeDB.cpp. +// +// Caller (NodeDB::loadFromDisk) decides what to do with the result: +// - true -> persist via saveNodeDatabaseToDisk() +// - false -> reset via installDefaultNodeDatabase() +// +// This file (and the deviceonly_legacy proto) can be removed once +// DEVICESTATE_MIN_VER advances past 24. + +#include "NodeDB.h" +#include "concurrency/LockGuard.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h" + +#include +#include +#include +#include + +bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field) +{ + const auto *iter = reinterpret_cast(field); + if (ostream) { + const auto *vec = static_cast *>(iter->pData); + for (auto item : *vec) { + if (!pb_encode_tag_for_field(ostream, iter)) + return false; + if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_Legacy_fields, &item)) + return false; + } + } + if (istream) { + meshtastic_NodeInfoLite_Legacy node; + auto *vec = static_cast *>(iter->pData); + if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_Legacy_fields, &node)) + vec->push_back(node); + } + return true; +} + +bool NodeDB::migrateLegacyNodeDatabase() +{ + 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) { + LOG_ERROR("Failed to load NodeDatabase via legacy descriptor; installing default"); + return false; + } + + nodeDatabase.nodes.clear(); + const size_t maxToMigrate = std::min(legacyDb.nodes.size(), MAX_NUM_NODES); + nodeDatabase.nodes.reserve(maxToMigrate); + size_t posCount = 0, telCount = 0; + { + concurrency::LockGuard guard(&satelliteMutex); + 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 + } +#if !MESHTASTIC_EXCLUDE_POSITIONDB + posCount = nodePositions.size(); +#endif +#if !MESHTASTIC_EXCLUDE_TELEMETRYDB + telCount = nodeTelemetry.size(); +#endif + } + nodeDatabase.version = DEVICESTATE_CUR_VER; + meshNodes = &nodeDatabase.nodes; + numMeshNodes = nodeDatabase.nodes.size(); + LOG_INFO("Migrated %u nodes from legacy -> v%u (positions: %u, telemetry: %u)", (unsigned)numMeshNodes, DEVICESTATE_CUR_VER, + (unsigned)posCount, (unsigned)telCount); + return true; +} diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 5c3ab486a..ffe413056 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,257 @@ 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 = nodeDB->snapshotPositionNodeNums(nodeDB->getNodeNum()); + 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++]; + meshtastic_PositionLite pos; + if (!nodeDB->copyNodePosition(num, 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 = nodeDB->snapshotTelemetryNodeNums(nodeDB->getNodeNum()); + 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++]; + meshtastic_DeviceMetrics dm; + if (!nodeDB->copyNodeTelemetry(num, 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 = nodeDB->snapshotEnvironmentNodeNums(nodeDB->getNodeNum()); + 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++]; + meshtastic_EnvironmentMetrics env; + if (!nodeDB->copyNodeEnvironment(num, 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 = nodeDB->snapshotStatusNodeNums(nodeDB->getNodeNum()); + 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++]; + meshtastic_StatusMessage status; + if (!nodeDB->copyNodeStatus(num, 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 +1115,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..fa2417c46 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 && nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "???"; return sender; } @@ -121,4 +121,4 @@ template class ProtobufModule : protected SinglePortModule return alterReceivedProtobuf(mp, decoded); } } -}; \ No newline at end of file +}; 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 197566899..980ceb6df 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..254af6132 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,56 @@ 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 = nodeInfoLiteViaMqtt(lite); + info.is_favorite = nodeInfoLiteIsFavorite(lite); + info.is_ignored = nodeInfoLiteIsIgnored(lite); + info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite); + info.is_muted = nodeInfoLiteIsMuted(lite); 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 (nodeInfoLiteHasUser(lite)) { 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) +{ + meshtastic_PositionLite posScratch; + meshtastic_DeviceMetrics dmScratch; + const meshtastic_PositionLite *pos = (nodeDB && nodeDB->copyNodePosition(lite->num, posScratch)) ? &posScratch : nullptr; + const meshtastic_DeviceMetrics *dm = (nodeDB && nodeDB->copyNodeTelemetry(lite->num, dmScratch)) ? &dmScratch : 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 +93,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.has_is_unmessagable && 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 = nodeInfoLiteIsLicensed(lite); + 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 = nodeInfoLiteHasIsUnmessagable(lite); + user.is_unmessagable = nodeInfoLiteIsUnmessagable(lite); + // 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..916951419 --- /dev/null +++ b/src/mesh/generated/meshtastic/deviceonly_legacy.pb.h @@ -0,0 +1,124 @@ +/* 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. + This preserves the original NodeInfoLite-compatible field numbers needed + to parse old wire bytes cleanly, including user (2), position (3), + device_metrics (6), and the legacy-only compatibility fields via_mqtt (8), + is_favorite (10), and is_ignored (11). 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..57bbfe61b 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); + meshtastic_PositionLite posLite; + if (nodeDB->copyNodePosition(tempNodeInfo->num, 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)); } @@ -930,4 +930,4 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) res->print(jsonString.c_str()); delete value; } -#endif \ No newline at end of file +#endif 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..396917693 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; @@ -437,7 +436,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta LOG_INFO("Client received toggle_muted_node command"); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); if (node != NULL) { - node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); + nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_MUTED_MASK, !nodeInfoLiteIsMuted(node)); saveChanges(SEGMENT_NODEDATABASE, false); } break; @@ -445,9 +444,11 @@ 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); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + // 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/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 16ccdd744..1a0b69930 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -372,7 +372,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP // If we receive a direct message and the receipent is us, apply DM mute setting // Else we just handle it as not muted. const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); - bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) + bool is_muted = isDmToUs ? nodeInfoLiteIsMuted(sender) : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); const bool buzzerModeIsDirectOnly = diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 6d0255d53..67317ac9b 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -10,6 +10,18 @@ KeyVerificationModule *keyVerificationModule; +namespace +{ +void copyNodeLongNameOrUnknown(char *dest, size_t destSize, const meshtastic_NodeInfoLite *node) +{ + if (!dest || destSize == 0) + return; + const char *name = (node && nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : "Unknown"; + strncpy(dest, name, destSize - 1); + dest[destSize - 1] = '\0'; +} +} // namespace + KeyVerificationModule::KeyVerificationModule() : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { @@ -37,7 +49,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && request->key_verification.nonce == currentNonce) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + if (remoteNodePtr) + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; resetToIdle(); } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { resetToIdle(); @@ -72,9 +85,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & sprintf(cn->message, "Enter Security Number for Key Verification"); 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, - sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_request.remote_longname, + sizeof(cn->payload_variant.key_verification_number_request.remote_longname), + nodeDB->getMeshNode(currentRemoteNode)); service->sendClientNotification(cn); LOG_INFO("Received hash2"); currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; @@ -95,7 +108,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & [=](int selected) { if (selected == 1) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); - remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + if (remoteNodePtr) + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; } }; screen->showOverlayBanner(options);) @@ -104,9 +118,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); 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, - sizeof(cn->payload_variant.key_verification_final.remote_longname)); + copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname, + sizeof(cn->payload_variant.key_verification_final.remote_longname), + nodeDB->getMeshNode(currentRemoteNode)); cn->payload_variant.key_verification_final.isSender = false; service->sendClientNotification(cn); @@ -202,9 +216,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply() currentSecurityNumber % 1000); 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, - sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_inform.remote_longname, + sizeof(cn->payload_variant.key_verification_number_inform.remote_longname), + nodeDB->getMeshNode(currentRemoteNode)); cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; service->sendClientNotification(cn); LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); @@ -219,7 +233,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 (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) { currentState = KEY_VERIFICATION_IDLE; return; // should we throw an error here? } @@ -232,7 +246,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(); @@ -265,9 +279,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); 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, - sizeof(cn->payload_variant.key_verification_final.remote_longname)); + copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname, + sizeof(cn->payload_variant.key_verification_final.remote_longname), + nodeDB->getMeshNode(currentRemoteNode)); cn->payload_variant.key_verification_final.isSender = true; service->sendClientNotification(cn); LOG_INFO(message); @@ -310,4 +324,4 @@ void KeyVerificationModule::generateVerificationCode(char *readableCode) readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. } } -#endif \ No newline at end of file +#endif diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index ac81e9c57..c13904c31 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -183,8 +183,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() return nullptr; } - meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position - assert(node->has_position); + const meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a 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)); + meshtastic_PositionLite cachedSelf; + if (nodeDB->copyNodePosition(node->num, cachedSelf)) + nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(cachedSelf)); } localPosition.seq_number++; @@ -420,7 +421,7 @@ int32_t PositionModule::runOnce() doDeepSleep(nightyNightMs, false, false); } - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (node == nullptr) return RUNONCE_INTERVAL; @@ -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; + meshtastic_PositionLite selfPos; + if (nodeDB->copyNodePosition(node->num, 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); + meshtastic_PositionLite selfPos; + if (!nodeDB->copyNodePosition(node->num, 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; } } } @@ -563,11 +570,14 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic void PositionModule::handleNewPosition() { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); 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); + meshtastic_PositionLite selfPos; + if (!nodeDB->copyNodePosition(node->num, 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..9d3c3ffba 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -268,26 +268,36 @@ 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 + meshtastic_PositionLite senderPos; + const bool haveSenderPos = nodeDB->copyNodePosition(getFrom(&mp), senderPos); + if (haveSenderPos) { + 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 + meshtastic_PositionLite usPos; + if (nodeDB->copyNodePosition(nodeDB->getNodeNum(), 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 (haveSenderPos && 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 { @@ -340,4 +350,4 @@ bool RangeTestModuleRadio::removeFile() return 0; #endif -} \ No newline at end of file +} 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..0266bcec5 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)) { + meshtastic_PositionLite pos; + if (nodeDB->copyNodePosition(tempNodeInfo->num, 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); + } } } } @@ -714,4 +721,4 @@ void SerialModule::processWXSerial() #endif return; } -#endif \ No newline at end of file +#endif 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..2238a8437 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,29 +18,16 @@ 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->copyNodeStatus(num, ...). */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; - - private: - static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5; - std::vector recentReceived; }; extern StatusMessageModule *statusMessageModule; -#endif \ No newline at end of file +#endif 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..ca96eaa7a 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -100,7 +100,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, char distStr[20] = ""; // Get our node, to use our own position - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // Match compass sizing/placement to favorite node screen logic. const int w = display->getWidth(); @@ -147,8 +147,10 @@ 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; + meshtastic_PositionLite ownPos; + const bool haveOwnPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ownPos); + if (hasOwnPositionFix && haveOwnPos) { + 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 3e477cae5..bd02ac045 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..36ed4b507 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -319,10 +319,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, // Lambda function for adding a long name to the route 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; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); + bool name_known = nodeInfoLiteHasUser(node); + if (name_known) { + const size_t copy_len = + (sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1; + memcpy(long_name, node->long_name, copy_len); + long_name[copy_len] = '\0'; + } route->push_back(new JSONValue(long_name)); }; addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) @@ -473,4 +477,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa delete value; return jsonStr; } -#endif \ No newline at end of file +#endif diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index bd0a29c51..c79b3d269 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -292,9 +292,13 @@ 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; - if (name_known) - memcpy(long_name, node->user.long_name, sizeof(long_name)); + bool name_known = nodeInfoLiteHasUser(node); + if (name_known) { + const size_t copy_len = + (sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1; + memcpy(long_name, node->long_name, copy_len); + long_name[copy_len] = '\0'; + } route->add(long_name); }; @@ -418,4 +422,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa return jsonStr; } -#endif \ No newline at end of file +#endif 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_packet_history/test_main.cpp b/test/test_packet_history/test_main.cpp index 2453956c5..4f4782ab6 100644 --- a/test/test_packet_history/test_main.cpp +++ b/test/test_packet_history/test_main.cpp @@ -641,30 +641,12 @@ void test_sender_zero_substituted(void) TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); } -void test_uninitialized_wasSeenRecently(void) -{ - // Simulate uninitialized state — create a PacketHistory that looks uninitialized - // We can't easily make allocation fail, but we can test the initOk guard with a destructed one - PacketHistory h(4); - TEST_ASSERT_TRUE(h.initOk()); // sanity check - h.~PacketHistory(); - - auto p = makePacket(0x1111, 100); - TEST_ASSERT_FALSE(h.wasSeenRecently(&p)); - - // Reconstruct in place to allow proper destruction - new (&h) PacketHistory(4); -} - -void test_uninitialized_wasRelayer(void) -{ - PacketHistory h(4); - h.~PacketHistory(); - - TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111)); - - new (&h) PacketHistory(4); -} +// NOTE: Tests for the !initOk() short-circuit in wasSeenRecently / wasRelayer +// were removed: the only way to drive that branch from a test is to destruct +// the object and then call methods on it, which is UB and trips ASAN under +// `pio test -e coverage`. In practice the guard can only fire if `new[]` throws +// (in which case the constructor doesn't return), so the lost coverage is +// purely defensive. void test_multiple_instances_independent(void) { @@ -819,8 +801,6 @@ void setup() // Group 10 — Edge Cases RUN_TEST(test_packet_id_zero_not_stored); RUN_TEST(test_sender_zero_substituted); - RUN_TEST(test_uninitialized_wasSeenRecently); - RUN_TEST(test_uninitialized_wasRelayer); RUN_TEST(test_multiple_instances_independent); // Group 11 — Hash Table Stress 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() {}