2.8: NodeDB shrink, decoupling, and restructuring (#10413)

* 2.8: NodeDB refactor to decouple satellite entries and decrease size

* Regen

* Refactor node mute handling to use dedicated functions for clarity and consistency

* Develop ref

* Fix NodeDB review follow-ups

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Address review validation nits

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Trunk

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Extract legacy NodeDatabase migration

* Fix remaining NodeDB review issues

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/c76b9a5a-7244-4fbc-9ef0-98091d8caaea

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Fixes

* Trunk

* Fix latest review compile follow-ups

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/5198da01-ec4c-4c16-8a09-68b8e6d5d410

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Fix cppcheck style warnings

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/e60287ba-4ece-46e0-83d8-a6d89664c0bb

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Change pointer type for mesh node in set_favorite function

* Change pointer types for mesh node references to const in multiple applets

* Add NodeDB layout v25 documentation and migration guidelines

* Remove tests for uninitialized PacketHistory state due to undefined behavior

* Fix code block formatting in copilot instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ben Meadors
2026-05-09 15:12:10 -05:00
committed by GitHub
co-authored by GitHub copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Copilot Autofix powered by AI
parent 1bcabb893b
commit 94bb21ecc7
58 changed files with 2605 additions and 643 deletions
+89
View File
@@ -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. - **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. - **`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<NodeNum, …>` 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 ## Project Structure
``` ```
+3 -6
View File
@@ -53,8 +53,7 @@ class GPSStatus : public Status
int32_t getLatitude() const int32_t getLatitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.latitude_i;
return node->position.latitude_i;
} else { } else {
return p.latitude_i; return p.latitude_i;
} }
@@ -63,8 +62,7 @@ class GPSStatus : public Status
int32_t getLongitude() const int32_t getLongitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.longitude_i;
return node->position.longitude_i;
} else { } else {
return p.longitude_i; return p.longitude_i;
} }
@@ -73,8 +71,7 @@ class GPSStatus : public Status
int32_t getAltitude() const int32_t getAltitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.altitude;
return node->position.altitude;
} else { } else {
return p.altitude; return p.altitude;
} }
+2 -2
View File
@@ -1285,7 +1285,7 @@ void Screen::setFrames(FrameFocus focus)
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(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); 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_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel = const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); 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<const char *>(packet->decoded.payload.bytes); const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
+18 -22
View File
@@ -845,10 +845,10 @@ void menuHandler::messageViewModeMenu()
// Encode peers // Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) { for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i]; uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer); const auto *node = nodeDB->getMeshNode(peer);
std::string name; std::string name;
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
name = sanitizeString(node->user.long_name).substr(0, 15); name = sanitizeString(node->long_name).substr(0, 15);
else { else {
char buf[20]; char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer); snprintf(buf, sizeof(buf), "Node %08X", peer);
@@ -1355,14 +1355,14 @@ void menuHandler::manageNodeMenu()
static int optionsEnumArray[enumEnd] = {Back}; static int optionsEnumArray[enumEnd] = {Back};
int options = 1; int options = 1;
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
optionsArray[options] = "Unfavorite"; optionsArray[options] = "Unfavorite";
} else { } else {
optionsArray[options] = "Favorite"; optionsArray[options] = "Favorite";
} }
optionsEnumArray[options++] = Favorite; optionsEnumArray[options++] = Favorite;
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
if (isMuted) { if (isMuted) {
optionsArray[options] = "Unmute Notifications"; optionsArray[options] = "Unmute Notifications";
} else { } else {
@@ -1376,7 +1376,7 @@ void menuHandler::manageNodeMenu()
optionsArray[options] = "Key Verification"; optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = KeyVerification; optionsEnumArray[options++] = KeyVerification;
if (node->is_ignored) { if (nodeInfoLiteIsIgnored(node)) {
optionsArray[options] = "Unignore Node"; optionsArray[options] = "Unignore Node";
} else { } else {
optionsArray[options] = "Ignore Node"; optionsArray[options] = "Ignore Node";
@@ -1386,8 +1386,8 @@ void menuHandler::manageNodeMenu()
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
std::string title = ""; std::string title = "";
if (node->has_user && node->user.long_name && node->user.long_name[0]) { if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
title += sanitizeString(node->user.long_name).substr(0, 15); title += sanitizeString(node->long_name).substr(0, 15);
} else { } else {
char buf[20]; char buf[20];
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
@@ -1409,7 +1409,7 @@ void menuHandler::manageNodeMenu()
if (!n) { if (!n) {
return; return;
} }
if (n->is_favorite) { if (nodeInfoLiteIsFavorite(n)) {
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(false, menuHandler::pickedNodeNum); nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
} else { } else {
@@ -1426,13 +1426,9 @@ void menuHandler::manageNodeMenu()
return; return;
} }
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { const bool wasMuted = nodeInfoLiteIsMuted(n);
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
} else {
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true); nodeDB->notifyObservers(true);
nodeDB->saveToDisk(); nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1461,11 +1457,11 @@ void menuHandler::manageNodeMenu()
return; return;
} }
if (n->is_ignored) { if (nodeInfoLiteIsIgnored(n)) {
n->is_ignored = false; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
} else { } else {
n->is_ignored = true; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
} }
nodeDB->notifyObservers(true); nodeDB->notifyObservers(true);
@@ -2079,9 +2075,9 @@ void menuHandler::removeFavoriteMenu()
static const char *optionsArray[] = {"Back", "Yes"}; static const char *optionsArray[] = {"Back", "Yes"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
std::string message = "Unfavorite This Node?\n"; std::string message = "Unfavorite This Node?\n";
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
message += sanitizeString(node->user.long_name).substr(0, 15); message += sanitizeString(node->long_name).substr(0, 15);
} }
bannerOptions.message = message.c_str(); bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
+18 -19
View File
@@ -462,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} }
case ThreadMode::DIRECT: { case ThreadMode::DIRECT: {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
if (node && node->has_user && node->user.short_name[0]) { if (nodeInfoLiteHasUser(node) && node->short_name[0]) {
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name);
} else { } else {
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); 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); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
char senderName[64] = ""; char senderName[64] = "";
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
if (node->user.long_name[0]) { if (node->long_name[0]) {
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); strncpy(senderName, node->long_name, sizeof(senderName) - 1);
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); strncpy(senderName, node->short_name, sizeof(senderName) - 1);
} }
senderName[sizeof(senderName) - 1] = '\0'; 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 // If this is *our own* message, override senderName to who the recipient was
bool mine = (m.sender == nodeDB->getNodeNum()); bool mine = (m.sender == nodeDB->getNodeNum());
if (mine && node_recipient && node_recipient->has_user) { if (mine && nodeInfoLiteHasUser(node_recipient)) {
if (node_recipient->user.long_name[0]) { if (node_recipient->long_name[0]) {
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0'; senderName[sizeof(senderName) - 1] = '\0';
} else if (node_recipient->user.short_name[0]) { } else if (node_recipient->short_name[0]) {
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0'; senderName[sizeof(senderName) - 1] = '\0';
} }
} }
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages. // If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
if (mine && (!node_recipient || !node_recipient->has_user || if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) {
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
} }
@@ -1073,12 +1072,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
// Banner logic // Banner logic
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
char longName[64] = "?"; char longName[64] = "?";
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
if (node->user.long_name[0]) { if (node->long_name[0]) {
strncpy(longName, node->user.long_name, sizeof(longName) - 1); strncpy(longName, node->long_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0'; longName[sizeof(longName) - 1] = '\0';
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
strncpy(longName, node->user.short_name, sizeof(longName) - 1); strncpy(longName, node->short_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0'; longName[sizeof(longName) - 1] = '\0';
} }
} }
+46 -44
View File
@@ -98,29 +98,23 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
// 1) Choose target candidate (long vs short) only if present // 1) Choose target candidate (long vs short) only if present
const char *raw = nullptr; 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, // 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; std::string composedFromStatus;
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) { if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) {
const auto &recent = statusMessageModule->getRecentReceived(); meshtastic_StatusMessage cachedStatus;
const StatusMessageModule::RecentStatus *found = nullptr; if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
for (auto it = recent.rbegin(); it != recent.rend(); ++it) { const char *shortName = node->short_name;
if (it->fromNodeId == node->num && !it->statusText.empty()) { const size_t statusLen = std::strlen(cachedStatus.status);
found = &(*it); composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen);
break;
}
}
if (found) {
const char *shortName = node->user.short_name;
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
composedFromStatus += "("; composedFromStatus += "(";
if (shortName && *shortName) { if (shortName && *shortName) {
composedFromStatus += shortName; composedFromStatus += shortName;
} }
composedFromStatus += ") "; composedFromStatus += ") ";
composedFromStatus += found->statusText; composedFromStatus += cachedStatus.status;
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string 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 we didn't compose from status, use normal long/short selection
if (!raw) { if (!raw) {
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; 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, static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
int16_t nameX, int16_t y, int nameMaxWidth) int16_t nameX, int16_t y, int nameMaxWidth)
{ {
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) { if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) {
return; return;
} }
@@ -259,7 +253,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
char timeStr[10]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -279,14 +273,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -321,20 +315,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -401,15 +395,19 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { meshtastic_PositionLite ourPos;
double lat1 = ourNode->position.latitude_i * 1e-7; meshtastic_PositionLite theirPos;
double lon1 = ourNode->position.longitude_i * 1e-7; const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
double lat2 = node->position.latitude_i * 1e-7; const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos);
double lon2 = node->position.longitude_i * 1e-7; 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 earthRadiusKm = 6371.0;
double dLat = (lat2 - lat1) * DEG_TO_RAD; 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->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -509,19 +507,19 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -542,8 +540,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
int centerX = x + columnWidth - arrowXOffset; int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2; int centerY = y + FONT_HEIGHT_SMALL / 2;
double nodeLat = node->position.latitude_i * 1e-7; meshtastic_PositionLite nodePos;
double nodeLon = node->position.longitude_i * 1e-7; 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 bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian); float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing); float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
@@ -652,7 +653,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
continue; continue;
if (n->num == nodeDB->getNodeNum()) if (n->num == nodeDB->getNodeNum())
continue; continue;
if (locationScreen && !n->has_position) if (locationScreen && !nodeDB->hasNodePosition(n->num))
continue; continue;
drawList.push_back(n->num); 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) void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
float headingRadian = 0.0f; float headingRadian = 0.0f;
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { 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); drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
return; return;
} }
double lat = DegD(ourNode->position.latitude_i); double lat = DegD(ourSelfPos.latitude_i);
double lon = DegD(ourNode->position.longitude_i); double lon = DegD(ourSelfPos.longitude_i);
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
display->clear(); display->clear();
+5 -5
View File
@@ -317,12 +317,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char tempName[48] = {0}; char tempName[48] = {0};
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
const char *rawName = nullptr; const char *rawName = nullptr;
if (node->user.long_name[0]) { if (node->long_name[0]) {
rawName = node->user.long_name; rawName = node->long_name;
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
rawName = node->user.short_name; rawName = node->short_name;
} }
if (rawName) { if (rawName) {
const int arrowWidth = (currentResolution == ScreenResolution::High) const int arrowWidth = (currentResolution == ScreenResolution::High)
+42 -44
View File
@@ -464,11 +464,11 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset,
return true; 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. // Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down.
char rawStatus[96]; char rawStatus[96];
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str()); snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText ? statusText : "");
char clippedStatus[96]; char clippedStatus[96];
UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth()); UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth());
@@ -495,7 +495,7 @@ void graphics::UIRenderer::rebuildFavoritedNodes()
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (!n || n->num == nodeDB->getNodeNum()) if (!n || n->num == nodeDB->getNodeNum())
continue; continue;
if (n->is_favorite) if (nodeInfoLiteIsFavorite(n))
favoritedNodes.push_back(n); favoritedNodes.push_back(n);
} }
@@ -750,7 +750,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
return; return;
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node))
return; return;
display->clear(); display->clear();
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
@@ -763,7 +763,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
#endif #endif
currentFavoriteNodeNum = node->num; currentFavoriteNodeNum = node->num;
// === Create the shortName and title string === // === 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]; char titlestr[40];
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); 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) === // === 1. Long Name (always try to show first) ===
const char *username; const char *username;
if (currentResolution == ScreenResolution::UltraLow) { 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 { } 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") // 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); 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 === // === Optional: Last received StatusMessage line for this node ===
// Display it directly under the username line (if we have one). // Display it directly under the username line (if we have one). The cache
if (statusMessageModule) { // lives on NodeDB now, keyed by NodeNum, so this is an O(1) lookup.
const auto &recent = statusMessageModule->getRecentReceived(); if (nodeDB) {
const StatusMessageModule::RecentStatus *found = nullptr; meshtastic_StatusMessage cachedStatus;
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
// Search newest-to-oldest drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus.status);
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);
} }
} }
#endif #endif
@@ -971,25 +962,30 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
#if !defined(M5STACK_UNITC6L) #if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) === // === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = ""; 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:" char upPrefix[12]; // enough for leftSideSpacing + "Up:"
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); 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]) { if (uptimeStr[0]) {
display->drawString(x, getTextPositions(display)[line++], uptimeStr); display->drawString(x, getTextPositions(display)[line++], uptimeStr);
} }
// === 5. Distance (only if both nodes have GPS position) === // === 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 char distStr[24] = ""; // Make buffer big enough for any string
bool haveDistance = false; 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. // Use shared meter conversion, then format display units with lightweight integer rounding.
const float distanceMeters = const float distanceMeters = GeoCoord::latLongToMeter(DegD(nodePos.latitude_i), DegD(nodePos.longitude_i),
GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i), DegD(ourPos.latitude_i), DegD(ourPos.longitude_i));
DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i));
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f); const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f);
if (feet > 0 && feet < 1000) { if (feet > 0 && feet < 1000) {
@@ -1024,19 +1020,19 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
char batLine[32] = ""; char batLine[32] = "";
bool haveBatLine = false; bool haveBatLine = false;
if (node->has_device_metrics) { if (haveNodeMetrics) {
bool hasPct = node->device_metrics.has_battery_level; bool hasPct = nodeMetrics.has_battery_level;
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; bool hasVolt = nodeMetrics.has_voltage && nodeMetrics.voltage > 0.001f;
int pct = 0; int pct = 0;
float volt = 0.0f; float volt = 0.0f;
if (hasPct) { if (hasPct) {
pct = (int)node->device_metrics.battery_level; pct = (int)nodeMetrics.battery_level;
} }
if (hasVolt) { if (hasVolt) {
volt = node->device_metrics.voltage; volt = nodeMetrics.voltage;
} }
if (hasPct && pct > 0 && pct <= 100) { if (hasPct && pct > 0 && pct <= 100) {
@@ -1076,11 +1072,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
const bool hasNodePositionFix = nodeDB->hasValidPosition(node); const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
const char *statusLine1 = nullptr; const char *statusLine1 = nullptr;
const char *statusLine2 = nullptr; const char *statusLine2 = nullptr;
if (hasOwnPositionFix && hasNodePositionFix) { if (hasOwnPositionFix && hasNodePositionFix && haveOurPos && haveNodePos) {
const auto &op = ourNode->position; const auto &op = ourPos;
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
if (showCompass) { 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 = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading); bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
} else { } else {
@@ -1130,7 +1126,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int line = 1; int line = 1;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header === // === Header ===
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
@@ -1270,7 +1266,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; 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 : ""; const char *shortName = owner.short_name ? owner.short_name : "";
char combinedName[96]; char combinedName[96];
if (longName[0] && shortName[0]) { 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()), geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
int32_t(gpsStatus->getAltitude())); 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 hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
const bool hasLiveGpsFix = const bool hasLiveGpsFix =
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0)); (gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
@@ -1613,9 +1609,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
headingLat = DegD(gpsStatus->getLatitude()); headingLat = DegD(gpsStatus->getLatitude());
headingLon = DegD(gpsStatus->getLongitude()); headingLon = DegD(gpsStatus->getLongitude());
} else if (hasOwnPositionFix) { } else if (hasOwnPositionFix) {
const auto &op = ourNode->position; meshtastic_PositionLite ownPos;
headingLat = DegD(op.latitude_i); if (nodeDB->copyNodePosition(ourNode->num, ownPos)) {
headingLon = DegD(op.longitude_i); headingLat = DegD(ownPos.latitude_i);
headingLon = DegD(ownPos.longitude_i);
}
} }
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading); validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
} }
+2 -2
View File
@@ -362,8 +362,8 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
assert(node); assert(node);
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
if (node->has_user) { if (nodeInfoLiteHasUser(node)) {
std::string parsed = parse(node->user.short_name); std::string parsed = parse(node->short_name);
if (isPrintable(parsed)) if (isPrintable(parsed))
return parsed; return parsed;
} }
@@ -136,9 +136,10 @@ void InkHUD::MapApplet::onRender(bool full)
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
// Draw our node LAST with full white fill + outline // Draw our node LAST with full white fill + outline
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) { meshtastic_PositionLite ourSelfPos;
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); 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 centerX = X(0.5) + (self.eastMeters * metersToPx);
int16_t centerY = Y(0.5) - (self.northMeters * 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) void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
{ {
// If we have a valid position for our own node, use that as the anchor // If we have a valid position for our own node, use that as the anchor
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) { meshtastic_PositionLite ourSelfPos;
*lat = ourNode->position.latitude_i * 1e-7; if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
*lng = ourNode->position.longitude_i * 1e-7; *lat = ourSelfPos.latitude_i * 1e-7;
*lng = ourSelfPos.longitude_i * 1e-7;
} else { } else {
// Find mean lat long coords // Find mean lat long coords
// ============================ // ============================
@@ -201,9 +203,13 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
if (!shouldDrawNode(node)) if (!shouldDrawNode(node))
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Latitude and Longitude of node, in radians // Latitude and Longitude of node, in radians
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; float latRad = pos.latitude_i * (1e-7) * DEG_TO_RAD;
float lngRad = node->position.longitude_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 // 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 // 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)) if (!shouldDrawNode(node))
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Check for a new top or bottom latitude // 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); northernmost = max(northernmost, latNode);
southernmost = min(southernmost, latNode); southernmost = min(southernmost, latNode);
// Longitude is trickier // 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 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 float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward) 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 // Find x and y position based on node's position in nodeDB
assert(nodeDB->hasValidPosition(node)); assert(nodeDB->hasValidPosition(node));
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style meshtastic_PositionLite pos;
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style const bool hasPos = nodeDB->copyNodePosition(node->num, pos);
node->has_hops_away, // Is the hopsAway number valid assert(hasPos);
node->hops_away // Hops away 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 // Convert to pixel coords
@@ -516,13 +529,16 @@ void InkHUD::MapApplet::calculateAllMarkers()
if (node->num == nodeDB->getNodeNum()) if (node->num == nodeDB->getNodeNum())
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Calculate marker and store it // Calculate marker and store it
markers.push_back( markers.push_back(calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
calculateMarker(node->position.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->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style node->has_hops_away, // Is the hopsAway number valid
node->has_hops_away, // Is the hopsAway number valid node->hops_away // Hops away
node->hops_away // Hops away ));
));
} }
} }
@@ -50,21 +50,23 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
// Assemble info: from nodeDB (needed to detect changes) // Assemble info: from nodeDB (needed to detect changes)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node) { if (node) {
if (node->has_hops_away) if (node->has_hops_away)
c.hopsAway = node->hops_away; c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float meshtastic_PositionLite ourPos;
// Meshtastic stores these as integers internally meshtastic_PositionLite theirPos;
float ourLat = ourNode->position.latitude_i * 1e-7; if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
float ourLong = ourNode->position.longitude_i * 1e-7; float ourLat = ourPos.latitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7; float ourLong = ourPos.longitude_i * 1e-7;
float theirLong = node->position.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 -- // -- Longname --
// Parse special chars in long name // Parse special chars in long name
// Use node id if unknown // Use node id if unknown
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
longName = parse(node->user.long_name); // Found in nodeDB longName = parse(node->long_name); // Found in nodeDB
else { else {
// Not found in nodeDB, show a hex nodeid instead // Not found in nodeDB, show a hex nodeid instead
longName = hexifyNodeNum(nodeNum); longName = hexifyNodeNum(nodeNum);
@@ -2090,7 +2090,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Count favorites // Count favorites
for (uint32_t i = 0; i < nodeCount; i++) { for (uint32_t i = 0; i < nodeCount; i++) {
if (nodeDB->getMeshNodeByIndex(i)->is_favorite) if (nodeInfoLiteIsFavorite(nodeDB->getMeshNodeByIndex(i)))
favoriteCount++; favoriteCount++;
} }
@@ -2098,10 +2098,10 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Don't want some monstrous list that takes 100 clicks to reach exit // Don't want some monstrous list that takes 100 clicks to reach exit
if (favoriteCount < 20) { if (favoriteCount < 20) {
for (uint32_t i = 0; i < nodeCount; i++) { 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 // Skip node if not a favorite
if (!node->is_favorite) if (!nodeInfoLiteIsFavorite(node))
continue; continue;
CannedMessages::RecipientItem r; CannedMessages::RecipientItem r;
@@ -2111,8 +2111,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Set a label for the menu item // Set a label for the menu item
r.label = "DM: "; r.label = "DM: ";
if (node->has_user) if (nodeInfoLiteHasUser(node))
r.label += parse(node->user.long_name); r.label += parse(node->long_name);
else else
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
@@ -241,7 +241,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += msgIsBroadcast ? "From:" : "DM: "; text += msgIsBroadcast ? "From:" : "DM: ";
// Sender id // Sender id
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
text += parseShortName(node); text += parseShortName(node);
else else
text += hexifyNodeNum(message->sender); text += hexifyNodeNum(message->sender);
@@ -255,7 +255,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += msgIsBroadcast ? "Msg from " : "DM from "; text += msgIsBroadcast ? "Msg from " : "DM from ";
// Sender id // Sender id
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
text += parseShortName(node); text += parseShortName(node);
else else
text += hexifyNodeNum(message->sender); text += hexifyNodeNum(message->sender);
@@ -70,10 +70,10 @@ void InkHUD::AllMessageApplet::onRender(bool full)
// - short name and long name, if available, or // - short name and long name, if available, or
// - node id // - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); 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 += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " ("; header += " (";
header += parse(sender->user.long_name); header += parse(sender->long_name);
header += ")"; header += ")";
} else } else
header += hexifyNodeNum(message->sender); header += hexifyNodeNum(message->sender);
@@ -66,10 +66,10 @@ void InkHUD::DMApplet::onRender(bool full)
// - shortname and long name, if available, or // - shortname and long name, if available, or
// - node id // - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); 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 += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " ("; header += " (";
header += parse(sender->user.long_name); header += parse(sender->long_name);
header += ")"; header += ")";
} else } else
header += hexifyNodeNum(latestMessage->dm.sender); header += hexifyNodeNum(latestMessage->dm.sender);
@@ -8,7 +8,7 @@ using namespace NicheGraphics;
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
{ {
// Keep our own node available as map anchor/center; all others must be favorited. // 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) 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. // Draw our latest "node of interest" as a special marker.
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); 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); drawLabeledMarker(node);
} }
@@ -74,7 +74,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
} else { } else {
// For non-local packets, this applet only reacts to favorited nodes. // For non-local packets, this applet only reacts to favorited nodes.
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (!sender || !sender->is_favorite) if (!nodeInfoLiteIsFavorite(sender))
return ProcessMessage::CONTINUE; return ProcessMessage::CONTINUE;
// Check if this position is from someone different than our previous position packet. // Check if this position is from someone different than our previous position packet.
@@ -80,8 +80,8 @@ void InkHUD::HeardApplet::populateFromNodeDB()
ordered.resize(maxCards()); ordered.resize(maxCards());
// Create card info for these (stale) node observations // Create card info for these (stale) node observations
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
for (meshtastic_NodeInfoLite *node : ordered) { for (const meshtastic_NodeInfoLite *node : ordered) {
CardInfo c; CardInfo c;
c.nodeNum = node->num; c.nodeNum = node->num;
@@ -89,14 +89,16 @@ void InkHUD::HeardApplet::populateFromNodeDB()
c.hopsAway = node->hops_away; c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float meshtastic_PositionLite ourPos;
// Meshtastic stores these as integers internally meshtastic_PositionLite theirPos;
float ourLat = ourNode->position.latitude_i * 1e-7; if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
float ourLong = ourNode->position.longitude_i * 1e-7; float ourLat = ourPos.latitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7; float ourLong = ourPos.longitude_i * 1e-7;
float theirLong = node->position.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) // Insert into the card collection (member of base class)
@@ -122,4 +124,4 @@ std::string InkHUD::HeardApplet::getHeaderText()
return text; return text;
} }
#endif #endif
+2 -2
View File
@@ -112,7 +112,7 @@ bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, mes
* @param bytes Buffer containing plaintext input. * @param bytes Buffer containing plaintext input.
* @param bytesOut Output buffer to be populated with encrypted ciphertext. * @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) uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
{ {
uint8_t *auth; uint8_t *auth;
@@ -152,7 +152,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
* @param bytes Buffer containing ciphertext input. * @param bytes Buffer containing ciphertext input.
* @param bytesOut Output buffer to be populated with decrypted plaintext. * @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) size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
{ {
const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text?
+5 -2
View File
@@ -40,9 +40,12 @@ class CryptoEngine
#endif #endif
void setDHPrivateKey(uint8_t *_private_key); 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); 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); size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
virtual bool setDHPublicKey(uint8_t *publicKey); virtual bool setDHPublicKey(uint8_t *publicKey);
virtual void hash(uint8_t *bytes, size_t numBytes); virtual void hash(uint8_t *bytes, size_t numBytes);
+12 -12
View File
@@ -94,8 +94,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
// ignore our request for its NodeInfo // ignore our request for its NodeInfo
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster &&
!nodeDB->isFull()) {
if (airTime->isTxAllowedChannelUtil(true)) { if (airTime->isTxAllowedChannelUtil(true)) {
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
@@ -392,19 +393,16 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
assert(node); 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) // Update our local node info with our time (even if we don't decide to update anyone else)
node->last_heard = node->last_heard =
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid 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) { if (powerStatus->getHasBattery() == 1) {
updateBatteryLevel(powerStatus->getBatteryChargePercent()); updateBatteryLevel(powerStatus->getBatteryChargePercent());
@@ -432,7 +430,9 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
// Used fixed position if configured regardless of GPS lock // Used fixed position if configured regardless of GPS lock
if (config.position.fixed_position) { if (config.position.fixed_position) {
LOG_WARN("Use 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 // Add a fresh timestamp
+553 -125
View File
@@ -22,6 +22,7 @@
#include "error.h" #include "error.h"
#include "main.h" #include "main.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include "meshUtils.h" #include "meshUtils.h"
#include "modules/NeighborInfoModule.h" #include "modules/NeighborInfoModule.h"
#include <ErriezCRC32.h> #include <ErriezCRC32.h>
@@ -151,24 +152,103 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
#endif #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) { const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData; switch (iter->tag) {
for (auto item : *vec) { case meshtastic_NodeDatabase_nodes_tag: {
if (!pb_encode_tag_for_field(ostream, field)) if (ostream) {
return false; const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); 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<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
vec->push_back(node);
}
return true;
} }
if (istream) { case meshtastic_NodeDatabase_positions_tag: {
meshtastic_NodeInfoLite node; // this gets good data if (ostream) {
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData; const auto *vec = static_cast<const std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
for (auto item : *vec) {
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) if (!pb_encode_tag_for_field(ostream, iter))
vec->push_back(node); return false;
if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodePositionEntry entry;
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(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<const std::vector<meshtastic_NodeTelemetryEntry> *>(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<std::vector<meshtastic_NodeTelemetryEntry> *>(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<const std::vector<meshtastic_NodeStatusEntry> *>(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<std::vector<meshtastic_NodeStatusEntry> *>(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<const std::vector<meshtastic_NodeEnvironmentEntry> *>(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<std::vector<meshtastic_NodeEnvironmentEntry> *>(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 /** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
@@ -298,8 +378,7 @@ NodeDB::NodeDB()
#endif #endif
// Include our owner in the node db under our nodenum // Include our owner in the node db under our nodenum
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
info->user = TypeConversions::ConvertToUserLite(owner); TypeConversions::CopyUserToNodeInfoLite(info, owner);
info->has_user = true;
// If node database has not been saved for the first time, save it now // If node database has not been saved for the first time, save it now
#ifdef FSCom #ifdef FSCom
@@ -442,8 +521,12 @@ NodeDB::NodeDB()
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
config.has_position = true; config.has_position = true;
info->has_position = true; #if !MESHTASTIC_EXCLUDE_POSITIONDB
info->position = TypeConversions::ConvertToPositionLite(fixedGPS); {
concurrency::LockGuard guard(&satelliteMutex);
nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS);
}
#endif
nodeDB->setLocalPosition(fixedGPS); nodeDB->setLocalPosition(fixedGPS);
config.position.fixed_position = true; config.position.fixed_position = true;
#endif #endif
@@ -479,6 +562,29 @@ bool isBroadcast(uint32_t dest)
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
} }
namespace
{
template <typename Map, typename Value> 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 <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map &map, NodeNum exclude)
{
std::vector<NodeNum> 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) void NodeDB::resetRadioConfig(bool is_fresh_install)
{ {
if (is_fresh_install) { if (is_fresh_install) {
@@ -548,6 +654,19 @@ void NodeDB::installDefaultNodeDatabase()
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES); nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
numMeshNodes = 0; numMeshNodes = 0;
meshNodes = &nodeDatabase.nodes; 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) void NodeDB::installDefaultConfig(bool preserveKey = false)
@@ -1019,12 +1138,14 @@ void NodeDB::resetNodes(bool keepFavorites)
{ {
if (!config.position.fixed_position) if (!config.position.fixed_position)
clearLocalPosition(); clearLocalPosition();
NodeNum ourNum = getNodeNum();
numMeshNodes = 1; numMeshNodes = 1;
if (keepFavorites) { if (keepFavorites) {
LOG_INFO("Clearing node database - preserving favorites"); LOG_INFO("Clearing node database - preserving favorites");
for (size_t i = 0; i < meshNodes->size(); i++) { for (size_t i = 0; i < meshNodes->size(); i++) {
meshtastic_NodeInfoLite &node = meshNodes->at(i); meshtastic_NodeInfoLite &node = meshNodes->at(i);
if (i > 0 && !node.is_favorite) { if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
eraseNodeSatellites(node.num);
node = meshtastic_NodeInfoLite(); node = meshtastic_NodeInfoLite();
} else { } else {
numMeshNodes += 1; numMeshNodes += 1;
@@ -1032,8 +1153,14 @@ void NodeDB::resetNodes(bool keepFavorites)
}; };
} else { } else {
LOG_INFO("Clearing node database - removing favorites"); 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()); std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
} }
(void)ourNum;
devicestate.has_rx_text_message = false; devicestate.has_rx_text_message = false;
devicestate.has_rx_waypoint = false; devicestate.has_rx_waypoint = false;
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
@@ -1054,36 +1181,173 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
numMeshNodes -= removed; numMeshNodes -= removed;
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
meshtastic_NodeInfoLite()); meshtastic_NodeInfoLite());
if (removed)
eraseNodeSatellites(nodeNum);
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
} }
void NodeDB::clearLocalPosition() void NodeDB::clearLocalPosition()
{ {
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); #if !MESHTASTIC_EXCLUDE_POSITIONDB
node->position.latitude_i = 0; concurrency::LockGuard guard(&satelliteMutex);
node->position.longitude_i = 0; nodePositions.erase(getNodeNum());
node->position.altitude = 0; #endif
node->position.time = 0;
setLocalPosition(meshtastic_Position_init_default); setLocalPosition(meshtastic_Position_init_default);
localPositionUpdatedSinceBoot = false; 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<NodeNum> NodeDB::snapshotPositionNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodePositions, exclude);
#endif
}
std::vector<NodeNum> NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodeTelemetry, exclude);
#endif
}
std::vector<NodeNum> NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodeEnvironment, exclude);
#endif
}
std::vector<NodeNum> 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() void NodeDB::cleanupMeshDB()
{ {
int newPos = 0, removed = 0; int newPos = 0, removed = 0;
for (int i = 0; i < numMeshNodes; i++) { for (int i = 0; i < numMeshNodes; i++) {
if (meshNodes->at(i).has_user) { meshtastic_NodeInfoLite &n = meshNodes->at(i);
if (meshNodes->at(i).user.public_key.size > 0) { if (nodeInfoLiteHasUser(&n)) {
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { if (n.public_key.size > 0) {
meshNodes->at(i).user.public_key.size = 0; if (memfll(n.public_key.bytes, 0, n.public_key.size)) {
n.public_key.size = 0;
} }
} }
if (newPos != i) if (newPos != i)
meshNodes->at(newPos++) = meshNodes->at(i); meshNodes->at(newPos++) = n;
else else
newPos++; newPos++;
} else { } else {
// No user info - drop this node and its satellites
const NodeNum gone = n.num;
if (gone)
eraseNodeSatellites(gone);
removed++; removed++;
} }
} }
@@ -1141,14 +1405,22 @@ void NodeDB::pickNewNodeNum()
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; 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; 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 == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found) 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 " LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
"trying for 0x%x",
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
nodeNum = candidate; nodeNum = candidate;
} }
LOG_DEBUG("Use nodenum 0x%x ", nodeNum); LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
@@ -1169,7 +1441,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
if (f) { if (f) {
LOG_INFO("Load %s", filename); LOG_INFO("Load %s", filename);
pb_istream_t stream = {&readcb, &f, protoSize}; 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); memset(dest_struct, 0, objSize);
if (!pb_decode(&stream, fields, dest_struct)) { if (!pb_decode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); 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) { if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
installDefaultNodeDatabase(); installDefaultNodeDatabase();
} else if (nodeDatabase.version < DEVICESTATE_CUR_VER) {
if (migrateLegacyNodeDatabase())
saveNodeDatabaseToDisk();
else
installDefaultNodeDatabase();
} else { } else {
meshNodes = &nodeDatabase.nodes; meshNodes = &nodeDatabase.nodes;
numMeshNodes = nodeDatabase.nodes.size(); 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()); 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. // Attempt recovery of owner fields from our own NodeDB entry if available.
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); 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", LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
us->num); 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'; 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.short_name[sizeof(owner.short_name) - 1] = '\0';
owner.is_licensed = us->user.is_licensed; owner.is_licensed = nodeInfoLiteIsLicensed(us);
owner.has_is_unmessagable = us->user.has_is_unmessagable; owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us);
owner.is_unmessagable = us->user.is_unmessagable; owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us);
// Save the recovered owner to device state on disk // Save the recovered owner to device state on disk
saveToDisk(SEGMENT_DEVICESTATE); saveToDisk(SEGMENT_DEVICESTATE);
@@ -1528,9 +1849,75 @@ bool NodeDB::saveNodeDatabaseToDisk()
FSCom.mkdir("/prefs"); FSCom.mkdir("/prefs");
spiLock->unlock(); spiLock->unlock();
#endif #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; size_t nodeDatabaseSize;
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); 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) bool NodeDB::saveToDiskNoRetry(int saveWhat)
@@ -1699,7 +2086,7 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
// FIXME this implementation is kinda expensive // FIXME this implementation is kinda expensive
for (int i = 0; i < numMeshNodes; i++) { for (int i = 0; i < numMeshNodes; i++) {
if (localOnly && meshNodes->at(i).via_mqtt) if (localOnly && nodeInfoLiteViaMqtt(&meshNodes->at(i)))
continue; continue;
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
numseen++; numseen++;
@@ -1736,62 +2123,94 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
return; 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) { if (src == RX_SRC_LOCAL) {
// Local packet, fully authoritative LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i,
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); p.altitude);
setLocalPosition(p); 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; updateGUIforNode = info;
notifyObservers(true); // Force an update whether or not our node counts have changed notifyObservers(true); // Force an update whether or not our node counts have changed
#endif
} }
/** Update telemetry info for this node based on received metrics /** Update telemetry info for this node based on received metrics. Stores
* We only care about device telemetry here * 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) void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
{ {
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
// Environment metrics should never go to NodeDb but we'll safegaurd anyway if (!info)
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
return; return;
}
if (src == RX_SRC_LOCAL) { if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
// Local packet, fully authoritative if (src == RX_SRC_LOCAL) {
LOG_DEBUG("updateTelemetry 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 { } 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; 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 // If the local node has this node marked as manually verified
// and the client does not, do not allow the client to update the // and the client does not, do not allow the client to update the
// saved public key. // saved public key.
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { if (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) {
if (contact.user.public_key.size != info->user.public_key.size || if (contact.user.public_key.size != info->public_key.size ||
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) {
return; return;
} }
} }
info->num = contact.node_num; info->num = contact.node_num;
info->has_user = true; TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
info->user = TypeConversions::ConvertToUserLite(contact.user);
if (contact.should_ignore) { if (contact.should_ignore) {
// If should_ignore is set, // If should_ignore is set,
// we need to clear the public key and other cruft, in addition to setting the node as ignored // we need to clear the public key and other cruft, in addition to setting the node as ignored
info->is_ignored = true; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
info->is_favorite = false; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
info->has_device_metrics = false; eraseNodeSatellites(contact.node_num);
info->has_position = false; info->public_key.size = 0;
info->user.public_key.size = 0; memset(info->public_key.bytes, 0, sizeof(info->public_key.bytes));
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
} else { } else {
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with /* 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! * 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 { } else {
// Normal case: set is_favorite to prevent expiration. // 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). // 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 // 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) { 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. // Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info; updateGUIforNode = info;
@@ -1887,9 +2304,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
return false; 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 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"); LOG_WARN("Public Key mismatch, dropping NodeInfo");
return false; 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 // Always ensure user.id is derived from nodeId, regardless of what was received
snprintf(p.id, sizeof(p.id), "!%08x", nodeId); 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 meshtastic_NodeInfoLite before = *info;
auto lite = TypeConversions::ConvertToUserLite(p); TypeConversions::CopyUserToNodeInfoLite(info, p);
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); 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->public_key.size == 32) {
if (info->user.public_key.size == 32) { printBytes("Saved Pubkey: ", info->public_key.bytes, 32);
printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32);
} }
if (nodeId != getNodeNum()) if (nodeId != getNodeNum())
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) 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->channel);
info->has_user = true;
if (changed) { if (changed) {
updateGUIforNode = info; updateGUIforNode = info;
@@ -1956,7 +2376,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
if (mp.rx_snr) if (mp.rx_snr)
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. 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 // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
const int8_t hopsAway = getHopsAway(mp); 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) void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
{ {
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite && lite->is_favorite != is_favorite) { if (lite && nodeInfoLiteIsFavorite(lite) != is_favorite) {
lite->is_favorite = is_favorite; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite);
sortMeshDB(); sortMeshDB();
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
} }
@@ -1986,10 +2407,10 @@ bool NodeDB::isFavorite(uint32_t nodeId)
if (nodeId == NODENUM_BROADCAST) if (nodeId == NODENUM_BROADCAST)
return false; return false;
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite) { if (lite) {
return lite->is_favorite; return nodeInfoLiteIsFavorite(lite);
} }
return false; return false;
} }
@@ -2014,14 +2435,14 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
lite = &meshNodes->at(i); lite = &meshNodes->at(i);
if (lite->num == p.from) { if (lite->num == p.from) {
if (lite->is_favorite) if (nodeInfoLiteIsFavorite(lite))
return true; return true;
seenFrom = true; seenFrom = true;
} }
if (lite->num == p.to) { if (lite->num == p.to) {
if (lite->is_favorite) if (nodeInfoLiteIsFavorite(lite))
return true; return true;
seenTo = 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 // 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)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true; 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)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true; 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 // noop
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
@@ -2120,17 +2541,18 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
int oldestIndex = -1; int oldestIndex = -1;
int oldestBoringIndex = -1; int oldestBoringIndex = -1;
for (int i = 1; i < numMeshNodes; i++) { 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 // Simply the oldest non-favorite, non-ignored, non-verified node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && oldest = cand->last_heard;
meshNodes->at(i).last_heard < oldest) {
oldest = meshNodes->at(i).last_heard;
oldestIndex = i; oldestIndex = i;
} }
// The oldest "boring" node // The oldest "boring" node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
meshNodes->at(i).last_heard < oldestBoring) { oldestBoring = cand->last_heard;
oldestBoring = meshNodes->at(i).last_heard;
oldestBoringIndex = i; oldestBoringIndex = i;
} }
} }
@@ -2140,6 +2562,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
} }
if (oldestIndex != -1) { if (oldestIndex != -1) {
eraseNodeSatellites(meshNodes->at(oldestIndex).num);
// Shove the remaining nodes down the chain // Shove the remaining nodes down the chain
for (int i = oldestIndex; i < numMeshNodes - 1; i++) { for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
meshNodes->at(i) = meshNodes->at(i + 1); meshNodes->at(i) = meshNodes->at(i + 1);
@@ -2163,18 +2586,23 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
/// valid lat/lon /// valid lat/lon
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) 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 /// If we have a node / user and they report is_licensed = true
/// we consider them licensed /// we consider them licensed
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
{ {
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
if (!info || !info->has_user) { if (!nodeInfoLiteHasUser(info))
return UserLicenseStatus::NotKnown; return UserLicenseStatus::NotKnown;
} return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
} }
#if !defined(MESHTASTIC_EXCLUDE_PKI) #if !defined(MESHTASTIC_EXCLUDE_PKI)
+134 -4
View File
@@ -6,10 +6,12 @@
#include <assert.h> #include <assert.h>
#include <pb_encode.h> #include <pb_encode.h>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include "MeshTypes.h" #include "MeshTypes.h"
#include "NodeStatus.h" #include "NodeStatus.h"
#include "concurrency/Lock.h"
#include "configuration.h" #include "configuration.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode #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_CHANNELS 8
#define SEGMENT_NODEDATABASE 16 #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 #define DEVICESTATE_MIN_VER 24
extern meshtastic_DeviceState devicestate; extern meshtastic_DeviceState devicestate;
@@ -166,6 +170,21 @@ class NodeDB
Observable<const meshtastic::NodeStatus *> newStatus; Observable<const meshtastic::NodeStatus *> newStatus;
pb_size_t numMeshNodes; 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<NodeNum, meshtastic_PositionLite> nodePositions;
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
std::unordered_map<NodeNum, meshtastic_DeviceMetrics> nodeTelemetry;
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
std::unordered_map<NodeNum, meshtastic_EnvironmentMetrics> nodeEnvironment;
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
std::unordered_map<NodeNum, meshtastic_StatusMessage> nodeStatus;
#endif
bool keyIsLowEntropy = false; bool keyIsLowEntropy = false;
bool hasWarned = false; bool hasWarned = false;
@@ -277,6 +296,43 @@ class NodeDB
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
size_t getNumMeshNodes() { return numMeshNodes; } 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<NodeNum> snapshotPositionNodeNums(NodeNum exclude) const;
std::vector<NodeNum> snapshotTelemetryNodeNums(NodeNum exclude) const;
std::vector<NodeNum> snapshotEnvironmentNodeNums(NodeNum exclude) const;
std::vector<NodeNum> 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); UserLicenseStatus getLicenseStatus(uint32_t nodeNum);
size_t getMaxNodesAllocatedSize() size_t getMaxNodesAllocatedSize()
@@ -285,7 +341,11 @@ class NodeDB
emptyNodeDatabase.version = DEVICESTATE_CUR_VER; emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
size_t nodeDatabaseSize; size_t nodeDatabaseSize;
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); 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 // returns true if the maximum number of nodes is reached or we are running low on memory
@@ -329,6 +389,7 @@ class NodeDB
} }
private: private:
mutable concurrency::Lock satelliteMutex;
bool duplicateWarned = false; bool duplicateWarned = false;
bool localPositionUpdatedSinceBoot = false; bool localPositionUpdatedSinceBoot = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
@@ -363,6 +424,11 @@ class NodeDB
bool saveDeviceStateToDisk(); bool saveDeviceStateToDisk();
bool saveNodeDatabaseToDisk(); bool saveNodeDatabaseToDisk();
void sortMeshDB(); 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; extern NodeDB *nodeDB;
@@ -397,10 +463,74 @@ extern meshtastic_CriticalErrorCode error_code;
* A numeric error address (nonzero if available) * A numeric error address (nonzero if available)
*/ */
extern uint32_t error_address; 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_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_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 \ #define Module_Config_size \
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
+129
View File
@@ -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 <algorithm>
#include <cstring>
#include <pb_decode.h>
#include <pb_encode.h>
bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
{
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite_Legacy> *>(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<std::vector<meshtastic_NodeInfoLite_Legacy> *>(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<size_t>(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;
}
+419 -7
View File
@@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig()
onConfigStart(); onConfigStart();
// even if we were already connected - restart our state machine // 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 // If client only wants node info, jump directly to sending nodes
state = STATE_SEND_OWN_NODEINFO; state = STATE_SEND_OWN_NODEINFO;
LOG_INFO("Client only wants node info, skipping other config"); LOG_INFO("Client only wants node info, skipping other config");
@@ -81,6 +81,15 @@ void PhoneAPI::handleStartConfig()
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoForPhone = {}; nodeInfoForPhone = {};
nodeInfoQueue.clear(); nodeInfoQueue.clear();
replayQueue.clear();
replayPositionOrder.clear();
replayTelemetryOrder.clear();
replayEnvironmentOrder.clear();
replayStatusOrder.clear();
replayPositionIndex = 0;
replayTelemetryIndex = 0;
replayEnvironmentIndex = 0;
replayStatusIndex = 0;
} }
resetReadIndex(); resetReadIndex();
} }
@@ -120,6 +129,15 @@ void PhoneAPI::close()
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoForPhone = {}; nodeInfoForPhone = {};
nodeInfoQueue.clear(); nodeInfoQueue.clear();
replayQueue.clear();
replayPositionOrder.clear();
replayTelemetryOrder.clear();
replayEnvironmentOrder.clear();
replayStatusOrder.clear();
replayPositionIndex = 0;
replayTelemetryIndex = 0;
replayEnvironmentIndex = 0;
replayStatusIndex = 0;
} }
packetForPhone = NULL; packetForPhone = NULL;
filesManifest.clear(); filesManifest.clear();
@@ -302,7 +320,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
nodeInfoForPhone.num = 0; 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 // If client only wants node info, jump directly to sending nodes
state = STATE_SEND_OTHER_NODEINFOS; state = STATE_SEND_OTHER_NODEINFOS;
onNowHasData(0); 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()); LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoQueue.clear(); 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; state = STATE_SEND_FILEMANIFEST;
// Go ahead and send that ID right now
return getFromRadio(buf); return getFromRadio(buf);
} }
break; break;
@@ -538,9 +672,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
case STATE_SEND_FILEMANIFEST: { case STATE_SEND_FILEMANIFEST: {
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
// last element // ONLY_NODES variants skip the manifest.
if (config_state == filesManifest.size() || if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES ||
config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
config_state = 0; config_state = 0;
filesManifest.clear(); filesManifest.clear();
// Skip to complete packet // Skip to complete packet
@@ -653,6 +787,7 @@ void PhoneAPI::prefetchNodeInfos()
{ {
bool added = false; bool added = false;
bool wasEmpty = 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. // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
{ {
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
@@ -662,7 +797,8 @@ void PhoneAPI::prefetchNodeInfos()
if (!nextNode) if (!nextNode)
break; break;
auto info = TypeConversions::ConvertToNodeInfo(nextNode); auto info =
gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : TypeConversions::ConvertToNodeInfo(nextNode);
bool isUs = info.num == nodeDB->getNodeNum(); bool isUs = info.num == nodeDB->getNodeNum();
info.hops_away = isUs ? 0 : info.hops_away; info.hops_away = isUs ? 0 : info.hops_away;
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
@@ -682,6 +818,257 @@ void PhoneAPI::prefetchNodeInfos()
onNowHasData(0); 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() void PhoneAPI::releaseMqttClientProxyPhonePacket()
{ {
if (mqttClientProxyMessageForPhone) { if (mqttClientProxyMessageForPhone) {
@@ -728,6 +1115,31 @@ bool PhoneAPI::available()
PREFETCH_NODEINFO: PREFETCH_NODEINFO:
prefetchNodeInfos(); prefetchNodeInfos();
return true; 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: { case STATE_SEND_PACKETS: {
if (!queueStatusPacketForPhone) if (!queueStatusPacketForPhone)
queueStatusPacketForPhone = service->getQueueStatusForPhone(); queueStatusPacketForPhone = service->getQueueStatusForPhone();
+44 -1
View File
@@ -4,12 +4,16 @@
#include "concurrency/Lock.h" #include "concurrency/Lock.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshtastic/portnums.pb.h" #include "meshtastic/portnums.pb.h"
#include <cstdint>
#include <deque> #include <deque>
#include <iterator> #include <iterator>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
// 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 // Make sure that we never let our packets grow too large for one BLE packet
#define MAX_TO_FROM_RADIO_SIZE 512 #define MAX_TO_FROM_RADIO_SIZE 512
@@ -22,6 +26,9 @@
#define SPECIAL_NONCE_ONLY_CONFIG 69420 #define SPECIAL_NONCE_ONLY_CONFIG 69420
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°) #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 * 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_CONFIG, // Replacement for the old Radioconfig
STATE_SEND_MODULECONFIG, // Send Module specific config 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_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_COMPLETE_ID,
STATE_SEND_PACKETS // send packets or debug strings 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. // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
concurrency::Lock nodeInfoMutex; concurrency::Lock nodeInfoMutex;
// Synthetic-packet replay queue (paced via prefetch).
std::deque<meshtastic_MeshPacket> 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<uint32_t> replayPositionOrder;
std::vector<uint32_t> replayTelemetryOrder;
std::vector<uint32_t> replayEnvironmentOrder;
std::vector<uint32_t> replayStatusOrder;
size_t replayPositionIndex = 0;
size_t replayTelemetryIndex = 0;
size_t replayEnvironmentIndex = 0;
size_t replayStatusIndex = 0;
meshtastic_ToRadio toRadioScratch = { meshtastic_ToRadio toRadioScratch = {
0}; // this is a static scratch object, any data must be copied elsewhere before returning 0}; // this is a static scratch object, any data must be copied elsewhere before returning
@@ -137,6 +164,10 @@ class PhoneAPI
bool isConnected() { return state != STATE_SEND_NOTHING; } bool isConnected() { return state != STATE_SEND_NOTHING; }
bool isSendingPackets() { return state == STATE_SEND_PACKETS; } 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: protected:
/// Our fromradio packet while it is being assembled /// Our fromradio packet while it is being assembled
@@ -185,6 +216,18 @@ class PhoneAPI
void releaseQueueStatusPhonePacket(); void releaseQueueStatusPhonePacket();
void prefetchNodeInfos(); 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(); void releaseMqttClientProxyPhonePacket();
+2 -2
View File
@@ -58,7 +58,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
const char *getSenderShortName(const meshtastic_MeshPacket &mp) const char *getSenderShortName(const meshtastic_MeshPacket &mp)
{ {
auto node = nodeDB->getMeshNode(getFrom(&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; return sender;
} }
@@ -121,4 +121,4 @@ template <class T> class ProtobufModule : protected SinglePortModule
return alterReceivedProtobuf(mp, decoded); return alterReceivedProtobuf(mp, decoded);
} }
} }
}; };
+1 -1
View File
@@ -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); 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 && } 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"); LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
routingModule->getHopLimitForResponse(*p)); routingModule->getHopLimitForResponse(*p));
+16 -16
View File
@@ -120,17 +120,17 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
if (!node) if (!node)
continue; continue;
// Check 1: is_favorite (cheapest - single bool) // Check 1: is_favorite (cheapest - single bit test)
if (!node->is_favorite) if (!nodeInfoLiteIsFavorite(node))
continue; continue;
// Check 2: has_user (cheap - single bool) // Check 2: has_user (cheap - single bit test)
if (!node->has_user) if (!nodeInfoLiteHasUser(node))
continue; continue;
// Check 3: role check (moderate cost - multiple comparisons) // Check 3: role check (moderate cost - multiple comparisons)
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
continue; continue;
} }
@@ -433,7 +433,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
concurrency::LockGuard g(cryptLock); concurrency::LockGuard g(cryptLock);
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && 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); LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
return DecodeState::DECODE_FAILURE; return DecodeState::DECODE_FAILURE;
} }
@@ -451,11 +451,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
#if !(MESHTASTIC_EXCLUDE_PKI) #if !(MESHTASTIC_EXCLUDE_PKI)
// Attempt PKI decryption first // Attempt PKI decryption first
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && 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 && nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
rawSize > MESHTASTIC_PKC_OVERHEAD) { nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
LOG_DEBUG("Attempt PKI decryption"); 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)) { bytes)) {
LOG_INFO("PKI Decryption worked!"); LOG_INFO("PKI Decryption worked!");
@@ -467,7 +467,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
decrypted = true; decrypted = true;
LOG_INFO("Packet decrypted using PKI!"); LOG_INFO("Packet decrypted using PKI!");
p->pki_encrypted = true; 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->public_key.size = 32;
p->decoded = decodedtmp; p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded 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) if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE; return meshtastic_Routing_Error_TOO_LARGE;
// Check for a known public key for the destination // 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, LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
p->decoded.portnum); p->decoded.portnum);
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
} }
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && 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, 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; 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; numbytes += MESHTASTIC_PKC_OVERHEAD;
p->channel = 0; p->channel = 0;
p->pki_encrypted = true; p->pki_encrypted = true;
@@ -860,7 +860,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
} }
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); 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); LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
packetPool.release(p); packetPool.release(p);
return; return;
+69 -49
View File
@@ -3,7 +3,9 @@
#include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h"
#include "meshUtils.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; meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
@@ -11,42 +13,56 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
info.snr = lite->snr; info.snr = lite->snr;
info.last_heard = lite->last_heard; info.last_heard = lite->last_heard;
info.channel = lite->channel; info.channel = lite->channel;
info.via_mqtt = lite->via_mqtt; info.via_mqtt = nodeInfoLiteViaMqtt(lite);
info.is_favorite = lite->is_favorite; info.is_favorite = nodeInfoLiteIsFavorite(lite);
info.is_ignored = lite->is_ignored; info.is_ignored = nodeInfoLiteIsIgnored(lite);
info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite);
info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; info.is_muted = nodeInfoLiteIsMuted(lite);
if (lite->has_hops_away) { if (lite->has_hops_away) {
info.has_hops_away = true; info.has_hops_away = true;
info.hops_away = lite->hops_away; info.hops_away = lite->hops_away;
} }
if (lite->has_position) { if (position) {
info.has_position = true; info.has_position = true;
if (lite->position.latitude_i != 0) if (position->latitude_i != 0)
info.position.has_latitude_i = true; info.position.has_latitude_i = true;
info.position.latitude_i = lite->position.latitude_i; info.position.latitude_i = position->latitude_i;
if (lite->position.longitude_i != 0) if (position->longitude_i != 0)
info.position.has_longitude_i = true; info.position.has_longitude_i = true;
info.position.longitude_i = lite->position.longitude_i; info.position.longitude_i = position->longitude_i;
if (lite->position.altitude != 0) if (position->altitude != 0)
info.position.has_altitude = true; info.position.has_altitude = true;
info.position.altitude = lite->position.altitude; info.position.altitude = position->altitude;
info.position.location_source = lite->position.location_source; info.position.location_source = position->location_source;
info.position.time = lite->position.time; info.position.time = position->time;
} }
if (lite->has_user) { if (nodeInfoLiteHasUser(lite)) {
info.has_user = true; 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.has_device_metrics = true;
info.device_metrics = lite->device_metrics; info.device_metrics = *deviceMetrics;
} }
return info; 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 TypeConversions::ConvertToPositionLite(meshtastic_Position position)
{ {
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
@@ -77,46 +93,50 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
return position; 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)); strncpy(lite->long_name, user.long_name, sizeof(lite->long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0'; lite->long_name[sizeof(lite->long_name) - 1] = '\0';
sanitizeUtf8(lite.long_name, sizeof(lite.long_name)); sanitizeUtf8(lite->long_name, sizeof(lite->long_name));
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); strncpy(lite->short_name, user.short_name, sizeof(lite->short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0'; lite->short_name[sizeof(lite->short_name) - 1] = '\0';
sanitizeUtf8(lite.short_name, sizeof(lite.short_name)); sanitizeUtf8(lite->short_name, sizeof(lite->short_name));
lite.hw_model = user.hw_model; lite->hw_model = user.hw_model;
lite.role = user.role; lite->role = user.role;
lite.is_licensed = user.is_licensed; memcpy(lite->public_key.bytes, user.public_key.bytes, sizeof(lite->public_key.bytes));
memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); lite->public_key.size = user.public_key.size;
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);
lite.has_is_unmessagable = user.has_is_unmessagable; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable);
lite.is_unmessagable = user.is_unmessagable; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable && user.is_unmessagable);
return lite; 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; meshtastic_User user = meshtastic_User_init_default;
if (!lite)
return user;
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); snprintf(user.id, sizeof(user.id), "!%08x", lite->num);
strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); strncpy(user.long_name, lite->long_name, sizeof(user.long_name));
user.long_name[sizeof(user.long_name) - 1] = '\0'; user.long_name[sizeof(user.long_name) - 1] = '\0';
sanitizeUtf8(user.long_name, sizeof(user.long_name)); 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'; user.short_name[sizeof(user.short_name) - 1] = '\0';
sanitizeUtf8(user.short_name, sizeof(user.short_name)); sanitizeUtf8(user.short_name, sizeof(user.short_name));
user.hw_model = lite.hw_model; user.hw_model = lite->hw_model;
user.role = lite.role; user.role = lite->role;
user.is_licensed = lite.is_licensed; user.is_licensed = nodeInfoLiteIsLicensed(lite);
memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); memcpy(user.public_key.bytes, lite->public_key.bytes, sizeof(user.public_key.bytes));
memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); user.public_key.size = lite->public_key.size;
user.public_key.size = lite.public_key.size; user.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(lite);
user.has_is_unmessagable = lite.has_is_unmessagable; user.is_unmessagable = nodeInfoLiteIsUnmessagable(lite);
user.is_unmessagable = lite.is_unmessagable; // macaddr is gone from the slim header; zero-fill for old clients that read it.
memset(user.macaddr, 0, sizeof(user.macaddr));
return user; return user;
} }
+9 -2
View File
@@ -7,9 +7,16 @@
class TypeConversions class TypeConversions
{ {
public: 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); 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_PositionLite ConvertToPositionLite(meshtastic_Position position);
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); 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);
}; };
@@ -18,6 +18,18 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO)
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2) 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) PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO)
+128 -43
View File
@@ -63,43 +63,35 @@ typedef struct _meshtastic_UserLite {
bool is_unmessagable; bool is_unmessagable;
} meshtastic_UserLite; } meshtastic_UserLite;
typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t;
typedef struct _meshtastic_NodeInfoLite { typedef struct _meshtastic_NodeInfoLite {
/* The node number */ /* The node number */
uint32_t num; 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, /* 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 */ as measured by the receiver. Return SNR of the last received message in dB */
float snr; float snr;
/* Set to indicate the last time we received a packet from this node */ /* Set to indicate the last time we received a packet from this node */
uint32_t last_heard; 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. */ /* local channel index we heard that node on. Only populated if its not the default channel. */
uint8_t 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) */ /* Number of hops away from us this node is (0 if direct neighbor) */
bool has_hops_away; bool has_hops_away;
uint8_t 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. */ /* 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; uint8_t next_hop;
/* Bitfield for storing booleans. /* Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */
LSB 0 is_key_manually_verified
LSB 1 is_muted */
uint32_t bitfield; 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; } meshtastic_NodeInfoLite;
/* This message is never sent over the wire, but it is used for serializing DB /* 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_NodeRemoteHardwarePin node_remote_hardware_pins[12];
} meshtastic_DeviceState; } 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 { typedef struct _meshtastic_NodeDatabase {
/* A version integer used to invalidate old save files when we make /* 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 incompatible changes This integer is set at build time and is private to
@@ -150,6 +166,12 @@ typedef struct _meshtastic_NodeDatabase {
uint32_t version; uint32_t version;
/* New lite version of NodeDB to decrease memory footprint */ /* New lite version of NodeDB to decrease memory footprint */
std::vector<meshtastic_NodeInfoLite> nodes; std::vector<meshtastic_NodeInfoLite> nodes;
/* Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
these via MESHTASTIC_EXCLUDE_*DB build flags. */
std::vector<meshtastic_NodePositionEntry> positions;
std::vector<meshtastic_NodeTelemetryEntry> telemetry;
std::vector<meshtastic_NodeStatusEntry> status;
std::vector<meshtastic_NodeEnvironmentEntry> environment;
} meshtastic_NodeDatabase; } meshtastic_NodeDatabase;
/* The on-disk saved channels */ /* The on-disk saved channels */
@@ -191,16 +213,24 @@ extern "C" {
/* Initializer values for message structs */ /* Initializer values for message structs */
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #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_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_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_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_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_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_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_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_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} #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_public_key_tag 7
#define meshtastic_UserLite_is_unmessagable_tag 9 #define meshtastic_UserLite_is_unmessagable_tag 9
#define meshtastic_NodeInfoLite_num_tag 1 #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_snr_tag 4
#define meshtastic_NodeInfoLite_last_heard_tag 5 #define meshtastic_NodeInfoLite_last_heard_tag 5
#define meshtastic_NodeInfoLite_device_metrics_tag 6
#define meshtastic_NodeInfoLite_channel_tag 7 #define meshtastic_NodeInfoLite_channel_tag 7
#define meshtastic_NodeInfoLite_via_mqtt_tag 8
#define meshtastic_NodeInfoLite_hops_away_tag 9 #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_next_hop_tag 12
#define meshtastic_NodeInfoLite_bitfield_tag 13 #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_my_node_tag 2
#define meshtastic_DeviceState_owner_tag 3 #define meshtastic_DeviceState_owner_tag 3
#define meshtastic_DeviceState_receive_queue_tag 5 #define meshtastic_DeviceState_receive_queue_tag 5
@@ -240,8 +269,20 @@ extern "C" {
#define meshtastic_DeviceState_did_gps_reset_tag 11 #define meshtastic_DeviceState_did_gps_reset_tag 11
#define meshtastic_DeviceState_rx_waypoint_tag 12 #define meshtastic_DeviceState_rx_waypoint_tag 12
#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13 #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_version_tag 1
#define meshtastic_NodeDatabase_nodes_tag 2 #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_channels_tag 1
#define meshtastic_ChannelFile_version_tag 2 #define meshtastic_ChannelFile_version_tag 2
#define meshtastic_BackupPreferences_version_tag 1 #define meshtastic_BackupPreferences_version_tag 1
@@ -275,23 +316,19 @@ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9)
#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \ #define meshtastic_NodeInfoLite_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \ 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, FLOAT, snr, 4) \
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ 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, UINT32, channel, 7) \
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ 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, 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_CALLBACK NULL
#define meshtastic_NodeInfoLite_DEFAULT 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) \ #define meshtastic_DeviceState_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \ 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_rx_waypoint_MSGTYPE meshtastic_MeshPacket
#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin #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) \ #define meshtastic_NodeDatabase_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, version, 1) \ 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); 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_CALLBACK meshtastic_NodeDatabase_callback
#define meshtastic_NodeDatabase_DEFAULT NULL #define meshtastic_NodeDatabase_DEFAULT NULL
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite #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) \ #define meshtastic_ChannelFile_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \ 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_UserLite_msg;
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg; extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
extern const pb_msgdesc_t meshtastic_DeviceState_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_NodeDatabase_msg;
extern const pb_msgdesc_t meshtastic_ChannelFile_msg; extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
extern const pb_msgdesc_t meshtastic_BackupPreferences_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_UserLite_fields &meshtastic_UserLite_msg
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg #define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_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_NodeDatabase_fields &meshtastic_NodeDatabase_msg
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg #define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_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_BackupPreferences_size 2432
#define meshtastic_ChannelFile_size 718 #define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737 #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_PositionLite_size 28
#define meshtastic_UserLite_size 98 #define meshtastic_UserLite_size 98
@@ -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)
@@ -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 <pb.h>
#include <vector>
#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<meshtastic_NodeInfoLite_Legacy> 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
+16 -16
View File
@@ -729,7 +729,7 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
uint32_t readIndex = 0; uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) { while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user) { if (nodeInfoLiteHasUser(tempNodeInfo)) {
JSONObject node; JSONObject node;
char id[16]; char id[16];
@@ -737,26 +737,26 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
node["id"] = new JSONValue(id); node["id"] = new JSONValue(id);
node["snr"] = new JSONValue(tempNodeInfo->snr); 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["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
node["position"] = new JSONValue(); node["position"] = new JSONValue();
if (nodeDB->hasValidPosition(tempNodeInfo)) { if (nodeDB->hasValidPosition(tempNodeInfo)) {
JSONObject position; meshtastic_PositionLite posLite;
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7); if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7); JSONObject position;
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude); position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
node["position"] = new JSONValue(position); 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["long_name"] = new JSONValue(tempNodeInfo->long_name);
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name); node["short_name"] = new JSONValue(tempNodeInfo->short_name);
char macStr[18]; // mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0], node["mac_address"] = new JSONValue("00:00:00:00:00:00");
tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3], node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
node["mac_address"] = new JSONValue(macStr);
node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
nodesArray.push_back(new JSONValue(node)); nodesArray.push_back(new JSONValue(node));
} }
@@ -930,4 +930,4 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
res->print(jsonString.c_str()); res->print(jsonString.c_str());
delete value; delete value;
} }
#endif #endif
+37 -3
View File
@@ -37,9 +37,43 @@
#define MAX_RX_NOTIFICATION_TOPHONE 2 #define MAX_RX_NOTIFICATION_TOPHONE 2
#endif #endif
/// Verify baseline assumption of node size. If it increases, we need to reevaluate /// Tighten this when the slim header shrinks; loosen only with deliberate
/// the impact of its memory footprint, notably on MAX_NUM_NODES. /// awareness of MAX_NUM_NODES impact per platform.
static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); 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 /// max number of nodes allowed in the nodeDB
#ifndef MAX_NUM_NODES #ifndef MAX_NUM_NODES
+15 -14
View File
@@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
// Automatically favorite the node that is using the admin key // Automatically favorite the node that is using the admin key
auto remoteNode = nodeDB->getMeshNode(mp.from); 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) { 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 // 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. // without the user doing so deliberately.
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
} else { } else {
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
remoteNode->is_favorite = true; nodeInfoLiteSetBit(remoteNode, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
} }
} }
} else { } else {
@@ -393,7 +393,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received set_favorite_node command"); LOG_INFO("Client received set_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
if (node != NULL) { if (node != NULL) {
node->is_favorite = true; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
if (screen) if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens 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"); LOG_INFO("Client received remove_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
if (node != NULL) { if (node != NULL) {
node->is_favorite = false; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
if (screen) if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens 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"); LOG_INFO("Client received set_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
if (node != NULL) { if (node != NULL) {
node->is_ignored = true; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
node->has_device_metrics = false; nodeDB->eraseNodeSatellites(node->num);
node->has_position = false; node->public_key.size = 0;
node->user.public_key.size = 0; memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -428,7 +427,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received remove_ignored_node command"); LOG_INFO("Client received remove_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
if (node != NULL) { if (node != NULL) {
node->is_ignored = false; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -437,7 +436,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received toggle_muted_node command"); LOG_INFO("Client received toggle_muted_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
if (node != NULL) { if (node != NULL) {
node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_MUTED_MASK, !nodeInfoLiteIsMuted(node));
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -445,9 +444,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
case meshtastic_AdminMessage_set_fixed_position_tag: { case meshtastic_AdminMessage_set_fixed_position_tag: {
LOG_INFO("Client received set_fixed_position command"); LOG_INFO("Client received set_fixed_position command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
node->has_position = true; // Route the fixed position through updatePosition so it lands in the
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); // 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); nodeDB->setLocalPosition(r->set_fixed_position);
config.position.fixed_position = true; config.position.fixed_position = true;
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
+12 -12
View File
@@ -141,7 +141,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
static bool returnToCannedList = false; static bool returnToCannedList = false;
bool hasKeyForNode(const meshtastic_NodeInfoLite *node) 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 * @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) { for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(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; continue;
const String &nodeName = node->user.long_name; const String &nodeName = node->long_name;
if (searchQuery.length() == 0) { if (searchQuery.length() == 0) {
this->filteredNodes.push_back({node, sinceLastSeen(node)}); this->filteredNodes.push_back({node, sinceLastSeen(node)});
@@ -1054,7 +1054,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
} }
NodeNum myNodeNum = nodeDB->getNodeNum(); 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->pki_encrypted = true;
p->channel = 0; // force PKI p->channel = 0; // force PKI
} }
@@ -1415,8 +1415,8 @@ const char *CannedMessageModule::getNodeName(NodeNum node)
return "Broadcast"; return "Broadcast";
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user && strlen(info->user.long_name) > 0) { if (nodeInfoLiteHasUser(info) && strlen(info->long_name) > 0) {
return info->user.long_name; return info->long_name;
} }
static char fallback[12]; static char fallback[12];
@@ -1723,17 +1723,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
if (node) { if (node) {
if (display->getWidth() <= 64) { if (display->getWidth() <= 64) {
entryText = node->user.short_name; entryText = node->short_name;
} else if (node->user.long_name[0]) { } else if (node->long_name[0]) {
entryText = node->user.long_name; entryText = node->long_name;
} else { } else {
entryText = node->user.short_name; entryText = node->short_name;
} }
} }
int availWidth = display->getWidth() - int availWidth = display->getWidth() -
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
((node && node->is_favorite) ? 10 : 0); ((nodeInfoLiteIsFavorite(node)) ? 10 : 0);
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
char truncatedEntry[96]; char truncatedEntry[96];
@@ -1742,7 +1742,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
entryText = truncatedEntry; entryText = truncatedEntry;
// Prepend "* " if this is a favorite // Prepend "* " if this is a favorite
if (node && node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
entryText = "* " + entryText; entryText = "* " + entryText;
} }
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
+1 -1
View File
@@ -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 // If we receive a direct message and the receipent is us, apply DM mute setting
// Else we just handle it as not muted. // Else we just handle it as not muted.
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); 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); : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
const bool buzzerModeIsDirectOnly = const bool buzzerModeIsDirectOnly =
+31 -17
View File
@@ -10,6 +10,18 @@
KeyVerificationModule *keyVerificationModule; 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() KeyVerificationModule::KeyVerificationModule()
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) : 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 && } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
request->key_verification.nonce == currentNonce) { request->key_verification.nonce == currentNonce) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); 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(); resetToIdle();
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
resetToIdle(); resetToIdle();
@@ -72,9 +85,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
sprintf(cn->message, "Enter Security Number for Key Verification"); sprintf(cn->message, "Enter Security Number for Key Verification");
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
cn->payload_variant.key_verification_number_request.nonce = currentNonce; 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 copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_request.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(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); service->sendClientNotification(cn);
LOG_INFO("Received hash2"); LOG_INFO("Received hash2");
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
@@ -95,7 +108,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
[=](int selected) { [=](int selected) {
if (selected == 1) { if (selected == 1) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); 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);) 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); sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce; cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(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; cn->payload_variant.key_verification_final.isSender = false;
service->sendClientNotification(cn); service->sendClientNotification(cn);
@@ -202,9 +216,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
currentSecurityNumber % 1000); currentSecurityNumber % 1000);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
cn->payload_variant.key_verification_number_inform.nonce = currentNonce; 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 copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_inform.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(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; cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
service->sendClientNotification(cn); service->sendClientNotification(cn);
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); 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); LOG_WARN("received security number: %u", incomingNumber);
meshtastic_NodeInfoLite *remoteNodePtr = nullptr; meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); 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; currentState = KEY_VERIFICATION_IDLE;
return; // should we throw an error here? return; // should we throw an error here?
} }
@@ -232,7 +246,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
hash.update(&currentRemoteNode, sizeof(currentRemoteNode)); hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(owner.public_key.bytes, owner.public_key.size); 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.finalize(hash1, 32);
hash.reset(); hash.reset();
@@ -265,9 +279,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce; cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(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; cn->payload_variant.key_verification_final.isSender = true;
service->sendClientNotification(cn); service->sendClientNotification(cn);
LOG_INFO(message); 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. readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
} }
} }
#endif #endif
+23 -13
View File
@@ -183,8 +183,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
return nullptr; return nullptr;
} }
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position const meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
assert(node->has_position);
// configuration of POSITION packet // configuration of POSITION packet
// consider making this a function argument? // 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 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 is totally empty, put our last saved position (lite) in there
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { 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++; localPosition.seq_number++;
@@ -420,7 +421,7 @@ int32_t PositionModule::runOnce()
doDeepSleep(nightyNightMs, false, false); doDeepSleep(nightyNightMs, false, false);
} }
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node == nullptr) if (node == nullptr)
return RUNONCE_INTERVAL; return RUNONCE_INTERVAL;
@@ -445,8 +446,11 @@ int32_t PositionModule::runOnce()
} else if (nodeDB->hasValidPosition(node)) { } else if (nodeDB->hasValidPosition(node)) {
lastGpsSend = now; lastGpsSend = now;
lastGpsLatitude = node->position.latitude_i; meshtastic_PositionLite selfPos;
lastGpsLongitude = node->position.longitude_i; if (nodeDB->copyNodePosition(node->num, selfPos)) {
lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = selfPos.longitude_i;
}
if (transmitHistory) if (transmitHistory)
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
@@ -461,7 +465,10 @@ int32_t PositionModule::runOnce()
if (nodeDB->hasValidPosition(node2)) { if (nodeDB->hasValidPosition(node2)) {
// The minimum time (in seconds) that would pass before we are able to send a new position packet. // 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; msSinceLastSend = now - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold && if (smartPosition.hasTraveledOverThreshold &&
@@ -479,8 +486,8 @@ int32_t PositionModule::runOnce()
msSinceLastSend, minimumTimeThreshold); msSinceLastSend, minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send // Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i; lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = node->position.longitude_i; lastGpsLongitude = selfPos.longitude_i;
} }
} }
} }
@@ -563,11 +570,14 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic
void PositionModule::handleNewPosition() 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 const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
// We limit our GPS broadcasts to a max rate // We limit our GPS broadcasts to a max rate
if (nodeDB->hasValidPosition(node2)) { 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; uint32_t msSinceLastSend = millis() - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold && if (smartPosition.hasTraveledOverThreshold &&
Throttle::execute( Throttle::execute(
@@ -583,8 +593,8 @@ void PositionModule::handleNewPosition()
minimumTimeThreshold); minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send // Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i; lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = node->position.longitude_i; lastGpsLongitude = selfPos.longitude_i;
} }
} }
} }
+21 -11
View File
@@ -268,26 +268,36 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
fileToAppend.printf("??:??:??,"); // Time fileToAppend.printf("??:??:??,"); // Time
} }
fileToAppend.printf("%d,", getFrom(&mp)); // From fileToAppend.printf("%d,", getFrom(&mp)); // From
fileToAppend.printf("%s,", n->user.long_name); // Long Name fileToAppend.printf("%s,", n ? n->long_name : ""); // Long Name
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat meshtastic_PositionLite senderPos;
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long 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) { if (gpsStatus->getIsConnected() || config.position.fixed_position) {
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
} else { } else {
// When the phone API is in use, the node info will be updated with position // When the phone API is in use, the node info will be updated with position
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_PositionLite usPos;
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat if (nodeDB->copyNodePosition(nodeDB->getNodeNum(), usPos)) {
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long fileToAppend.printf("%f,", usPos.latitude_i * 1e-7); // RX Lat
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude 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 fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { if (haveSenderPos && senderPos.latitude_i && senderPos.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, float distance = GeoCoord::latLongToMeter(senderPos.latitude_i * 1e-7, senderPos.longitude_i * 1e-7,
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
fileToAppend.printf("%f,", distance); // Distance in meters fileToAppend.printf("%f,", distance); // Distance in meters
} else { } else {
@@ -340,4 +350,4 @@ bool RangeTestModuleRadio::removeFile()
return 0; return 0;
#endif #endif
} }
+1 -2
View File
@@ -17,8 +17,7 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
if (!maybePKI) if (!maybePKI)
return false; return false;
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && if (!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.from)) && !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.to)))
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
return false; return false;
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) || } else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) { (nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
+15 -8
View File
@@ -250,9 +250,12 @@ int32_t SerialModule::runOnce()
uint32_t readIndex = 0; uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) { while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { if (nodeInfoLiteHasUser(tempNodeInfo) && nodeDB->hasValidPosition(tempNodeInfo)) {
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); meshtastic_PositionLite pos;
serialPrint->printf("%s", outbuf); if (nodeDB->copyNodePosition(tempNodeInfo->num, pos)) {
printWPL(outbuf, sizeof(outbuf), pos, tempNodeInfo->long_name, true);
serialPrint->printf("%s", outbuf);
}
} }
tempNodeInfo = nodeDB->readNextMeshNode(readIndex); tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
} }
@@ -401,7 +404,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
serialPrint->write(p.payload.bytes, p.payload.size); serialPrint->write(p.payload.bytes, p.payload.size);
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); 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->println();
serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->printf("%s: %s", sender, p.payload.bytes);
serialPrint->println(); serialPrint->println();
@@ -417,9 +420,13 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
decoded = &scratch; decoded = &scratch;
} }
// send position packet as WPL to the serial port // 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); meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp));
serialPrint->printf("%s", outbuf); 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 #endif
return; return;
} }
#endif #endif
+3 -10
View File
@@ -2,6 +2,7 @@
#include "StatusMessageModule.h" #include "StatusMessageModule.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h"
#include "ProtobufModule.h" #include "ProtobufModule.h"
StatusMessageModule *statusMessageModule; StatusMessageModule *statusMessageModule;
@@ -36,16 +37,8 @@ ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
RecentStatus entry; if (nodeDB)
entry.fromNodeId = mp.from; nodeDB->setNodeStatus(mp.from, incomingMessage);
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
}
} }
} }
return ProcessMessage::CONTINUE; return ProcessMessage::CONTINUE;
+3 -18
View File
@@ -2,8 +2,6 @@
#if !MESHTASTIC_EXCLUDE_STATUS #if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h" #include "SinglePortModule.h"
#include "configuration.h" #include "configuration.h"
#include <string>
#include <vector>
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{ {
@@ -20,29 +18,16 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
this->setInterval(1000 * 12 * 60 * 60); this->setInterval(1000 * 12 * 60 * 60);
} }
// TODO: If we have a string, set the initial delay (15 minutes maybe) // 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; virtual int32_t runOnce() override;
struct RecentStatus {
uint32_t fromNodeId; // mp.from
std::string statusText; // incomingMessage.status
};
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
protected: 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; virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
std::vector<RecentStatus> recentReceived;
}; };
extern StatusMessageModule *statusMessageModule; extern StatusMessageModule *statusMessageModule;
#endif #endif
@@ -515,6 +515,10 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
packetPool.release(lastMeasurementPacket); packetPool.release(lastMeasurementPacket);
lastMeasurementPacket = packetPool.allocCopy(mp); 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 return false; // Let others look at this message also if they want
+5 -5
View File
@@ -492,12 +492,12 @@ TraceRouteModule::TraceRouteModule()
const char *TraceRouteModule::getNodeName(NodeNum node) const char *TraceRouteModule::getNodeName(NodeNum node)
{ {
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user) { if (nodeInfoLiteHasUser(info)) {
if (strlen(info->user.short_name) > 0) { if (strlen(info->short_name) > 0) {
return info->user.short_name; return info->short_name;
} }
if (strlen(info->user.long_name) > 0) { if (strlen(info->long_name) > 0) {
return info->user.long_name; return info->long_name;
} }
} }
+2 -2
View File
@@ -1226,9 +1226,9 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke
// Fallback only when PSRAM cache is unavailable on this target. // Fallback only when PSRAM cache is unavailable on this target.
// In this mode we use the node-wide table maintained by NodeInfoModule. // In this mode we use the node-wide table maintained by NodeInfoModule.
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
if (!node || !node->has_user) if (!nodeInfoLiteHasUser(node))
return false; return false;
cachedUser = TypeConversions::ConvertToUser(node->num, node->user); cachedUser = TypeConversions::ConvertToUser(node);
} }
if (!sendResponse) if (!sendResponse)
+5 -3
View File
@@ -100,7 +100,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
char distStr[20] = ""; char distStr[20] = "";
// Get our node, to use our own position // 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. // Match compass sizing/placement to favorite node screen logic.
const int w = display->getWidth(); const int w = display->getWidth();
@@ -147,8 +147,10 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
const char *statusLine2 = nullptr; const char *statusLine2 = nullptr;
// Distance only needs our own position fix; compass/bearing additionally needs heading. // Distance only needs our own position fix; compass/bearing additionally needs heading.
if (hasOwnPositionFix) { meshtastic_PositionLite ownPos;
const meshtastic_PositionLite &op = ourNode->position; const bool haveOwnPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ownPos);
if (hasOwnPositionFix && haveOwnPos) {
const meshtastic_PositionLite &op = ownPos;
const float d = const float d =
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
+1 -1
View File
@@ -141,7 +141,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); 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 // 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 // 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()); router->enqueueReceivedMessage(p.release());
} else if (router && } else if (router &&
perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key
+9 -5
View File
@@ -319,10 +319,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
// Lambda function for adding a long name to the route // Lambda function for adding a long name to the route
auto addToRoute = [](JSONArray *route, NodeNum num) { auto addToRoute = [](JSONArray *route, NodeNum num) {
char long_name[40] = "Unknown"; char long_name[40] = "Unknown";
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
bool name_known = node ? node->has_user : false; bool name_known = nodeInfoLiteHasUser(node);
if (name_known) if (name_known) {
memcpy(long_name, node->user.long_name, sizeof(long_name)); 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)); route->push_back(new JSONValue(long_name));
}; };
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) 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; delete value;
return jsonStr; return jsonStr;
} }
#endif #endif
@@ -292,9 +292,13 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
auto addToRoute = [](JsonArray *route, NodeNum num) { auto addToRoute = [](JsonArray *route, NodeNum num) {
char long_name[40] = "Unknown"; char long_name[40] = "Unknown";
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
bool name_known = node ? node->has_user : false; bool name_known = nodeInfoLiteHasUser(node);
if (name_known) if (name_known) {
memcpy(long_name, node->user.long_name, sizeof(long_name)); 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); route->add(long_name);
}; };
@@ -418,4 +422,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
return jsonStr; return jsonStr;
} }
#endif #endif
+1 -1
View File
@@ -113,7 +113,7 @@ void test_DH25519(void)
void test_PKC(void) void test_PKC(void)
{ {
uint8_t private_key[32]; 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_shared[32];
uint8_t expected_decrypted[32]; uint8_t expected_decrypted[32];
uint8_t radioBytes[128] __attribute__((__aligned__)); uint8_t radioBytes[128] __attribute__((__aligned__));
+6 -26
View File
@@ -641,30 +641,12 @@ void test_sender_zero_substituted(void)
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
} }
void test_uninitialized_wasSeenRecently(void) // 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
// Simulate uninitialized state — create a PacketHistory that looks uninitialized // the object and then call methods on it, which is UB and trips ASAN under
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one // `pio test -e coverage`. In practice the guard can only fire if `new[]` throws
PacketHistory h(4); // (in which case the constructor doesn't return), so the lost coverage is
TEST_ASSERT_TRUE(h.initOk()); // sanity check // purely defensive.
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);
}
void test_multiple_instances_independent(void) void test_multiple_instances_independent(void)
{ {
@@ -819,8 +801,6 @@ void setup()
// Group 10 — Edge Cases // Group 10 — Edge Cases
RUN_TEST(test_packet_id_zero_not_stored); RUN_TEST(test_packet_id_zero_not_stored);
RUN_TEST(test_sender_zero_substituted); RUN_TEST(test_sender_zero_substituted);
RUN_TEST(test_uninitialized_wasSeenRecently);
RUN_TEST(test_uninitialized_wasRelayer);
RUN_TEST(test_multiple_instances_independent); RUN_TEST(test_multiple_instances_independent);
// Group 11 — Hash Table Stress // Group 11 — Hash Table Stress
+4 -4
View File
@@ -50,7 +50,7 @@ class MockNodeDB : public NodeDB
hasCachedNode = true; hasCachedNode = true;
cachedNodeNum = n; cachedNodeNum = n;
cachedNode.num = n; cachedNode.num = n;
cachedNode.has_user = true; cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
} }
private: private:
@@ -494,9 +494,9 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result)); TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
TEST_ASSERT_NOT_NULL(requestor); TEST_ASSERT_NOT_NULL(requestor);
TEST_ASSERT_TRUE(requestor->has_user); TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name);
TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name);
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
} }
+406
View File
@@ -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 <cstdio>
#include <cstring>
#include <unity.h>
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<char>(0xF0); // emoji lead byte at position 22
input[23] = static_cast<char>(0x9F); // continuation
input[24] = static_cast<char>(0xA4); // continuation - past the cap
input[25] = static_cast<char>(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() {}