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:
co-authored by
GitHub
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Copilot Autofix powered by AI
parent
1bcabb893b
commit
94bb21ecc7
@@ -135,6 +135,95 @@ On top of authorization, any remote admin message that **mutates** state (not a
|
||||
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
|
||||
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
|
||||
|
||||
## NodeDB Layout (v25)
|
||||
|
||||
`DEVICESTATE_CUR_VER = 25`, `DEVICESTATE_MIN_VER = 24`. The on-device NodeDB was split in v25 into a slim header table plus four optional satellite stores. Older v24 saves auto-migrate at boot. Old training-data instincts (`node->user.long_name`, `node->position.latitude_i`, `node->is_favorite`, `node->device_metrics.battery_level`) are wrong now — the fields aren't there. Read this section before touching anything that walks `nodeDB->meshNodes`.
|
||||
|
||||
### Slim `NodeInfoLite`
|
||||
|
||||
`UserLite` is flattened onto `NodeInfoLite` (no nested sub-message); `position` and `device_metrics` are removed entirely (tags reserved). MAC address is dropped. Long names are capped at 25 chars (`max_size:25` in `deviceonly.options`); `hw_model` and `role` are `int_size:8`. Encoded size dropped from ~166 B → ~105 B per node.
|
||||
|
||||
Booleans are bit-packed into `NodeInfoLite.bitfield`. **Do not read or write the bits directly** — use the inline helpers in `src/mesh/NodeDB.h`:
|
||||
|
||||
```cpp
|
||||
nodeInfoLiteHasUser(n) // bit 5 — user fields populated
|
||||
nodeInfoLiteIsFavorite(n) // bit 3
|
||||
nodeInfoLiteIsIgnored(n) // bit 4
|
||||
nodeInfoLiteIsMuted(n) // bit 1
|
||||
nodeInfoLiteIsLicensed(n) // bit 6 — Ham mode peer
|
||||
nodeInfoLiteIsKeyManuallyVerified(n) // bit 0
|
||||
nodeInfoLiteHasIsUnmessagable(n) // bit 8 — "is_unmessagable was sent"
|
||||
nodeInfoLiteIsUnmessagable(n) // bit 7
|
||||
// via_mqtt is bit 2 (mask exposed; predicate uses the mask directly)
|
||||
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); // setter
|
||||
```
|
||||
|
||||
### Satellite stores
|
||||
|
||||
Four `std::unordered_map<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
|
||||
|
||||
```
|
||||
|
||||
+1
-1
Submodule protobufs updated: 1d6f1a71ff...149586802f
+3
-6
@@ -53,8 +53,7 @@ class GPSStatus : public Status
|
||||
int32_t getLatitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.latitude_i;
|
||||
return localPosition.latitude_i;
|
||||
} else {
|
||||
return p.latitude_i;
|
||||
}
|
||||
@@ -63,8 +62,7 @@ class GPSStatus : public Status
|
||||
int32_t getLongitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.longitude_i;
|
||||
return localPosition.longitude_i;
|
||||
} else {
|
||||
return p.longitude_i;
|
||||
}
|
||||
@@ -73,8 +71,7 @@ class GPSStatus : public Status
|
||||
int32_t getAltitude() const
|
||||
{
|
||||
if (config.position.fixed_position) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
return node->position.altitude;
|
||||
return localPosition.altitude;
|
||||
} else {
|
||||
return p.altitude;
|
||||
}
|
||||
|
||||
@@ -1285,7 +1285,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
}
|
||||
}
|
||||
@@ -1670,7 +1670,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
const meshtastic_Channel channel =
|
||||
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
|
||||
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
|
||||
const char *longName = nodeInfoLiteHasUser(node) ? node->long_name : nullptr;
|
||||
|
||||
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
|
||||
|
||||
|
||||
@@ -845,10 +845,10 @@ void menuHandler::messageViewModeMenu()
|
||||
// Encode peers
|
||||
for (size_t i = 0; i < uniquePeers.size(); ++i) {
|
||||
uint32_t peer = uniquePeers[i];
|
||||
auto node = nodeDB->getMeshNode(peer);
|
||||
const auto *node = nodeDB->getMeshNode(peer);
|
||||
std::string name;
|
||||
if (node && node->has_user)
|
||||
name = sanitizeString(node->user.long_name).substr(0, 15);
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
name = sanitizeString(node->long_name).substr(0, 15);
|
||||
else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Node %08X", peer);
|
||||
@@ -1355,14 +1355,14 @@ void menuHandler::manageNodeMenu()
|
||||
static int optionsEnumArray[enumEnd] = {Back};
|
||||
int options = 1;
|
||||
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
optionsArray[options] = "Unfavorite";
|
||||
} else {
|
||||
optionsArray[options] = "Favorite";
|
||||
}
|
||||
optionsEnumArray[options++] = Favorite;
|
||||
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
if (isMuted) {
|
||||
optionsArray[options] = "Unmute Notifications";
|
||||
} else {
|
||||
@@ -1376,7 +1376,7 @@ void menuHandler::manageNodeMenu()
|
||||
optionsArray[options] = "Key Verification";
|
||||
optionsEnumArray[options++] = KeyVerification;
|
||||
|
||||
if (node->is_ignored) {
|
||||
if (nodeInfoLiteIsIgnored(node)) {
|
||||
optionsArray[options] = "Unignore Node";
|
||||
} else {
|
||||
optionsArray[options] = "Ignore Node";
|
||||
@@ -1386,8 +1386,8 @@ void menuHandler::manageNodeMenu()
|
||||
BannerOverlayOptions bannerOptions;
|
||||
|
||||
std::string title = "";
|
||||
if (node->has_user && node->user.long_name && node->user.long_name[0]) {
|
||||
title += sanitizeString(node->user.long_name).substr(0, 15);
|
||||
if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
|
||||
title += sanitizeString(node->long_name).substr(0, 15);
|
||||
} else {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
|
||||
@@ -1409,7 +1409,7 @@ void menuHandler::manageNodeMenu()
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
if (n->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(n)) {
|
||||
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
|
||||
nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
@@ -1426,13 +1426,9 @@ void menuHandler::manageNodeMenu()
|
||||
return;
|
||||
}
|
||||
|
||||
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) {
|
||||
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
|
||||
}
|
||||
const bool wasMuted = nodeInfoLiteIsMuted(n);
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
|
||||
LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
|
||||
nodeDB->notifyObservers(true);
|
||||
nodeDB->saveToDisk();
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
@@ -1461,11 +1457,11 @@ void menuHandler::manageNodeMenu()
|
||||
return;
|
||||
}
|
||||
|
||||
if (n->is_ignored) {
|
||||
n->is_ignored = false;
|
||||
if (nodeInfoLiteIsIgnored(n)) {
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
|
||||
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
|
||||
} else {
|
||||
n->is_ignored = true;
|
||||
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
||||
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
|
||||
}
|
||||
nodeDB->notifyObservers(true);
|
||||
@@ -2079,9 +2075,9 @@ void menuHandler::removeFavoriteMenu()
|
||||
static const char *optionsArray[] = {"Back", "Yes"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
std::string message = "Unfavorite This Node?\n";
|
||||
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
if (node && node->has_user) {
|
||||
message += sanitizeString(node->user.long_name).substr(0, 15);
|
||||
const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
message += sanitizeString(node->long_name).substr(0, 15);
|
||||
}
|
||||
bannerOptions.message = message.c_str();
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
|
||||
@@ -462,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
}
|
||||
case ThreadMode::DIRECT: {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
|
||||
if (node && node->has_user && node->user.short_name[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name);
|
||||
if (nodeInfoLiteHasUser(node) && node->short_name[0]) {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name);
|
||||
} else {
|
||||
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
|
||||
}
|
||||
@@ -585,11 +585,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
|
||||
|
||||
char senderName[64] = "";
|
||||
if (node && node->has_user) {
|
||||
if (node->user.long_name[0]) {
|
||||
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1);
|
||||
} else if (node->user.short_name[0]) {
|
||||
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
if (node->long_name[0]) {
|
||||
strncpy(senderName, node->long_name, sizeof(senderName) - 1);
|
||||
} else if (node->short_name[0]) {
|
||||
strncpy(senderName, node->short_name, sizeof(senderName) - 1);
|
||||
}
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
}
|
||||
@@ -599,18 +599,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
// If this is *our own* message, override senderName to who the recipient was
|
||||
bool mine = (m.sender == nodeDB->getNodeNum());
|
||||
if (mine && node_recipient && node_recipient->has_user) {
|
||||
if (node_recipient->user.long_name[0]) {
|
||||
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1);
|
||||
if (mine && nodeInfoLiteHasUser(node_recipient)) {
|
||||
if (node_recipient->long_name[0]) {
|
||||
strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1);
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
} else if (node_recipient->user.short_name[0]) {
|
||||
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1);
|
||||
} else if (node_recipient->short_name[0]) {
|
||||
strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1);
|
||||
senderName[sizeof(senderName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
|
||||
if (mine && (!node_recipient || !node_recipient->has_user ||
|
||||
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
|
||||
if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) {
|
||||
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
|
||||
}
|
||||
|
||||
@@ -1073,12 +1072,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
|
||||
// Banner logic
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
|
||||
char longName[64] = "?";
|
||||
if (node && node->has_user) {
|
||||
if (node->user.long_name[0]) {
|
||||
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
if (node->long_name[0]) {
|
||||
strncpy(longName, node->long_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
} else if (node->user.short_name[0]) {
|
||||
strncpy(longName, node->user.short_name, sizeof(longName) - 1);
|
||||
} else if (node->short_name[0]) {
|
||||
strncpy(longName, node->short_name, sizeof(longName) - 1);
|
||||
longName[sizeof(longName) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,29 +98,23 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
// If long-name mode is enabled, and we have a recent status for this node,
|
||||
// prefer "(short_name) statusText" as the raw candidate.
|
||||
// prefer "(short_name) statusText" as the raw candidate. Pull straight out
|
||||
// of NodeDB's per-NodeNum cache instead of scanning a FIFO.
|
||||
std::string composedFromStatus;
|
||||
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
const char *shortName = node->user.short_name;
|
||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
|
||||
if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) {
|
||||
meshtastic_StatusMessage cachedStatus;
|
||||
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
|
||||
const char *shortName = node->short_name;
|
||||
const size_t statusLen = std::strlen(cachedStatus.status);
|
||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen);
|
||||
composedFromStatus += "(";
|
||||
if (shortName && *shortName) {
|
||||
composedFromStatus += shortName;
|
||||
}
|
||||
composedFromStatus += ") ";
|
||||
composedFromStatus += found->statusText;
|
||||
composedFromStatus += cachedStatus.status;
|
||||
|
||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||
}
|
||||
@@ -129,8 +123,8 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// If we didn't compose from status, use normal long/short selection
|
||||
if (!raw) {
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
raw = config.display.use_long_node_name ? node->long_name : node->short_name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +212,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
||||
int16_t nameX, int16_t y, int nameMaxWidth)
|
||||
{
|
||||
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) {
|
||||
if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -259,7 +253,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -279,14 +273,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -321,20 +315,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -401,15 +395,19 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||
double lat1 = ourNode->position.latitude_i * 1e-7;
|
||||
double lon1 = ourNode->position.longitude_i * 1e-7;
|
||||
double lat2 = node->position.latitude_i * 1e-7;
|
||||
double lon2 = node->position.longitude_i * 1e-7;
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourPos;
|
||||
meshtastic_PositionLite theirPos;
|
||||
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
|
||||
const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos);
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveOurPos && haveTheirPos) {
|
||||
double lat1 = ourPos.latitude_i * 1e-7;
|
||||
double lon1 = ourPos.longitude_i * 1e-7;
|
||||
double lat2 = theirPos.latitude_i * 1e-7;
|
||||
double lon2 = theirPos.longitude_i * 1e-7;
|
||||
|
||||
double earthRadiusKm = 6371.0;
|
||||
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||
@@ -455,14 +453,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -509,19 +507,19 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
bool isMuted = nodeInfoLiteIsMuted(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
|
||||
if (node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
if (node->is_ignored || isMuted) {
|
||||
if (nodeInfoLiteIsIgnored(node) || isMuted) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
|
||||
} else {
|
||||
@@ -542,8 +540,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||
|
||||
double nodeLat = node->position.latitude_i * 1e-7;
|
||||
double nodeLon = node->position.longitude_i * 1e-7;
|
||||
meshtastic_PositionLite nodePos;
|
||||
if (!nodeDB->copyNodePosition(node->num, nodePos))
|
||||
return;
|
||||
double nodeLat = nodePos.latitude_i * 1e-7;
|
||||
double nodeLon = nodePos.longitude_i * 1e-7;
|
||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||
@@ -652,7 +653,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
continue;
|
||||
if (n->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
if (locationScreen && !n->has_position)
|
||||
if (locationScreen && !nodeDB->hasNodePosition(n->num))
|
||||
continue;
|
||||
|
||||
drawList.push_back(n->num);
|
||||
@@ -883,14 +884,15 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float headingRadian = 0.0f;
|
||||
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
|
||||
const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourSelfPos;
|
||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
double lat = DegD(ourNode->position.latitude_i);
|
||||
double lon = DegD(ourNode->position.longitude_i);
|
||||
double lat = DegD(ourSelfPos.latitude_i);
|
||||
double lon = DegD(ourSelfPos.longitude_i);
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
|
||||
@@ -317,12 +317,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||
char tempName[48] = {0};
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
|
||||
if (node && node->has_user) {
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
const char *rawName = nullptr;
|
||||
if (node->user.long_name[0]) {
|
||||
rawName = node->user.long_name;
|
||||
} else if (node->user.short_name[0]) {
|
||||
rawName = node->user.short_name;
|
||||
if (node->long_name[0]) {
|
||||
rawName = node->long_name;
|
||||
} else if (node->short_name[0]) {
|
||||
rawName = node->short_name;
|
||||
}
|
||||
if (rawName) {
|
||||
const int arrowWidth = (currentResolution == ScreenResolution::High)
|
||||
|
||||
@@ -464,11 +464,11 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset,
|
||||
return true;
|
||||
}
|
||||
|
||||
static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const std::string &statusText)
|
||||
static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const char *statusText)
|
||||
{
|
||||
// Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down.
|
||||
char rawStatus[96];
|
||||
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str());
|
||||
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText ? statusText : "");
|
||||
|
||||
char clippedStatus[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth());
|
||||
@@ -495,7 +495,7 @@ void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (!n || n->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
if (n->is_favorite)
|
||||
if (nodeInfoLiteIsFavorite(n))
|
||||
favoritedNodes.push_back(n);
|
||||
}
|
||||
|
||||
@@ -750,7 +750,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
return;
|
||||
|
||||
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
||||
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
||||
if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node))
|
||||
return;
|
||||
display->clear();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
@@ -763,7 +763,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
#endif
|
||||
currentFavoriteNodeNum = node->num;
|
||||
// === Create the shortName and title string ===
|
||||
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node";
|
||||
const char *shortName = (nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "Node";
|
||||
char titlestr[40];
|
||||
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
|
||||
|
||||
@@ -781,9 +781,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
// === 1. Long Name (always try to show first) ===
|
||||
const char *username;
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
|
||||
username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->short_name : nullptr;
|
||||
} else {
|
||||
username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
|
||||
username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : nullptr;
|
||||
}
|
||||
|
||||
// Print node's long name (e.g. "Backpack Node")
|
||||
@@ -796,23 +796,14 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||
}
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
// === Optional: Last received StatusMessage line for this node ===
|
||||
// Display it directly under the username line (if we have one).
|
||||
if (statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
|
||||
// Search newest-to-oldest
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], found->statusText);
|
||||
// Display it directly under the username line (if we have one). The cache
|
||||
// lives on NodeDB now, keyed by NodeNum, so this is an O(1) lookup.
|
||||
if (nodeDB) {
|
||||
meshtastic_StatusMessage cachedStatus;
|
||||
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
|
||||
drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus.status);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -971,25 +962,30 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === 4. Uptime (only show if metric is present) ===
|
||||
char uptimeStr[32] = "";
|
||||
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
|
||||
meshtastic_DeviceMetrics nodeMetrics;
|
||||
const bool haveNodeMetrics = nodeDB->copyNodeTelemetry(node->num, nodeMetrics);
|
||||
if (haveNodeMetrics && nodeMetrics.has_uptime_seconds) {
|
||||
char upPrefix[12]; // enough for leftSideSpacing + "Up:"
|
||||
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
|
||||
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
|
||||
getUptimeStr(nodeMetrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
|
||||
}
|
||||
if (uptimeStr[0]) {
|
||||
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
|
||||
}
|
||||
|
||||
// === 5. Distance (only if both nodes have GPS position) ===
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
char distStr[24] = ""; // Make buffer big enough for any string
|
||||
bool haveDistance = false;
|
||||
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||
meshtastic_PositionLite nodePos;
|
||||
meshtastic_PositionLite ourPos;
|
||||
const bool haveNodePos = nodeDB->copyNodePosition(node->num, nodePos);
|
||||
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveNodePos && haveOurPos) {
|
||||
// Use shared meter conversion, then format display units with lightweight integer rounding.
|
||||
const float distanceMeters =
|
||||
GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i),
|
||||
DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i));
|
||||
const float distanceMeters = GeoCoord::latLongToMeter(DegD(nodePos.latitude_i), DegD(nodePos.longitude_i),
|
||||
DegD(ourPos.latitude_i), DegD(ourPos.longitude_i));
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f);
|
||||
if (feet > 0 && feet < 1000) {
|
||||
@@ -1024,19 +1020,19 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
char batLine[32] = "";
|
||||
bool haveBatLine = false;
|
||||
|
||||
if (node->has_device_metrics) {
|
||||
bool hasPct = node->device_metrics.has_battery_level;
|
||||
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f;
|
||||
if (haveNodeMetrics) {
|
||||
bool hasPct = nodeMetrics.has_battery_level;
|
||||
bool hasVolt = nodeMetrics.has_voltage && nodeMetrics.voltage > 0.001f;
|
||||
|
||||
int pct = 0;
|
||||
float volt = 0.0f;
|
||||
|
||||
if (hasPct) {
|
||||
pct = (int)node->device_metrics.battery_level;
|
||||
pct = (int)nodeMetrics.battery_level;
|
||||
}
|
||||
|
||||
if (hasVolt) {
|
||||
volt = node->device_metrics.voltage;
|
||||
volt = nodeMetrics.voltage;
|
||||
}
|
||||
|
||||
if (hasPct && pct > 0 && pct <= 100) {
|
||||
@@ -1076,11 +1072,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
|
||||
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
if (hasOwnPositionFix && hasNodePositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
if (hasOwnPositionFix && hasNodePositionFix && haveOurPos && haveNodePos) {
|
||||
const auto &op = ourPos;
|
||||
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||
if (showCompass) {
|
||||
const auto &p = node->position;
|
||||
const auto &p = nodePos;
|
||||
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
||||
} else {
|
||||
@@ -1130,7 +1126,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// === Header ===
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
@@ -1270,7 +1266,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int textWidth = 0;
|
||||
int nameX = 0;
|
||||
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
|
||||
const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : "";
|
||||
const char *longName = (nodeInfoLiteHasUser(ourNode) && ourNode->long_name[0]) ? ourNode->long_name : "";
|
||||
const char *shortName = owner.short_name ? owner.short_name : "";
|
||||
char combinedName[96];
|
||||
if (longName[0] && shortName[0]) {
|
||||
@@ -1597,7 +1593,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
||||
int32_t(gpsStatus->getAltitude()));
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const bool hasLiveGpsFix =
|
||||
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
||||
@@ -1613,9 +1609,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
headingLat = DegD(gpsStatus->getLatitude());
|
||||
headingLon = DegD(gpsStatus->getLongitude());
|
||||
} else if (hasOwnPositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
headingLat = DegD(op.latitude_i);
|
||||
headingLon = DegD(op.longitude_i);
|
||||
meshtastic_PositionLite ownPos;
|
||||
if (nodeDB->copyNodePosition(ourNode->num, ownPos)) {
|
||||
headingLat = DegD(ownPos.latitude_i);
|
||||
headingLon = DegD(ownPos.longitude_i);
|
||||
}
|
||||
}
|
||||
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
||||
}
|
||||
|
||||
@@ -362,8 +362,8 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
|
||||
assert(node);
|
||||
|
||||
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
|
||||
if (node->has_user) {
|
||||
std::string parsed = parse(node->user.short_name);
|
||||
if (nodeInfoLiteHasUser(node)) {
|
||||
std::string parsed = parse(node->short_name);
|
||||
if (isPrintable(parsed))
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -136,9 +136,10 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
||||
|
||||
// Draw our node LAST with full white fill + outline
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourSelfPos;
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
||||
Marker self = calculateMarker(ourSelfPos.latitude_i * 1e-7, ourSelfPos.longitude_i * 1e-7, false, 0);
|
||||
|
||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||
@@ -168,10 +169,11 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
{
|
||||
// If we have a valid position for our own node, use that as the anchor
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
*lat = ourNode->position.latitude_i * 1e-7;
|
||||
*lng = ourNode->position.longitude_i * 1e-7;
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
meshtastic_PositionLite ourSelfPos;
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
|
||||
*lat = ourSelfPos.latitude_i * 1e-7;
|
||||
*lng = ourSelfPos.longitude_i * 1e-7;
|
||||
} else {
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
@@ -201,9 +203,13 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
meshtastic_PositionLite pos;
|
||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
||||
continue;
|
||||
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float latRad = pos.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = pos.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
@@ -300,13 +306,17 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
meshtastic_PositionLite pos;
|
||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
||||
continue;
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float latNode = node->position.latitude_i * 1e-7;
|
||||
float latNode = pos.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, latNode);
|
||||
southernmost = min(southernmost, latNode);
|
||||
|
||||
// Longitude is trickier
|
||||
float lngNode = node->position.longitude_i * 1e-7;
|
||||
float lngNode = pos.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
@@ -372,10 +382,13 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Find x and y position based on node's position in nodeDB
|
||||
assert(nodeDB->hasValidPosition(node));
|
||||
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
meshtastic_PositionLite pos;
|
||||
const bool hasPos = nodeDB->copyNodePosition(node->num, pos);
|
||||
assert(hasPos);
|
||||
Marker m = calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
);
|
||||
|
||||
// Convert to pixel coords
|
||||
@@ -516,13 +529,16 @@ void InkHUD::MapApplet::calculateAllMarkers()
|
||||
if (node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
|
||||
meshtastic_PositionLite pos;
|
||||
if (!nodeDB->copyNodePosition(node->num, pos))
|
||||
continue;
|
||||
|
||||
// Calculate marker and store it
|
||||
markers.push_back(
|
||||
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
));
|
||||
markers.push_back(calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,21 +50,23 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
|
||||
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||
|
||||
// Assemble info: from nodeDB (needed to detect changes)
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node) {
|
||||
if (node->has_hops_away)
|
||||
c.hopsAway = node->hops_away;
|
||||
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
meshtastic_PositionLite ourPos;
|
||||
meshtastic_PositionLite theirPos;
|
||||
if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
|
||||
float ourLat = ourPos.latitude_i * 1e-7;
|
||||
float ourLong = ourPos.longitude_i * 1e-7;
|
||||
float theirLat = theirPos.latitude_i * 1e-7;
|
||||
float theirLong = theirPos.longitude_i * 1e-7;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +181,8 @@ void InkHUD::NodeListApplet::onRender(bool full)
|
||||
// -- Longname --
|
||||
// Parse special chars in long name
|
||||
// Use node id if unknown
|
||||
if (node && node->has_user)
|
||||
longName = parse(node->user.long_name); // Found in nodeDB
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
longName = parse(node->long_name); // Found in nodeDB
|
||||
else {
|
||||
// Not found in nodeDB, show a hex nodeid instead
|
||||
longName = hexifyNodeNum(nodeNum);
|
||||
|
||||
@@ -2090,7 +2090,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
|
||||
// Count favorites
|
||||
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||
if (nodeDB->getMeshNodeByIndex(i)->is_favorite)
|
||||
if (nodeInfoLiteIsFavorite(nodeDB->getMeshNodeByIndex(i)))
|
||||
favoriteCount++;
|
||||
}
|
||||
|
||||
@@ -2098,10 +2098,10 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
// Don't want some monstrous list that takes 100 clicks to reach exit
|
||||
if (favoriteCount < 20) {
|
||||
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip node if not a favorite
|
||||
if (!node->is_favorite)
|
||||
if (!nodeInfoLiteIsFavorite(node))
|
||||
continue;
|
||||
|
||||
CannedMessages::RecipientItem r;
|
||||
@@ -2111,8 +2111,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
|
||||
// Set a label for the menu item
|
||||
r.label = "DM: ";
|
||||
if (node->has_user)
|
||||
r.label += parse(node->user.long_name);
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
r.label += parse(node->long_name);
|
||||
else
|
||||
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
text += msgIsBroadcast ? "From:" : "DM: ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
@@ -255,7 +255,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
|
||||
text += msgIsBroadcast ? "Msg from " : "DM from ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
if (nodeInfoLiteHasUser(node))
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
@@ -70,10 +70,10 @@ void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
// - short name and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
||||
if (sender && sender->has_user) {
|
||||
if (nodeInfoLiteHasUser(sender)) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += parse(sender->long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(message->sender);
|
||||
|
||||
@@ -66,10 +66,10 @@ void InkHUD::DMApplet::onRender(bool full)
|
||||
// - shortname and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
|
||||
if (sender && sender->has_user) {
|
||||
if (nodeInfoLiteHasUser(sender)) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += parse(sender->long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(latestMessage->dm.sender);
|
||||
|
||||
@@ -8,7 +8,7 @@ using namespace NicheGraphics;
|
||||
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Keep our own node available as map anchor/center; all others must be favorited.
|
||||
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite);
|
||||
return node && (node->num == nodeDB->getNodeNum() || nodeInfoLiteIsFavorite(node));
|
||||
}
|
||||
|
||||
void InkHUD::FavoritesMapApplet::onRender(bool full)
|
||||
@@ -25,7 +25,7 @@ void InkHUD::FavoritesMapApplet::onRender(bool full)
|
||||
|
||||
// Draw our latest "node of interest" as a special marker.
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
if (node && nodeInfoLiteIsFavorite(node) && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
|
||||
} else {
|
||||
// For non-local packets, this applet only reacts to favorited nodes.
|
||||
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
|
||||
if (!sender || !sender->is_favorite)
|
||||
if (!nodeInfoLiteIsFavorite(sender))
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check if this position is from someone different than our previous position packet.
|
||||
|
||||
@@ -80,8 +80,8 @@ void InkHUD::HeardApplet::populateFromNodeDB()
|
||||
ordered.resize(maxCards());
|
||||
|
||||
// Create card info for these (stale) node observations
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
for (meshtastic_NodeInfoLite *node : ordered) {
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
for (const meshtastic_NodeInfoLite *node : ordered) {
|
||||
CardInfo c;
|
||||
c.nodeNum = node->num;
|
||||
|
||||
@@ -89,14 +89,16 @@ void InkHUD::HeardApplet::populateFromNodeDB()
|
||||
c.hopsAway = node->hops_away;
|
||||
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
meshtastic_PositionLite ourPos;
|
||||
meshtastic_PositionLite theirPos;
|
||||
if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
|
||||
float ourLat = ourPos.latitude_i * 1e-7;
|
||||
float ourLong = ourPos.longitude_i * 1e-7;
|
||||
float theirLat = theirPos.latitude_i * 1e-7;
|
||||
float theirLong = theirPos.longitude_i * 1e-7;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into the card collection (member of base class)
|
||||
@@ -122,4 +124,4 @@ std::string InkHUD::HeardApplet::getHeaderText()
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -112,7 +112,7 @@ bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, mes
|
||||
* @param bytes Buffer containing plaintext input.
|
||||
* @param bytesOut Output buffer to be populated with encrypted ciphertext.
|
||||
*/
|
||||
bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
||||
bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
|
||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||
{
|
||||
uint8_t *auth;
|
||||
@@ -152,7 +152,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
|
||||
* @param bytes Buffer containing ciphertext input.
|
||||
* @param bytesOut Output buffer to be populated with decrypted plaintext.
|
||||
*/
|
||||
bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum,
|
||||
bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
|
||||
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
|
||||
{
|
||||
const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text?
|
||||
|
||||
@@ -40,9 +40,12 @@ class CryptoEngine
|
||||
|
||||
#endif
|
||||
void setDHPrivateKey(uint8_t *_private_key);
|
||||
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic,
|
||||
// The remotePublic key parameter takes the public_key bytes container from
|
||||
// a stored node header. NodeInfoLite is the on-device storage type since
|
||||
// the slim refactor flattened UserLite into it.
|
||||
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
|
||||
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
||||
virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum,
|
||||
virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
|
||||
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||
|
||||
+12
-12
@@ -94,8 +94,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
|
||||
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
|
||||
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
|
||||
// ignore our request for its NodeInfo
|
||||
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user &&
|
||||
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) {
|
||||
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster &&
|
||||
!nodeDB->isFull()) {
|
||||
if (airTime->isTxAllowedChannelUtil(true)) {
|
||||
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
|
||||
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
|
||||
@@ -392,19 +393,16 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
assert(node);
|
||||
|
||||
// We might not have a position yet for our local node, in that case, at least try to send the time
|
||||
if (!node->has_position) {
|
||||
memset(&node->position, 0, sizeof(node->position));
|
||||
node->has_position = true;
|
||||
}
|
||||
|
||||
meshtastic_PositionLite &position = node->position;
|
||||
|
||||
// Update our local node info with our time (even if we don't decide to update anyone else)
|
||||
node->last_heard =
|
||||
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid
|
||||
|
||||
position.time = getValidTime(RTCQualityFromNet);
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
// Make sure our own NodeNum has a slot in the position map so subsequent
|
||||
// updates (and the bundled NodeInfo emission to the phone) have somewhere
|
||||
// to read from. Insert a default-zero entry on first call.
|
||||
nodeDB->touchNodePositionTime(node->num, getValidTime(RTCQualityFromNet));
|
||||
#endif
|
||||
|
||||
if (powerStatus->getHasBattery() == 1) {
|
||||
updateBatteryLevel(powerStatus->getBatteryChargePercent());
|
||||
@@ -432,7 +430,9 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
|
||||
// Used fixed position if configured regardless of GPS lock
|
||||
if (config.position.fixed_position) {
|
||||
LOG_WARN("Use fixed position");
|
||||
pos = TypeConversions::ConvertToPosition(node->position);
|
||||
meshtastic_PositionLite fixedSlot;
|
||||
if (nodeDB->copyNodePosition(node->num, fixedSlot))
|
||||
pos = TypeConversions::ConvertToPosition(fixedSlot);
|
||||
}
|
||||
|
||||
// Add a fresh timestamp
|
||||
|
||||
+553
-125
@@ -22,6 +22,7 @@
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/NeighborInfoModule.h"
|
||||
#include <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
|
||||
|
||||
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field)
|
||||
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
||||
{
|
||||
if (ostream) {
|
||||
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
||||
for (auto item : *vec) {
|
||||
if (!pb_encode_tag_for_field(ostream, field))
|
||||
return false;
|
||||
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item);
|
||||
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
||||
switch (iter->tag) {
|
||||
case meshtastic_NodeDatabase_nodes_tag: {
|
||||
if (ostream) {
|
||||
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
||||
for (auto item : *vec) {
|
||||
if (!pb_encode_tag_for_field(ostream, iter))
|
||||
return false;
|
||||
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodeInfoLite node;
|
||||
auto *vec = static_cast<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) {
|
||||
meshtastic_NodeInfoLite node; // this gets good data
|
||||
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
||||
|
||||
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
||||
vec->push_back(node);
|
||||
case meshtastic_NodeDatabase_positions_tag: {
|
||||
if (ostream) {
|
||||
const auto *vec = static_cast<const std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
||||
for (auto item : *vec) {
|
||||
if (!pb_encode_tag_for_field(ostream, iter))
|
||||
return false;
|
||||
if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (istream) {
|
||||
meshtastic_NodePositionEntry entry;
|
||||
auto *vec = static_cast<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
|
||||
@@ -298,8 +378,7 @@ NodeDB::NodeDB()
|
||||
#endif
|
||||
// Include our owner in the node db under our nodenum
|
||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
||||
info->user = TypeConversions::ConvertToUserLite(owner);
|
||||
info->has_user = true;
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, owner);
|
||||
|
||||
// If node database has not been saved for the first time, save it now
|
||||
#ifdef FSCom
|
||||
@@ -442,8 +521,12 @@ NodeDB::NodeDB()
|
||||
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
|
||||
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
|
||||
config.has_position = true;
|
||||
info->has_position = true;
|
||||
info->position = TypeConversions::ConvertToPositionLite(fixedGPS);
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
{
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS);
|
||||
}
|
||||
#endif
|
||||
nodeDB->setLocalPosition(fixedGPS);
|
||||
config.position.fixed_position = true;
|
||||
#endif
|
||||
@@ -479,6 +562,29 @@ bool isBroadcast(uint32_t dest)
|
||||
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template <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)
|
||||
{
|
||||
if (is_fresh_install) {
|
||||
@@ -548,6 +654,19 @@ void NodeDB::installDefaultNodeDatabase()
|
||||
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
|
||||
numMeshNodes = 0;
|
||||
meshNodes = &nodeDatabase.nodes;
|
||||
concurrency::LockGuard satelliteGuard(&satelliteMutex);
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
nodePositions.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
nodeTelemetry.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
nodeEnvironment.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
nodeStatus.clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
@@ -1019,12 +1138,14 @@ void NodeDB::resetNodes(bool keepFavorites)
|
||||
{
|
||||
if (!config.position.fixed_position)
|
||||
clearLocalPosition();
|
||||
NodeNum ourNum = getNodeNum();
|
||||
numMeshNodes = 1;
|
||||
if (keepFavorites) {
|
||||
LOG_INFO("Clearing node database - preserving favorites");
|
||||
for (size_t i = 0; i < meshNodes->size(); i++) {
|
||||
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
||||
if (i > 0 && !node.is_favorite) {
|
||||
if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
|
||||
eraseNodeSatellites(node.num);
|
||||
node = meshtastic_NodeInfoLite();
|
||||
} else {
|
||||
numMeshNodes += 1;
|
||||
@@ -1032,8 +1153,14 @@ void NodeDB::resetNodes(bool keepFavorites)
|
||||
};
|
||||
} else {
|
||||
LOG_INFO("Clearing node database - removing favorites");
|
||||
for (size_t i = 1; i < meshNodes->size(); i++) {
|
||||
const NodeNum gone = meshNodes->at(i).num;
|
||||
if (gone)
|
||||
eraseNodeSatellites(gone);
|
||||
}
|
||||
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
||||
}
|
||||
(void)ourNum;
|
||||
devicestate.has_rx_text_message = false;
|
||||
devicestate.has_rx_waypoint = false;
|
||||
saveNodeDatabaseToDisk();
|
||||
@@ -1054,36 +1181,173 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
||||
numMeshNodes -= removed;
|
||||
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
||||
meshtastic_NodeInfoLite());
|
||||
if (removed)
|
||||
eraseNodeSatellites(nodeNum);
|
||||
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
||||
saveNodeDatabaseToDisk();
|
||||
}
|
||||
|
||||
void NodeDB::clearLocalPosition()
|
||||
{
|
||||
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum());
|
||||
node->position.latitude_i = 0;
|
||||
node->position.longitude_i = 0;
|
||||
node->position.altitude = 0;
|
||||
node->position.time = 0;
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
nodePositions.erase(getNodeNum());
|
||||
#endif
|
||||
setLocalPosition(meshtastic_Position_init_default);
|
||||
localPositionUpdatedSinceBoot = false;
|
||||
}
|
||||
|
||||
bool NodeDB::copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
(void)n;
|
||||
(void)out;
|
||||
return false;
|
||||
#else
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
return copySatelliteEntry(nodePositions, n, out);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NodeDB::copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
(void)n;
|
||||
(void)out;
|
||||
return false;
|
||||
#else
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
return copySatelliteEntry(nodeTelemetry, n, out);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NodeDB::copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
(void)n;
|
||||
(void)out;
|
||||
return false;
|
||||
#else
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
return copySatelliteEntry(nodeEnvironment, n, out);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool NodeDB::copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
||||
(void)n;
|
||||
(void)out;
|
||||
return false;
|
||||
#else
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
return copySatelliteEntry(nodeStatus, n, out);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<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()
|
||||
{
|
||||
int newPos = 0, removed = 0;
|
||||
for (int i = 0; i < numMeshNodes; i++) {
|
||||
if (meshNodes->at(i).has_user) {
|
||||
if (meshNodes->at(i).user.public_key.size > 0) {
|
||||
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) {
|
||||
meshNodes->at(i).user.public_key.size = 0;
|
||||
meshtastic_NodeInfoLite &n = meshNodes->at(i);
|
||||
if (nodeInfoLiteHasUser(&n)) {
|
||||
if (n.public_key.size > 0) {
|
||||
if (memfll(n.public_key.bytes, 0, n.public_key.size)) {
|
||||
n.public_key.size = 0;
|
||||
}
|
||||
}
|
||||
if (newPos != i)
|
||||
meshNodes->at(newPos++) = meshNodes->at(i);
|
||||
meshNodes->at(newPos++) = n;
|
||||
else
|
||||
newPos++;
|
||||
} else {
|
||||
// No user info - drop this node and its satellites
|
||||
const NodeNum gone = n.num;
|
||||
if (gone)
|
||||
eraseNodeSatellites(gone);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
@@ -1141,14 +1405,22 @@ void NodeDB::pickNewNodeNum()
|
||||
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
|
||||
}
|
||||
|
||||
// Identity check via public key (or "empty slot?" when no keys yet);
|
||||
// macaddr no longer lives on the slim header.
|
||||
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
|
||||
if (!n)
|
||||
return false;
|
||||
if (owner.public_key.size == 32 && n->public_key.size == 32)
|
||||
return memcmp(n->public_key.bytes, owner.public_key.bytes, 32) == 0;
|
||||
return !nodeInfoLiteHasUser(n);
|
||||
};
|
||||
|
||||
meshtastic_NodeInfoLite *found;
|
||||
while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) ||
|
||||
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
|
||||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
|
||||
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
|
||||
if (found)
|
||||
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so "
|
||||
"trying for 0x%x",
|
||||
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
|
||||
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
|
||||
nodeNum = candidate;
|
||||
}
|
||||
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
|
||||
@@ -1169,7 +1441,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
||||
if (f) {
|
||||
LOG_INFO("Load %s", filename);
|
||||
pb_istream_t stream = {&readcb, &f, protoSize};
|
||||
if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object
|
||||
if (fields != &meshtastic_NodeDatabase_msg &&
|
||||
fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members
|
||||
memset(dest_struct, 0, objSize);
|
||||
if (!pb_decode(&stream, fields, dest_struct)) {
|
||||
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
||||
@@ -1242,9 +1515,57 @@ void NodeDB::loadFromDisk()
|
||||
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
|
||||
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
|
||||
installDefaultNodeDatabase();
|
||||
} else if (nodeDatabase.version < DEVICESTATE_CUR_VER) {
|
||||
if (migrateLegacyNodeDatabase())
|
||||
saveNodeDatabaseToDisk();
|
||||
else
|
||||
installDefaultNodeDatabase();
|
||||
} else {
|
||||
meshNodes = &nodeDatabase.nodes;
|
||||
numMeshNodes = nodeDatabase.nodes.size();
|
||||
// Hydrate the satellite maps; the on-disk vectors stay empty in steady
|
||||
// state and are repopulated only at save time.
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
nodePositions.clear();
|
||||
nodePositions.reserve(nodeDatabase.positions.size());
|
||||
for (const auto &entry : nodeDatabase.positions) {
|
||||
if (entry.has_position)
|
||||
nodePositions[entry.num] = entry.position;
|
||||
}
|
||||
nodeDatabase.positions.clear();
|
||||
nodeDatabase.positions.shrink_to_fit();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
nodeTelemetry.clear();
|
||||
nodeTelemetry.reserve(nodeDatabase.telemetry.size());
|
||||
for (const auto &entry : nodeDatabase.telemetry) {
|
||||
if (entry.has_device_metrics)
|
||||
nodeTelemetry[entry.num] = entry.device_metrics;
|
||||
}
|
||||
nodeDatabase.telemetry.clear();
|
||||
nodeDatabase.telemetry.shrink_to_fit();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
nodeEnvironment.clear();
|
||||
nodeEnvironment.reserve(nodeDatabase.environment.size());
|
||||
for (const auto &entry : nodeDatabase.environment) {
|
||||
if (entry.has_environment_metrics)
|
||||
nodeEnvironment[entry.num] = entry.environment_metrics;
|
||||
}
|
||||
nodeDatabase.environment.clear();
|
||||
nodeDatabase.environment.shrink_to_fit();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
nodeStatus.clear();
|
||||
nodeStatus.reserve(nodeDatabase.status.size());
|
||||
for (const auto &entry : nodeDatabase.status) {
|
||||
if (entry.has_status)
|
||||
nodeStatus[entry.num] = entry.status;
|
||||
}
|
||||
nodeDatabase.status.clear();
|
||||
nodeDatabase.status.shrink_to_fit();
|
||||
#endif
|
||||
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
|
||||
}
|
||||
|
||||
@@ -1272,16 +1593,16 @@ void NodeDB::loadFromDisk()
|
||||
|
||||
// Attempt recovery of owner fields from our own NodeDB entry if available.
|
||||
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
|
||||
if (us && us->has_user) {
|
||||
if (nodeInfoLiteHasUser(us)) {
|
||||
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
||||
us->num);
|
||||
memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name));
|
||||
memcpy(owner.long_name, us->long_name, sizeof(owner.long_name));
|
||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
||||
memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name));
|
||||
memcpy(owner.short_name, us->short_name, sizeof(owner.short_name));
|
||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
||||
owner.is_licensed = us->user.is_licensed;
|
||||
owner.has_is_unmessagable = us->user.has_is_unmessagable;
|
||||
owner.is_unmessagable = us->user.is_unmessagable;
|
||||
owner.is_licensed = nodeInfoLiteIsLicensed(us);
|
||||
owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us);
|
||||
owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us);
|
||||
|
||||
// Save the recovered owner to device state on disk
|
||||
saveToDisk(SEGMENT_DEVICESTATE);
|
||||
@@ -1528,9 +1849,75 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
// Project the maps into the on-disk vectors just before encoding; cleared
|
||||
// again on the way out so we don't carry duplicate state.
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
nodeDatabase.positions.clear();
|
||||
nodeDatabase.positions.reserve(nodePositions.size());
|
||||
for (const auto &kv : nodePositions) {
|
||||
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_default;
|
||||
entry.num = kv.first;
|
||||
entry.has_position = true;
|
||||
entry.position = kv.second;
|
||||
nodeDatabase.positions.push_back(entry);
|
||||
}
|
||||
#else
|
||||
nodeDatabase.positions.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
nodeDatabase.telemetry.clear();
|
||||
nodeDatabase.telemetry.reserve(nodeTelemetry.size());
|
||||
for (const auto &kv : nodeTelemetry) {
|
||||
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_default;
|
||||
entry.num = kv.first;
|
||||
entry.has_device_metrics = true;
|
||||
entry.device_metrics = kv.second;
|
||||
nodeDatabase.telemetry.push_back(entry);
|
||||
}
|
||||
#else
|
||||
nodeDatabase.telemetry.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
nodeDatabase.environment.clear();
|
||||
nodeDatabase.environment.reserve(nodeEnvironment.size());
|
||||
for (const auto &kv : nodeEnvironment) {
|
||||
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_default;
|
||||
entry.num = kv.first;
|
||||
entry.has_environment_metrics = true;
|
||||
entry.environment_metrics = kv.second;
|
||||
nodeDatabase.environment.push_back(entry);
|
||||
}
|
||||
#else
|
||||
nodeDatabase.environment.clear();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
||||
nodeDatabase.status.clear();
|
||||
nodeDatabase.status.reserve(nodeStatus.size());
|
||||
for (const auto &kv : nodeStatus) {
|
||||
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_default;
|
||||
entry.num = kv.first;
|
||||
entry.has_status = true;
|
||||
entry.status = kv.second;
|
||||
nodeDatabase.status.push_back(entry);
|
||||
}
|
||||
#else
|
||||
nodeDatabase.status.clear();
|
||||
#endif
|
||||
|
||||
size_t nodeDatabaseSize;
|
||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
|
||||
return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
||||
bool ok = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
||||
|
||||
nodeDatabase.positions.clear();
|
||||
nodeDatabase.positions.shrink_to_fit();
|
||||
nodeDatabase.telemetry.clear();
|
||||
nodeDatabase.telemetry.shrink_to_fit();
|
||||
nodeDatabase.environment.clear();
|
||||
nodeDatabase.environment.shrink_to_fit();
|
||||
nodeDatabase.status.clear();
|
||||
nodeDatabase.status.shrink_to_fit();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
@@ -1699,7 +2086,7 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
|
||||
|
||||
// FIXME this implementation is kinda expensive
|
||||
for (int i = 0; i < numMeshNodes; i++) {
|
||||
if (localOnly && meshNodes->at(i).via_mqtt)
|
||||
if (localOnly && nodeInfoLiteViaMqtt(&meshNodes->at(i)))
|
||||
continue;
|
||||
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
|
||||
numseen++;
|
||||
@@ -1736,62 +2123,94 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
|
||||
return;
|
||||
}
|
||||
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
// Build flag opted out: header still tracks last_heard via updateFrom; we
|
||||
// simply don't cache the position payload anywhere on this device.
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
// Local packet, fully authoritative
|
||||
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
|
||||
LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i,
|
||||
p.altitude);
|
||||
|
||||
setLocalPosition(p);
|
||||
info->position = TypeConversions::ConvertToPositionLite(p);
|
||||
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
|
||||
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
|
||||
// (stop-gap fix for issue #900)
|
||||
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
|
||||
info->position.time = p.time;
|
||||
} else {
|
||||
// Be careful to only update fields that have been set by the REMOTE sender
|
||||
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
|
||||
// recorded based on the packet rxTime
|
||||
//
|
||||
// FIXME perhaps handle RX_SRC_USER separately?
|
||||
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
|
||||
|
||||
// First, back up fields that we want to protect from overwrite
|
||||
uint32_t tmp_time = info->position.time;
|
||||
|
||||
// Next, update atomically
|
||||
info->position = TypeConversions::ConvertToPositionLite(p);
|
||||
|
||||
// Last, restore any fields that may have been overwritten
|
||||
if (!info->position.time)
|
||||
info->position.time = tmp_time;
|
||||
}
|
||||
info->has_position = true;
|
||||
(void)nodeId;
|
||||
updateGUIforNode = info;
|
||||
notifyObservers(true);
|
||||
#else
|
||||
{
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
meshtastic_PositionLite &slot = nodePositions[nodeId]; // creates default-zero entry if missing
|
||||
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
// Local packet, fully authoritative
|
||||
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
|
||||
p.altitude);
|
||||
|
||||
setLocalPosition(p);
|
||||
slot = TypeConversions::ConvertToPositionLite(p);
|
||||
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
|
||||
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
|
||||
// (stop-gap fix for issue #900)
|
||||
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
|
||||
slot.time = p.time;
|
||||
} else {
|
||||
// Be careful to only update fields that have been set by the REMOTE sender
|
||||
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
|
||||
// recorded based on the packet rxTime
|
||||
//
|
||||
// FIXME perhaps handle RX_SRC_USER separately?
|
||||
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
|
||||
|
||||
// First, back up fields that we want to protect from overwrite
|
||||
uint32_t tmp_time = slot.time;
|
||||
|
||||
// Next, update atomically
|
||||
slot = TypeConversions::ConvertToPositionLite(p);
|
||||
|
||||
// Last, restore any fields that may have been overwritten
|
||||
if (!slot.time)
|
||||
slot.time = tmp_time;
|
||||
}
|
||||
}
|
||||
updateGUIforNode = info;
|
||||
notifyObservers(true); // Force an update whether or not our node counts have changed
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Update telemetry info for this node based on received metrics
|
||||
* We only care about device telemetry here
|
||||
/** Update telemetry info for this node based on received metrics. Stores
|
||||
* device_metrics and environment_metrics into their respective satellite
|
||||
* maps; other variants (air_quality, power, local_stats, health) are
|
||||
* intentionally not retained per-node.
|
||||
*/
|
||||
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
|
||||
{
|
||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
||||
// Environment metrics should never go to NodeDb but we'll safegaurd anyway
|
||||
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
|
||||
if (!info)
|
||||
return;
|
||||
}
|
||||
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
// Local packet, fully authoritative
|
||||
LOG_DEBUG("updateTelemetry LOCAL");
|
||||
if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
LOG_DEBUG("updateTelemetry LOCAL device");
|
||||
} else {
|
||||
LOG_DEBUG("updateTelemetry REMOTE device node=0x%x", nodeId);
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
nodeTelemetry[nodeId] = t.variant.device_metrics;
|
||||
#endif
|
||||
} else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
||||
if (src == RX_SRC_LOCAL) {
|
||||
LOG_DEBUG("updateTelemetry LOCAL env");
|
||||
} else {
|
||||
LOG_DEBUG("updateTelemetry REMOTE env node=0x%x", nodeId);
|
||||
}
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
concurrency::LockGuard guard(&satelliteMutex);
|
||||
nodeEnvironment[nodeId] = t.variant.environment_metrics;
|
||||
#endif
|
||||
} else {
|
||||
LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId);
|
||||
return; // air_quality / power / local_stats / health: not stored per-node
|
||||
}
|
||||
info->device_metrics = t.variant.device_metrics;
|
||||
info->has_device_metrics = true;
|
||||
updateGUIforNode = info;
|
||||
notifyObservers(true); // Force an update whether or not our node counts have changed
|
||||
notifyObservers(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1806,24 +2225,22 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
// If the local node has this node marked as manually verified
|
||||
// and the client does not, do not allow the client to update the
|
||||
// saved public key.
|
||||
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) {
|
||||
if (contact.user.public_key.size != info->user.public_key.size ||
|
||||
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) {
|
||||
if (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) {
|
||||
if (contact.user.public_key.size != info->public_key.size ||
|
||||
memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
info->num = contact.node_num;
|
||||
info->has_user = true;
|
||||
info->user = TypeConversions::ConvertToUserLite(contact.user);
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
||||
if (contact.should_ignore) {
|
||||
// If should_ignore is set,
|
||||
// we need to clear the public key and other cruft, in addition to setting the node as ignored
|
||||
info->is_ignored = true;
|
||||
info->is_favorite = false;
|
||||
info->has_device_metrics = false;
|
||||
info->has_position = false;
|
||||
info->user.public_key.size = 0;
|
||||
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
||||
eraseNodeSatellites(contact.node_num);
|
||||
info->public_key.size = 0;
|
||||
memset(info->public_key.bytes, 0, sizeof(info->public_key.bytes));
|
||||
} else {
|
||||
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
||||
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
||||
@@ -1842,12 +2259,12 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
} else {
|
||||
// Normal case: set is_favorite to prevent expiration.
|
||||
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
||||
info->is_favorite = true;
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
||||
}
|
||||
|
||||
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
||||
if (contact.manually_verified) {
|
||||
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK, true);
|
||||
}
|
||||
// Mark the node's key as manually verified to indicate trustworthiness.
|
||||
updateGUIforNode = info;
|
||||
@@ -1887,9 +2304,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
||||
if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
||||
// if the key doesn't match, don't update nodeDB at all.
|
||||
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) {
|
||||
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) {
|
||||
LOG_WARN("Public Key mismatch, dropping NodeInfo");
|
||||
return false;
|
||||
}
|
||||
@@ -1902,19 +2319,22 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
|
||||
// Always ensure user.id is derived from nodeId, regardless of what was received
|
||||
snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
|
||||
|
||||
// Both of info->user and p start as filled with zero so I think this is okay
|
||||
auto lite = TypeConversions::ConvertToUserLite(p);
|
||||
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex);
|
||||
meshtastic_NodeInfoLite before = *info;
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, p);
|
||||
bool changed =
|
||||
(memcmp(before.long_name, info->long_name, sizeof(info->long_name)) != 0) ||
|
||||
(memcmp(before.short_name, info->short_name, sizeof(info->short_name)) != 0) || (before.hw_model != info->hw_model) ||
|
||||
(before.role != info->role) || (before.public_key.size != info->public_key.size) ||
|
||||
(info->public_key.size > 0 && memcmp(before.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) ||
|
||||
(before.bitfield != info->bitfield) || (info->channel != channelIndex);
|
||||
|
||||
info->user = lite;
|
||||
if (info->user.public_key.size == 32) {
|
||||
printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32);
|
||||
if (info->public_key.size == 32) {
|
||||
printBytes("Saved Pubkey: ", info->public_key.bytes, 32);
|
||||
}
|
||||
if (nodeId != getNodeNum())
|
||||
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
|
||||
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId,
|
||||
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->long_name, info->short_name, nodeId,
|
||||
info->channel);
|
||||
info->has_user = true;
|
||||
|
||||
if (changed) {
|
||||
updateGUIforNode = info;
|
||||
@@ -1956,7 +2376,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
if (mp.rx_snr)
|
||||
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
||||
|
||||
info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT
|
||||
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
||||
mp.via_mqtt); // Store if we received this packet via MQTT
|
||||
|
||||
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
@@ -1971,8 +2392,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
|
||||
{
|
||||
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
||||
if (lite && lite->is_favorite != is_favorite) {
|
||||
lite->is_favorite = is_favorite;
|
||||
if (lite && nodeInfoLiteIsFavorite(lite) != is_favorite) {
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite);
|
||||
sortMeshDB();
|
||||
saveNodeDatabaseToDisk();
|
||||
}
|
||||
@@ -1986,10 +2407,10 @@ bool NodeDB::isFavorite(uint32_t nodeId)
|
||||
if (nodeId == NODENUM_BROADCAST)
|
||||
return false;
|
||||
|
||||
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
||||
const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
||||
|
||||
if (lite) {
|
||||
return lite->is_favorite;
|
||||
return nodeInfoLiteIsFavorite(lite);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2014,14 +2435,14 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
|
||||
lite = &meshNodes->at(i);
|
||||
|
||||
if (lite->num == p.from) {
|
||||
if (lite->is_favorite)
|
||||
if (nodeInfoLiteIsFavorite(lite))
|
||||
return true;
|
||||
|
||||
seenFrom = true;
|
||||
}
|
||||
|
||||
if (lite->num == p.to) {
|
||||
if (lite->is_favorite)
|
||||
if (nodeInfoLiteIsFavorite(lite))
|
||||
return true;
|
||||
|
||||
seenTo = true;
|
||||
@@ -2057,10 +2478,10 @@ void NodeDB::sortMeshDB()
|
||||
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
|
||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||
changed = true;
|
||||
} else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) {
|
||||
} else if (nodeInfoLiteIsFavorite(&meshNodes->at(i)) && !nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||
changed = true;
|
||||
} else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) {
|
||||
} else if (!nodeInfoLiteIsFavorite(&meshNodes->at(i)) && nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
||||
// noop
|
||||
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
|
||||
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
||||
@@ -2120,17 +2541,18 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
int oldestIndex = -1;
|
||||
int oldestBoringIndex = -1;
|
||||
for (int i = 1; i < numMeshNodes; i++) {
|
||||
const meshtastic_NodeInfoLite *cand = &meshNodes->at(i);
|
||||
const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand);
|
||||
const bool isIgnored = nodeInfoLiteIsIgnored(cand);
|
||||
const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand);
|
||||
// Simply the oldest non-favorite, non-ignored, non-verified node
|
||||
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored &&
|
||||
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) &&
|
||||
meshNodes->at(i).last_heard < oldest) {
|
||||
oldest = meshNodes->at(i).last_heard;
|
||||
if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
|
||||
oldest = cand->last_heard;
|
||||
oldestIndex = i;
|
||||
}
|
||||
// The oldest "boring" node
|
||||
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 &&
|
||||
meshNodes->at(i).last_heard < oldestBoring) {
|
||||
oldestBoring = meshNodes->at(i).last_heard;
|
||||
if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
|
||||
oldestBoring = cand->last_heard;
|
||||
oldestBoringIndex = i;
|
||||
}
|
||||
}
|
||||
@@ -2140,6 +2562,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
}
|
||||
|
||||
if (oldestIndex != -1) {
|
||||
eraseNodeSatellites(meshNodes->at(oldestIndex).num);
|
||||
// Shove the remaining nodes down the chain
|
||||
for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
|
||||
meshNodes->at(i) = meshNodes->at(i + 1);
|
||||
@@ -2163,18 +2586,23 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
/// valid lat/lon
|
||||
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0);
|
||||
if (!n)
|
||||
return false;
|
||||
if (n->num == getNodeNum()) {
|
||||
return localPosition.latitude_i != 0 || localPosition.longitude_i != 0;
|
||||
}
|
||||
meshtastic_PositionLite pos;
|
||||
return copyNodePosition(n->num, pos) && (pos.latitude_i != 0 || pos.longitude_i != 0);
|
||||
}
|
||||
|
||||
/// If we have a node / user and they report is_licensed = true
|
||||
/// we consider them licensed
|
||||
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
|
||||
{
|
||||
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
||||
if (!info || !info->has_user) {
|
||||
const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
||||
if (!nodeInfoLiteHasUser(info))
|
||||
return UserLicenseStatus::NotKnown;
|
||||
}
|
||||
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
|
||||
return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
|
||||
}
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_PKI)
|
||||
|
||||
+134
-4
@@ -6,10 +6,12 @@
|
||||
#include <assert.h>
|
||||
#include <pb_encode.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "MeshTypes.h"
|
||||
#include "NodeStatus.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode
|
||||
@@ -82,7 +84,9 @@ DeviceState versions used to be defined in the .proto file but really only this
|
||||
#define SEGMENT_CHANNELS 8
|
||||
#define SEGMENT_NODEDATABASE 16
|
||||
|
||||
#define DEVICESTATE_CUR_VER 24
|
||||
#define DEVICESTATE_CUR_VER 25
|
||||
// Lowest on-disk version we still know how to load. v24 saves are migrated
|
||||
// at boot via the parallel deviceonly_legacy descriptor and re-saved as v25.
|
||||
#define DEVICESTATE_MIN_VER 24
|
||||
|
||||
extern meshtastic_DeviceState devicestate;
|
||||
@@ -166,6 +170,21 @@ class NodeDB
|
||||
Observable<const meshtastic::NodeStatus *> newStatus;
|
||||
pb_size_t numMeshNodes;
|
||||
|
||||
// Satellite per-NodeNum maps for data we used to inline into NodeInfoLite,
|
||||
// gated by MESHTASTIC_EXCLUDE_*DB so STM32WL can omit them.
|
||||
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
std::unordered_map<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 hasWarned = false;
|
||||
|
||||
@@ -277,6 +296,43 @@ class NodeDB
|
||||
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
|
||||
size_t getNumMeshNodes() { return numMeshNodes; }
|
||||
|
||||
// Thread-safe satellite-map accessors. Return false if absent or the
|
||||
// corresponding DB is compiled out.
|
||||
bool copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const;
|
||||
bool copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const;
|
||||
bool copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const;
|
||||
bool copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const;
|
||||
std::vector<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);
|
||||
|
||||
size_t getMaxNodesAllocatedSize()
|
||||
@@ -285,7 +341,11 @@ class NodeDB
|
||||
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
|
||||
size_t nodeDatabaseSize;
|
||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
|
||||
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size);
|
||||
// Always include satellite slots so backups from higher-cap peers
|
||||
// decode without truncation, even when our build excludes the DBs.
|
||||
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size) +
|
||||
(MAX_NUM_NODES * meshtastic_NodePositionEntry_size) + (MAX_NUM_NODES * meshtastic_NodeTelemetryEntry_size) +
|
||||
(MAX_NUM_NODES * meshtastic_NodeEnvironmentEntry_size) + (MAX_NUM_NODES * meshtastic_NodeStatusEntry_size);
|
||||
}
|
||||
|
||||
// returns true if the maximum number of nodes is reached or we are running low on memory
|
||||
@@ -329,6 +389,7 @@ class NodeDB
|
||||
}
|
||||
|
||||
private:
|
||||
mutable concurrency::Lock satelliteMutex;
|
||||
bool duplicateWarned = false;
|
||||
bool localPositionUpdatedSinceBoot = false;
|
||||
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
|
||||
@@ -363,6 +424,11 @@ class NodeDB
|
||||
bool saveDeviceStateToDisk();
|
||||
bool saveNodeDatabaseToDisk();
|
||||
void sortMeshDB();
|
||||
|
||||
// Defined in NodeDBLegacyMigration.cpp. Decodes /prefs/nodes.proto via
|
||||
// the legacy descriptor and copies entries into the v25 layout. Caller
|
||||
// is responsible for save / install-default on the result.
|
||||
bool migrateLegacyNodeDatabase();
|
||||
};
|
||||
|
||||
extern NodeDB *nodeDB;
|
||||
@@ -397,10 +463,74 @@ extern meshtastic_CriticalErrorCode error_code;
|
||||
* A numeric error address (nonzero if available)
|
||||
*/
|
||||
extern uint32_t error_address;
|
||||
// Bit assignments for meshtastic_NodeInfoLite.bitfield.
|
||||
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0
|
||||
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1u << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1
|
||||
#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_MUTED_MASK (1u << NODEINFO_BITFIELD_IS_MUTED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_VIA_MQTT_SHIFT 2
|
||||
#define NODEINFO_BITFIELD_VIA_MQTT_MASK (1u << NODEINFO_BITFIELD_VIA_MQTT_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_FAVORITE_SHIFT 3
|
||||
#define NODEINFO_BITFIELD_IS_FAVORITE_MASK (1u << NODEINFO_BITFIELD_IS_FAVORITE_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_IGNORED_SHIFT 4
|
||||
#define NODEINFO_BITFIELD_IS_IGNORED_MASK (1u << NODEINFO_BITFIELD_IS_IGNORED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_HAS_USER_SHIFT 5
|
||||
#define NODEINFO_BITFIELD_HAS_USER_MASK (1u << NODEINFO_BITFIELD_HAS_USER_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_LICENSED_SHIFT 6
|
||||
#define NODEINFO_BITFIELD_IS_LICENSED_MASK (1u << NODEINFO_BITFIELD_IS_LICENSED_SHIFT)
|
||||
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT 7
|
||||
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT)
|
||||
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT 8
|
||||
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
|
||||
// Bits 9..31 reserved for future single-bit flags.
|
||||
|
||||
// Convenience accessors so call sites read like the old struct fields.
|
||||
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteViaMqtt(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsFavorite(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsIgnored(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsLicensed(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteHasIsUnmessagable(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsUnmessagable(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsMuted(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK);
|
||||
}
|
||||
inline bool nodeInfoLiteIsKeyManuallyVerified(const meshtastic_NodeInfoLite *n)
|
||||
{
|
||||
return n && (n->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK);
|
||||
}
|
||||
|
||||
inline void nodeInfoLiteSetBit(meshtastic_NodeInfoLite *n, uint32_t mask, bool value)
|
||||
{
|
||||
if (!n)
|
||||
return;
|
||||
if (value)
|
||||
n->bitfield |= mask;
|
||||
else
|
||||
n->bitfield &= ~mask;
|
||||
}
|
||||
|
||||
#define Module_Config_size \
|
||||
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
|
||||
|
||||
@@ -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
@@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig()
|
||||
onConfigStart();
|
||||
|
||||
// even if we were already connected - restart our state machine
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OWN_NODEINFO;
|
||||
LOG_INFO("Client only wants node info, skipping other config");
|
||||
@@ -81,6 +81,15 @@ void PhoneAPI::handleStartConfig()
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = {};
|
||||
nodeInfoQueue.clear();
|
||||
replayQueue.clear();
|
||||
replayPositionOrder.clear();
|
||||
replayTelemetryOrder.clear();
|
||||
replayEnvironmentOrder.clear();
|
||||
replayStatusOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
replayTelemetryIndex = 0;
|
||||
replayEnvironmentIndex = 0;
|
||||
replayStatusIndex = 0;
|
||||
}
|
||||
resetReadIndex();
|
||||
}
|
||||
@@ -120,6 +129,15 @@ void PhoneAPI::close()
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoForPhone = {};
|
||||
nodeInfoQueue.clear();
|
||||
replayQueue.clear();
|
||||
replayPositionOrder.clear();
|
||||
replayTelemetryOrder.clear();
|
||||
replayEnvironmentOrder.clear();
|
||||
replayStatusOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
replayTelemetryIndex = 0;
|
||||
replayEnvironmentIndex = 0;
|
||||
replayStatusIndex = 0;
|
||||
}
|
||||
packetForPhone = NULL;
|
||||
filesManifest.clear();
|
||||
@@ -302,7 +320,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
nodeInfoForPhone.num = 0;
|
||||
}
|
||||
}
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OTHER_NODEINFOS;
|
||||
onNowHasData(0);
|
||||
@@ -529,8 +547,124 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
nodeInfoQueue.clear();
|
||||
// Replay states no-op for legacy clients / excluded DBs.
|
||||
state = STATE_REPLAY_POSITIONS;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_POSITIONS: {
|
||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
||||
beginReplayPositions();
|
||||
prefetchReplayPositions();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying positions count=%u millis=%u", (unsigned)replayPositionIndex, millis());
|
||||
state = STATE_REPLAY_TELEMETRY;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_TELEMETRY: {
|
||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
||||
beginReplayTelemetry();
|
||||
prefetchReplayTelemetry();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying telemetry count=%u millis=%u", (unsigned)replayTelemetryIndex, millis());
|
||||
state = STATE_REPLAY_ENVIRONMENT;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_ENVIRONMENT: {
|
||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
||||
beginReplayEnvironment();
|
||||
prefetchReplayEnvironment();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying environment count=%u millis=%u", (unsigned)replayEnvironmentIndex, millis());
|
||||
state = STATE_REPLAY_STATUS;
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case STATE_REPLAY_STATUS: {
|
||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
||||
beginReplayStatus();
|
||||
prefetchReplayStatus();
|
||||
|
||||
meshtastic_MeshPacket pkt = {};
|
||||
bool havePkt = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
if (!replayQueue.empty()) {
|
||||
pkt = replayQueue.front();
|
||||
replayQueue.pop_front();
|
||||
havePkt = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (havePkt) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = pkt;
|
||||
} else {
|
||||
LOG_DEBUG("Done replaying status count=%u millis=%u", (unsigned)replayStatusIndex, millis());
|
||||
replayPositionOrder.clear();
|
||||
replayPositionOrder.shrink_to_fit();
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryOrder.shrink_to_fit();
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentOrder.shrink_to_fit();
|
||||
replayStatusOrder.clear();
|
||||
replayStatusOrder.shrink_to_fit();
|
||||
state = STATE_SEND_FILEMANIFEST;
|
||||
// Go ahead and send that ID right now
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
break;
|
||||
@@ -538,9 +672,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
|
||||
case STATE_SEND_FILEMANIFEST: {
|
||||
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
|
||||
// last element
|
||||
if (config_state == filesManifest.size() ||
|
||||
config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest
|
||||
// ONLY_NODES variants skip the manifest.
|
||||
if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES ||
|
||||
config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
|
||||
config_state = 0;
|
||||
filesManifest.clear();
|
||||
// Skip to complete packet
|
||||
@@ -653,6 +787,7 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
{
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
const bool gradient = clientWantsGradientSync();
|
||||
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
@@ -662,7 +797,8 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
if (!nextNode)
|
||||
break;
|
||||
|
||||
auto info = TypeConversions::ConvertToNodeInfo(nextNode);
|
||||
auto info =
|
||||
gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : TypeConversions::ConvertToNodeInfo(nextNode);
|
||||
bool isUs = info.num == nodeDB->getNodeNum();
|
||||
info.hops_away = isUs ? 0 : info.hops_away;
|
||||
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
|
||||
@@ -682,6 +818,257 @@ void PhoneAPI::prefetchNodeInfos()
|
||||
onNowHasData(0);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.id = generatePacketId();
|
||||
pkt.rx_time = pos.time;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
||||
meshtastic_Position fullPos = TypeConversions::ConvertToPosition(pos);
|
||||
size_t len =
|
||||
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Position_msg, &fullPos);
|
||||
pkt.decoded.payload.size = (pb_size_t)len;
|
||||
return pkt;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const meshtastic_DeviceMetrics &metrics)
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.id = generatePacketId();
|
||||
// No native timestamp on telemetry packets here; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
||||
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
||||
fullTel.time = pkt.rx_time;
|
||||
fullTel.which_variant = meshtastic_Telemetry_device_metrics_tag;
|
||||
fullTel.variant.device_metrics = metrics;
|
||||
size_t len =
|
||||
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
|
||||
pkt.decoded.payload.size = (pb_size_t)len;
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void PhoneAPI::beginReplayPositions()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
// Build excluded entirely - leave the order list empty so the state arm
|
||||
// immediately drains and advances.
|
||||
replayPositionOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayPositionOrder.clear();
|
||||
replayPositionIndex = 0;
|
||||
return;
|
||||
}
|
||||
// Snapshot the keyset at phase start so concurrent inserts/erases on the
|
||||
// map don't invalidate iteration. Skip our own node - the phone already
|
||||
// got our position bundled in STATE_SEND_OWN_NODEINFO.
|
||||
replayPositionOrder = nodeDB->snapshotPositionNodeNums(nodeDB->getNodeNum());
|
||||
replayPositionIndex = 0;
|
||||
LOG_INFO("Begin position replay: %u entries millis=%u", (unsigned)replayPositionOrder.size(), millis());
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::prefetchReplayPositions()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
wasEmpty = replayQueue.empty();
|
||||
while (replayQueue.size() < kReplayPrefetchDepth && replayPositionIndex < replayPositionOrder.size()) {
|
||||
NodeNum num = replayPositionOrder[replayPositionIndex++];
|
||||
meshtastic_PositionLite pos;
|
||||
if (!nodeDB->copyNodePosition(num, pos))
|
||||
continue; // entry was evicted between snapshot and now
|
||||
replayQueue.push_back(makeReplayPositionPacket(num, pos));
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (added && wasEmpty)
|
||||
onNowHasData(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::beginReplayTelemetry()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayTelemetryOrder.clear();
|
||||
replayTelemetryIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayTelemetryOrder = nodeDB->snapshotTelemetryNodeNums(nodeDB->getNodeNum());
|
||||
replayTelemetryIndex = 0;
|
||||
LOG_INFO("Begin telemetry replay: %u entries millis=%u", (unsigned)replayTelemetryOrder.size(), millis());
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::prefetchReplayTelemetry()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
wasEmpty = replayQueue.empty();
|
||||
while (replayQueue.size() < kReplayPrefetchDepth && replayTelemetryIndex < replayTelemetryOrder.size()) {
|
||||
NodeNum num = replayTelemetryOrder[replayTelemetryIndex++];
|
||||
meshtastic_DeviceMetrics dm;
|
||||
if (!nodeDB->copyNodeTelemetry(num, dm))
|
||||
continue;
|
||||
replayQueue.push_back(makeReplayTelemetryPacket(num, dm));
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (added && wasEmpty)
|
||||
onNowHasData(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env)
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.id = generatePacketId();
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
|
||||
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
|
||||
fullTel.time = pkt.rx_time;
|
||||
fullTel.which_variant = meshtastic_Telemetry_environment_metrics_tag;
|
||||
fullTel.variant.environment_metrics = env;
|
||||
size_t len =
|
||||
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
|
||||
pkt.decoded.payload.size = (pb_size_t)len;
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void PhoneAPI::beginReplayEnvironment()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayEnvironmentOrder.clear();
|
||||
replayEnvironmentIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayEnvironmentOrder = nodeDB->snapshotEnvironmentNodeNums(nodeDB->getNodeNum());
|
||||
replayEnvironmentIndex = 0;
|
||||
LOG_INFO("Begin environment replay: %u entries millis=%u", (unsigned)replayEnvironmentOrder.size(), millis());
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::prefetchReplayEnvironment()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
wasEmpty = replayQueue.empty();
|
||||
while (replayQueue.size() < kReplayPrefetchDepth && replayEnvironmentIndex < replayEnvironmentOrder.size()) {
|
||||
NodeNum num = replayEnvironmentOrder[replayEnvironmentIndex++];
|
||||
meshtastic_EnvironmentMetrics env;
|
||||
if (!nodeDB->copyNodeEnvironment(num, env))
|
||||
continue;
|
||||
replayQueue.push_back(makeReplayEnvironmentPacket(num, env));
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (added && wasEmpty)
|
||||
onNowHasData(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status)
|
||||
{
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
|
||||
pkt.from = num;
|
||||
pkt.to = nodeDB->getNodeNum();
|
||||
pkt.id = generatePacketId();
|
||||
// StatusMessage has no native timestamp; use last_heard.
|
||||
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
|
||||
pkt.rx_time = header ? header->last_heard : 0;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
|
||||
size_t len =
|
||||
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_StatusMessage_msg, &status);
|
||||
pkt.decoded.payload.size = (pb_size_t)len;
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void PhoneAPI::beginReplayStatus()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
||||
replayStatusOrder.clear();
|
||||
replayStatusIndex = 0;
|
||||
#else
|
||||
if (!clientWantsGradientSync()) {
|
||||
replayStatusOrder.clear();
|
||||
replayStatusIndex = 0;
|
||||
return;
|
||||
}
|
||||
replayStatusOrder = nodeDB->snapshotStatusNodeNums(nodeDB->getNodeNum());
|
||||
replayStatusIndex = 0;
|
||||
LOG_INFO("Begin status replay: %u entries millis=%u", (unsigned)replayStatusOrder.size(), millis());
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::prefetchReplayStatus()
|
||||
{
|
||||
#if MESHTASTIC_EXCLUDE_STATUSDB
|
||||
return;
|
||||
#else
|
||||
if (!clientWantsGradientSync())
|
||||
return;
|
||||
bool added = false;
|
||||
bool wasEmpty = false;
|
||||
{
|
||||
concurrency::LockGuard guard(&nodeInfoMutex);
|
||||
wasEmpty = replayQueue.empty();
|
||||
while (replayQueue.size() < kReplayPrefetchDepth && replayStatusIndex < replayStatusOrder.size()) {
|
||||
NodeNum num = replayStatusOrder[replayStatusIndex++];
|
||||
meshtastic_StatusMessage status;
|
||||
if (!nodeDB->copyNodeStatus(num, status) || status.status[0] == '\0')
|
||||
continue;
|
||||
replayQueue.push_back(makeReplayStatusPacket(num, status));
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
if (added && wasEmpty)
|
||||
onNowHasData(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PhoneAPI::releaseMqttClientProxyPhonePacket()
|
||||
{
|
||||
if (mqttClientProxyMessageForPhone) {
|
||||
@@ -728,6 +1115,31 @@ bool PhoneAPI::available()
|
||||
PREFETCH_NODEINFO:
|
||||
prefetchNodeInfos();
|
||||
return true;
|
||||
case STATE_REPLAY_POSITIONS: {
|
||||
// Prime the iterator if we haven't yet, then top up the queue.
|
||||
if (replayPositionOrder.empty() && replayPositionIndex == 0)
|
||||
beginReplayPositions();
|
||||
prefetchReplayPositions();
|
||||
return true; // Always advance state machine; arm itself transitions when drained
|
||||
}
|
||||
case STATE_REPLAY_TELEMETRY: {
|
||||
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
|
||||
beginReplayTelemetry();
|
||||
prefetchReplayTelemetry();
|
||||
return true;
|
||||
}
|
||||
case STATE_REPLAY_ENVIRONMENT: {
|
||||
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
|
||||
beginReplayEnvironment();
|
||||
prefetchReplayEnvironment();
|
||||
return true;
|
||||
}
|
||||
case STATE_REPLAY_STATUS: {
|
||||
if (replayStatusOrder.empty() && replayStatusIndex == 0)
|
||||
beginReplayStatus();
|
||||
prefetchReplayStatus();
|
||||
return true;
|
||||
}
|
||||
case STATE_SEND_PACKETS: {
|
||||
if (!queueStatusPacketForPhone)
|
||||
queueStatusPacketForPhone = service->getQueueStatusForPhone();
|
||||
|
||||
+44
-1
@@ -4,12 +4,16 @@
|
||||
#include "concurrency/Lock.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshtastic/portnums.pb.h"
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#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
|
||||
#define MAX_TO_FROM_RADIO_SIZE 512
|
||||
|
||||
@@ -22,6 +26,9 @@
|
||||
|
||||
#define SPECIAL_NONCE_ONLY_CONFIG 69420
|
||||
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°)
|
||||
// Gradient sync: phone sends one of these to opt into thin-header + replay.
|
||||
#define SPECIAL_NONCE_GRADIENT_SYNC 69422
|
||||
#define SPECIAL_NONCE_GRADIENT_ONLY_NODES 69423
|
||||
|
||||
/**
|
||||
* Provides our protobuf based API which phone/PC clients can use to talk to our device
|
||||
@@ -45,7 +52,13 @@ class PhoneAPI
|
||||
STATE_SEND_CONFIG, // Replacement for the old Radioconfig
|
||||
STATE_SEND_MODULECONFIG, // Send Module specific config
|
||||
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client
|
||||
STATE_SEND_FILEMANIFEST, // Send file manifest
|
||||
// Drain satellite DBs as synthetic POSITION_APP / TELEMETRY_APP /
|
||||
// NODE_STATUS_APP packets when the phone opted into gradient sync.
|
||||
STATE_REPLAY_POSITIONS,
|
||||
STATE_REPLAY_TELEMETRY,
|
||||
STATE_REPLAY_ENVIRONMENT,
|
||||
STATE_REPLAY_STATUS,
|
||||
STATE_SEND_FILEMANIFEST, // Send file manifest
|
||||
STATE_SEND_COMPLETE_ID,
|
||||
STATE_SEND_PACKETS // send packets or debug strings
|
||||
};
|
||||
@@ -88,6 +101,20 @@ class PhoneAPI
|
||||
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
|
||||
concurrency::Lock nodeInfoMutex;
|
||||
|
||||
// Synthetic-packet replay queue (paced via prefetch).
|
||||
std::deque<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 = {
|
||||
0}; // this is a static scratch object, any data must be copied elsewhere before returning
|
||||
|
||||
@@ -137,6 +164,10 @@ class PhoneAPI
|
||||
|
||||
bool isConnected() { return state != STATE_SEND_NOTHING; }
|
||||
bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
|
||||
bool clientWantsGradientSync() const
|
||||
{
|
||||
return config_nonce == SPECIAL_NONCE_GRADIENT_SYNC || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES;
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Our fromradio packet while it is being assembled
|
||||
@@ -185,6 +216,18 @@ class PhoneAPI
|
||||
void releaseQueueStatusPhonePacket();
|
||||
|
||||
void prefetchNodeInfos();
|
||||
void beginReplayPositions();
|
||||
void prefetchReplayPositions();
|
||||
void beginReplayTelemetry();
|
||||
void prefetchReplayTelemetry();
|
||||
void beginReplayEnvironment();
|
||||
void prefetchReplayEnvironment();
|
||||
void beginReplayStatus();
|
||||
void prefetchReplayStatus();
|
||||
meshtastic_MeshPacket makeReplayPositionPacket(uint32_t num, const meshtastic_PositionLite &pos);
|
||||
meshtastic_MeshPacket makeReplayTelemetryPacket(uint32_t num, const meshtastic_DeviceMetrics &metrics);
|
||||
meshtastic_MeshPacket makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env);
|
||||
meshtastic_MeshPacket makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status);
|
||||
|
||||
void releaseMqttClientProxyPhonePacket();
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
||||
const char *getSenderShortName(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
auto node = nodeDB->getMeshNode(getFrom(&mp));
|
||||
const char *sender = (node) ? node->user.short_name : "???";
|
||||
const char *sender = (node && nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "???";
|
||||
return sender;
|
||||
}
|
||||
|
||||
@@ -121,4 +121,4 @@ template <class T> class ProtobufModule : protected SinglePortModule
|
||||
return alterReceivedProtobuf(mp, decoded);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
|
||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||
}
|
||||
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
|
||||
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) {
|
||||
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->public_key.size == 0)) {
|
||||
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
|
||||
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
|
||||
routingModule->getHopLimitForResponse(*p));
|
||||
|
||||
+16
-16
@@ -120,17 +120,17 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
if (!node)
|
||||
continue;
|
||||
|
||||
// Check 1: is_favorite (cheapest - single bool)
|
||||
if (!node->is_favorite)
|
||||
// Check 1: is_favorite (cheapest - single bit test)
|
||||
if (!nodeInfoLiteIsFavorite(node))
|
||||
continue;
|
||||
|
||||
// Check 2: has_user (cheap - single bool)
|
||||
if (!node->has_user)
|
||||
// Check 2: has_user (cheap - single bit test)
|
||||
if (!nodeInfoLiteHasUser(node))
|
||||
continue;
|
||||
|
||||
// Check 3: role check (moderate cost - multiple comparisons)
|
||||
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER,
|
||||
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
||||
if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
|
||||
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
concurrency::LockGuard g(cryptLock);
|
||||
|
||||
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
|
||||
(nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) {
|
||||
!nodeInfoLiteHasUser(nodeDB->getMeshNode(p->from))) {
|
||||
LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
|
||||
return DecodeState::DECODE_FAILURE;
|
||||
}
|
||||
@@ -451,11 +451,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Attempt PKI decryption first
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
|
||||
nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 &&
|
||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
|
||||
nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
|
||||
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes,
|
||||
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
|
||||
bytes)) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
|
||||
@@ -467,7 +467,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
decrypted = true;
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32);
|
||||
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
@@ -665,18 +665,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a known public key for the destination
|
||||
if (node == nullptr || node->user.public_key.size != 32) {
|
||||
if (node == nullptr || node->public_key.size != 32) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
||||
}
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
|
||||
memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) {
|
||||
memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) {
|
||||
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
|
||||
*node->user.public_key.bytes);
|
||||
*node->public_key.bytes);
|
||||
return meshtastic_Routing_Error_PKI_FAILED;
|
||||
}
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
@@ -860,7 +860,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
|
||||
if (node != NULL && node->is_ignored) {
|
||||
if (nodeInfoLiteIsIgnored(node)) {
|
||||
LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
|
||||
packetPool.release(p);
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite,
|
||||
const meshtastic_PositionLite *position,
|
||||
const meshtastic_DeviceMetrics *deviceMetrics)
|
||||
{
|
||||
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
|
||||
|
||||
@@ -11,42 +13,56 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
|
||||
info.snr = lite->snr;
|
||||
info.last_heard = lite->last_heard;
|
||||
info.channel = lite->channel;
|
||||
info.via_mqtt = lite->via_mqtt;
|
||||
info.is_favorite = lite->is_favorite;
|
||||
info.is_ignored = lite->is_ignored;
|
||||
info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK;
|
||||
info.via_mqtt = nodeInfoLiteViaMqtt(lite);
|
||||
info.is_favorite = nodeInfoLiteIsFavorite(lite);
|
||||
info.is_ignored = nodeInfoLiteIsIgnored(lite);
|
||||
info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite);
|
||||
info.is_muted = nodeInfoLiteIsMuted(lite);
|
||||
|
||||
if (lite->has_hops_away) {
|
||||
info.has_hops_away = true;
|
||||
info.hops_away = lite->hops_away;
|
||||
}
|
||||
|
||||
if (lite->has_position) {
|
||||
if (position) {
|
||||
info.has_position = true;
|
||||
if (lite->position.latitude_i != 0)
|
||||
if (position->latitude_i != 0)
|
||||
info.position.has_latitude_i = true;
|
||||
info.position.latitude_i = lite->position.latitude_i;
|
||||
if (lite->position.longitude_i != 0)
|
||||
info.position.latitude_i = position->latitude_i;
|
||||
if (position->longitude_i != 0)
|
||||
info.position.has_longitude_i = true;
|
||||
info.position.longitude_i = lite->position.longitude_i;
|
||||
if (lite->position.altitude != 0)
|
||||
info.position.longitude_i = position->longitude_i;
|
||||
if (position->altitude != 0)
|
||||
info.position.has_altitude = true;
|
||||
info.position.altitude = lite->position.altitude;
|
||||
info.position.location_source = lite->position.location_source;
|
||||
info.position.time = lite->position.time;
|
||||
info.position.altitude = position->altitude;
|
||||
info.position.location_source = position->location_source;
|
||||
info.position.time = position->time;
|
||||
}
|
||||
if (lite->has_user) {
|
||||
if (nodeInfoLiteHasUser(lite)) {
|
||||
info.has_user = true;
|
||||
info.user = ConvertToUser(lite->num, lite->user);
|
||||
info.user = ConvertToUser(lite);
|
||||
}
|
||||
if (lite->has_device_metrics) {
|
||||
if (deviceMetrics) {
|
||||
info.has_device_metrics = true;
|
||||
info.device_metrics = lite->device_metrics;
|
||||
info.device_metrics = *deviceMetrics;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
||||
{
|
||||
meshtastic_PositionLite posScratch;
|
||||
meshtastic_DeviceMetrics dmScratch;
|
||||
const meshtastic_PositionLite *pos = (nodeDB && nodeDB->copyNodePosition(lite->num, posScratch)) ? &posScratch : nullptr;
|
||||
const meshtastic_DeviceMetrics *dm = (nodeDB && nodeDB->copyNodeTelemetry(lite->num, dmScratch)) ? &dmScratch : nullptr;
|
||||
return ConvertToNodeInfo(lite, pos, dm);
|
||||
}
|
||||
|
||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite)
|
||||
{
|
||||
return ConvertToNodeInfo(lite, nullptr, nullptr);
|
||||
}
|
||||
|
||||
meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position)
|
||||
{
|
||||
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
|
||||
@@ -77,46 +93,50 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
|
||||
return position;
|
||||
}
|
||||
|
||||
meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
||||
void TypeConversions::CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user)
|
||||
{
|
||||
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
||||
if (!lite)
|
||||
return;
|
||||
|
||||
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
|
||||
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
|
||||
lite.hw_model = user.hw_model;
|
||||
lite.role = user.role;
|
||||
lite.is_licensed = user.is_licensed;
|
||||
memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr));
|
||||
memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes));
|
||||
lite.public_key.size = user.public_key.size;
|
||||
lite.has_is_unmessagable = user.has_is_unmessagable;
|
||||
lite.is_unmessagable = user.is_unmessagable;
|
||||
return lite;
|
||||
strncpy(lite->long_name, user.long_name, sizeof(lite->long_name));
|
||||
lite->long_name[sizeof(lite->long_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite->long_name, sizeof(lite->long_name));
|
||||
strncpy(lite->short_name, user.short_name, sizeof(lite->short_name));
|
||||
lite->short_name[sizeof(lite->short_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite->short_name, sizeof(lite->short_name));
|
||||
lite->hw_model = user.hw_model;
|
||||
lite->role = user.role;
|
||||
memcpy(lite->public_key.bytes, user.public_key.bytes, sizeof(lite->public_key.bytes));
|
||||
lite->public_key.size = user.public_key.size;
|
||||
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_LICENSED_MASK, user.is_licensed);
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable);
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable && user.is_unmessagable);
|
||||
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_USER_MASK, true);
|
||||
}
|
||||
|
||||
meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite)
|
||||
meshtastic_User TypeConversions::ConvertToUser(const meshtastic_NodeInfoLite *lite)
|
||||
{
|
||||
meshtastic_User user = meshtastic_User_init_default;
|
||||
if (!lite)
|
||||
return user;
|
||||
|
||||
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||
snprintf(user.id, sizeof(user.id), "!%08x", lite->num);
|
||||
strncpy(user.long_name, lite->long_name, sizeof(user.long_name));
|
||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
||||
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||
strncpy(user.short_name, lite->short_name, sizeof(user.short_name));
|
||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
||||
user.hw_model = lite.hw_model;
|
||||
user.role = lite.role;
|
||||
user.is_licensed = lite.is_licensed;
|
||||
memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr));
|
||||
memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes));
|
||||
user.public_key.size = lite.public_key.size;
|
||||
user.has_is_unmessagable = lite.has_is_unmessagable;
|
||||
user.is_unmessagable = lite.is_unmessagable;
|
||||
user.hw_model = lite->hw_model;
|
||||
user.role = lite->role;
|
||||
user.is_licensed = nodeInfoLiteIsLicensed(lite);
|
||||
memcpy(user.public_key.bytes, lite->public_key.bytes, sizeof(user.public_key.bytes));
|
||||
user.public_key.size = lite->public_key.size;
|
||||
user.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(lite);
|
||||
user.is_unmessagable = nodeInfoLiteIsUnmessagable(lite);
|
||||
// macaddr is gone from the slim header; zero-fill for old clients that read it.
|
||||
memset(user.macaddr, 0, sizeof(user.macaddr));
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,16 @@
|
||||
class TypeConversions
|
||||
{
|
||||
public:
|
||||
// Either pointer may be null; the corresponding has_* fields stay unset.
|
||||
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite, const meshtastic_PositionLite *position,
|
||||
const meshtastic_DeviceMetrics *deviceMetrics);
|
||||
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite);
|
||||
// Identity + link-state only; satellite payloads are replayed afterward.
|
||||
static meshtastic_NodeInfo ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite);
|
||||
|
||||
static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position);
|
||||
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite);
|
||||
static meshtastic_UserLite ConvertToUserLite(meshtastic_User user);
|
||||
static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite);
|
||||
|
||||
static void CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user);
|
||||
static meshtastic_User ConvertToUser(const meshtastic_NodeInfoLite *lite);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,18 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO)
|
||||
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodePositionEntry, meshtastic_NodePositionEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeTelemetryEntry, meshtastic_NodeTelemetryEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeStatusEntry, meshtastic_NodeStatusEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO)
|
||||
|
||||
|
||||
|
||||
@@ -63,43 +63,35 @@ typedef struct _meshtastic_UserLite {
|
||||
bool is_unmessagable;
|
||||
} meshtastic_UserLite;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t;
|
||||
typedef struct _meshtastic_NodeInfoLite {
|
||||
/* The node number */
|
||||
uint32_t num;
|
||||
/* The user info for this node */
|
||||
bool has_user;
|
||||
meshtastic_UserLite user;
|
||||
/* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
|
||||
Position.time now indicates the last time we received a POSITION from that node. */
|
||||
bool has_position;
|
||||
meshtastic_PositionLite position;
|
||||
/* Returns the Signal-to-noise ratio (SNR) of the last received message,
|
||||
as measured by the receiver. Return SNR of the last received message in dB */
|
||||
float snr;
|
||||
/* Set to indicate the last time we received a packet from this node */
|
||||
uint32_t last_heard;
|
||||
/* The latest device metrics for the node. */
|
||||
bool has_device_metrics;
|
||||
meshtastic_DeviceMetrics device_metrics;
|
||||
/* local channel index we heard that node on. Only populated if its not the default channel. */
|
||||
uint8_t channel;
|
||||
/* True if we witnessed the node over MQTT instead of LoRA transport */
|
||||
bool via_mqtt;
|
||||
/* Number of hops away from us this node is (0 if direct neighbor) */
|
||||
bool has_hops_away;
|
||||
uint8_t hops_away;
|
||||
/* True if node is in our favorites list
|
||||
Persists between NodeDB internal clean ups */
|
||||
bool is_favorite;
|
||||
/* True if node is in our ignored list
|
||||
Persists between NodeDB internal clean ups */
|
||||
bool is_ignored;
|
||||
/* Last byte of the node number of the node that should be used as the next hop to reach this node. */
|
||||
uint8_t next_hop;
|
||||
/* Bitfield for storing booleans.
|
||||
LSB 0 is_key_manually_verified
|
||||
LSB 1 is_muted */
|
||||
/* Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */
|
||||
uint32_t bitfield;
|
||||
/* A full name for this user, i.e. "Kevin Hester". */
|
||||
char long_name[25];
|
||||
/* A VERY short name, ideally two characters or an emoji.
|
||||
Suitable for a tiny OLED screen. */
|
||||
char short_name[5];
|
||||
/* Hardware model the user's device is running. */
|
||||
meshtastic_HardwareModel hw_model;
|
||||
/* The user's role in the mesh. */
|
||||
meshtastic_Config_DeviceConfig_Role role;
|
||||
/* The public key of the user's device, for PKI-based encrypted DMs. */
|
||||
meshtastic_NodeInfoLite_public_key_t public_key;
|
||||
} meshtastic_NodeInfoLite;
|
||||
|
||||
/* This message is never sent over the wire, but it is used for serializing DB
|
||||
@@ -143,6 +135,30 @@ typedef struct _meshtastic_DeviceState {
|
||||
meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12];
|
||||
} meshtastic_DeviceState;
|
||||
|
||||
typedef struct _meshtastic_NodePositionEntry {
|
||||
uint32_t num;
|
||||
bool has_position;
|
||||
meshtastic_PositionLite position;
|
||||
} meshtastic_NodePositionEntry;
|
||||
|
||||
typedef struct _meshtastic_NodeTelemetryEntry {
|
||||
uint32_t num;
|
||||
bool has_device_metrics;
|
||||
meshtastic_DeviceMetrics device_metrics;
|
||||
} meshtastic_NodeTelemetryEntry;
|
||||
|
||||
typedef struct _meshtastic_NodeEnvironmentEntry {
|
||||
uint32_t num;
|
||||
bool has_environment_metrics;
|
||||
meshtastic_EnvironmentMetrics environment_metrics;
|
||||
} meshtastic_NodeEnvironmentEntry;
|
||||
|
||||
typedef struct _meshtastic_NodeStatusEntry {
|
||||
uint32_t num;
|
||||
bool has_status;
|
||||
meshtastic_StatusMessage status;
|
||||
} meshtastic_NodeStatusEntry;
|
||||
|
||||
typedef struct _meshtastic_NodeDatabase {
|
||||
/* A version integer used to invalidate old save files when we make
|
||||
incompatible changes This integer is set at build time and is private to
|
||||
@@ -150,6 +166,12 @@ typedef struct _meshtastic_NodeDatabase {
|
||||
uint32_t version;
|
||||
/* New lite version of NodeDB to decrease memory footprint */
|
||||
std::vector<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;
|
||||
|
||||
/* The on-disk saved channels */
|
||||
@@ -191,16 +213,24 @@ extern "C" {
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
||||
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
|
||||
#define meshtastic_NodeDatabase_init_default {0, {0}}
|
||||
#define meshtastic_NodePositionEntry_init_default {0, false, meshtastic_PositionLite_init_default}
|
||||
#define meshtastic_NodeTelemetryEntry_init_default {0, false, meshtastic_DeviceMetrics_init_default}
|
||||
#define meshtastic_NodeEnvironmentEntry_init_default {0, false, meshtastic_EnvironmentMetrics_init_default}
|
||||
#define meshtastic_NodeStatusEntry_init_default {0, false, meshtastic_StatusMessage_init_default}
|
||||
#define meshtastic_NodeDatabase_init_default {0, {0}, {0}, {0}, {0}, {0}}
|
||||
#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0}
|
||||
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
|
||||
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
|
||||
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
|
||||
#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
|
||||
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
|
||||
#define meshtastic_NodeDatabase_init_zero {0, {0}}
|
||||
#define meshtastic_NodePositionEntry_init_zero {0, false, meshtastic_PositionLite_init_zero}
|
||||
#define meshtastic_NodeTelemetryEntry_init_zero {0, false, meshtastic_DeviceMetrics_init_zero}
|
||||
#define meshtastic_NodeEnvironmentEntry_init_zero {0, false, meshtastic_EnvironmentMetrics_init_zero}
|
||||
#define meshtastic_NodeStatusEntry_init_zero {0, false, meshtastic_StatusMessage_init_zero}
|
||||
#define meshtastic_NodeDatabase_init_zero {0, {0}, {0}, {0}, {0}, {0}}
|
||||
#define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0}
|
||||
#define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero}
|
||||
|
||||
@@ -219,18 +249,17 @@ extern "C" {
|
||||
#define meshtastic_UserLite_public_key_tag 7
|
||||
#define meshtastic_UserLite_is_unmessagable_tag 9
|
||||
#define meshtastic_NodeInfoLite_num_tag 1
|
||||
#define meshtastic_NodeInfoLite_user_tag 2
|
||||
#define meshtastic_NodeInfoLite_position_tag 3
|
||||
#define meshtastic_NodeInfoLite_snr_tag 4
|
||||
#define meshtastic_NodeInfoLite_last_heard_tag 5
|
||||
#define meshtastic_NodeInfoLite_device_metrics_tag 6
|
||||
#define meshtastic_NodeInfoLite_channel_tag 7
|
||||
#define meshtastic_NodeInfoLite_via_mqtt_tag 8
|
||||
#define meshtastic_NodeInfoLite_hops_away_tag 9
|
||||
#define meshtastic_NodeInfoLite_is_favorite_tag 10
|
||||
#define meshtastic_NodeInfoLite_is_ignored_tag 11
|
||||
#define meshtastic_NodeInfoLite_next_hop_tag 12
|
||||
#define meshtastic_NodeInfoLite_bitfield_tag 13
|
||||
#define meshtastic_NodeInfoLite_long_name_tag 14
|
||||
#define meshtastic_NodeInfoLite_short_name_tag 15
|
||||
#define meshtastic_NodeInfoLite_hw_model_tag 16
|
||||
#define meshtastic_NodeInfoLite_role_tag 17
|
||||
#define meshtastic_NodeInfoLite_public_key_tag 18
|
||||
#define meshtastic_DeviceState_my_node_tag 2
|
||||
#define meshtastic_DeviceState_owner_tag 3
|
||||
#define meshtastic_DeviceState_receive_queue_tag 5
|
||||
@@ -240,8 +269,20 @@ extern "C" {
|
||||
#define meshtastic_DeviceState_did_gps_reset_tag 11
|
||||
#define meshtastic_DeviceState_rx_waypoint_tag 12
|
||||
#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13
|
||||
#define meshtastic_NodePositionEntry_num_tag 1
|
||||
#define meshtastic_NodePositionEntry_position_tag 2
|
||||
#define meshtastic_NodeTelemetryEntry_num_tag 1
|
||||
#define meshtastic_NodeTelemetryEntry_device_metrics_tag 2
|
||||
#define meshtastic_NodeEnvironmentEntry_num_tag 1
|
||||
#define meshtastic_NodeEnvironmentEntry_environment_metrics_tag 2
|
||||
#define meshtastic_NodeStatusEntry_num_tag 1
|
||||
#define meshtastic_NodeStatusEntry_status_tag 2
|
||||
#define meshtastic_NodeDatabase_version_tag 1
|
||||
#define meshtastic_NodeDatabase_nodes_tag 2
|
||||
#define meshtastic_NodeDatabase_positions_tag 3
|
||||
#define meshtastic_NodeDatabase_telemetry_tag 4
|
||||
#define meshtastic_NodeDatabase_status_tag 5
|
||||
#define meshtastic_NodeDatabase_environment_tag 6
|
||||
#define meshtastic_ChannelFile_channels_tag 1
|
||||
#define meshtastic_ChannelFile_version_tag 2
|
||||
#define meshtastic_BackupPreferences_version_tag 1
|
||||
@@ -275,23 +316,19 @@ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9)
|
||||
|
||||
#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \
|
||||
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
|
||||
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, channel, 7) \
|
||||
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
|
||||
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
|
||||
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
|
||||
X(a, STATIC, SINGULAR, UINT32, bitfield, 13)
|
||||
X(a, STATIC, SINGULAR, UINT32, bitfield, 13) \
|
||||
X(a, STATIC, SINGULAR, STRING, long_name, 14) \
|
||||
X(a, STATIC, SINGULAR, STRING, short_name, 15) \
|
||||
X(a, STATIC, SINGULAR, UENUM, hw_model, 16) \
|
||||
X(a, STATIC, SINGULAR, UENUM, role, 17) \
|
||||
X(a, STATIC, SINGULAR, BYTES, public_key, 18)
|
||||
#define meshtastic_NodeInfoLite_CALLBACK NULL
|
||||
#define meshtastic_NodeInfoLite_DEFAULT NULL
|
||||
#define meshtastic_NodeInfoLite_user_MSGTYPE meshtastic_UserLite
|
||||
#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite
|
||||
#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics
|
||||
|
||||
#define meshtastic_DeviceState_FIELDLIST(X, a) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \
|
||||
@@ -312,13 +349,49 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13)
|
||||
#define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket
|
||||
#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin
|
||||
|
||||
#define meshtastic_NodePositionEntry_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, position, 2)
|
||||
#define meshtastic_NodePositionEntry_CALLBACK NULL
|
||||
#define meshtastic_NodePositionEntry_DEFAULT NULL
|
||||
#define meshtastic_NodePositionEntry_position_MSGTYPE meshtastic_PositionLite
|
||||
|
||||
#define meshtastic_NodeTelemetryEntry_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 2)
|
||||
#define meshtastic_NodeTelemetryEntry_CALLBACK NULL
|
||||
#define meshtastic_NodeTelemetryEntry_DEFAULT NULL
|
||||
#define meshtastic_NodeTelemetryEntry_device_metrics_MSGTYPE meshtastic_DeviceMetrics
|
||||
|
||||
#define meshtastic_NodeEnvironmentEntry_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, environment_metrics, 2)
|
||||
#define meshtastic_NodeEnvironmentEntry_CALLBACK NULL
|
||||
#define meshtastic_NodeEnvironmentEntry_DEFAULT NULL
|
||||
#define meshtastic_NodeEnvironmentEntry_environment_metrics_MSGTYPE meshtastic_EnvironmentMetrics
|
||||
|
||||
#define meshtastic_NodeStatusEntry_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, num, 1) \
|
||||
X(a, STATIC, OPTIONAL, MESSAGE, status, 2)
|
||||
#define meshtastic_NodeStatusEntry_CALLBACK NULL
|
||||
#define meshtastic_NodeStatusEntry_DEFAULT NULL
|
||||
#define meshtastic_NodeStatusEntry_status_MSGTYPE meshtastic_StatusMessage
|
||||
|
||||
#define meshtastic_NodeDatabase_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, version, 1) \
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2)
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) \
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, positions, 3) \
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, telemetry, 4) \
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, status, 5) \
|
||||
X(a, CALLBACK, REPEATED, MESSAGE, environment, 6)
|
||||
extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
|
||||
#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback
|
||||
#define meshtastic_NodeDatabase_DEFAULT NULL
|
||||
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite
|
||||
#define meshtastic_NodeDatabase_positions_MSGTYPE meshtastic_NodePositionEntry
|
||||
#define meshtastic_NodeDatabase_telemetry_MSGTYPE meshtastic_NodeTelemetryEntry
|
||||
#define meshtastic_NodeDatabase_status_MSGTYPE meshtastic_NodeStatusEntry
|
||||
#define meshtastic_NodeDatabase_environment_MSGTYPE meshtastic_NodeEnvironmentEntry
|
||||
|
||||
#define meshtastic_ChannelFile_FIELDLIST(X, a) \
|
||||
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \
|
||||
@@ -345,6 +418,10 @@ extern const pb_msgdesc_t meshtastic_PositionLite_msg;
|
||||
extern const pb_msgdesc_t meshtastic_UserLite_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DeviceState_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodePositionEntry_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeTelemetryEntry_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeEnvironmentEntry_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeStatusEntry_msg;
|
||||
extern const pb_msgdesc_t meshtastic_NodeDatabase_msg;
|
||||
extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
|
||||
extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
@@ -354,6 +431,10 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
#define meshtastic_UserLite_fields &meshtastic_UserLite_msg
|
||||
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
|
||||
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg
|
||||
#define meshtastic_NodePositionEntry_fields &meshtastic_NodePositionEntry_msg
|
||||
#define meshtastic_NodeTelemetryEntry_fields &meshtastic_NodeTelemetryEntry_msg
|
||||
#define meshtastic_NodeEnvironmentEntry_fields &meshtastic_NodeEnvironmentEntry_msg
|
||||
#define meshtastic_NodeStatusEntry_fields &meshtastic_NodeStatusEntry_msg
|
||||
#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg
|
||||
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
|
||||
#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg
|
||||
@@ -364,7 +445,11 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
||||
#define meshtastic_BackupPreferences_size 2432
|
||||
#define meshtastic_ChannelFile_size 718
|
||||
#define meshtastic_DeviceState_size 1737
|
||||
#define meshtastic_NodeInfoLite_size 196
|
||||
#define meshtastic_NodeEnvironmentEntry_size 170
|
||||
#define meshtastic_NodeInfoLite_size 105
|
||||
#define meshtastic_NodePositionEntry_size 36
|
||||
#define meshtastic_NodeStatusEntry_size 89
|
||||
#define meshtastic_NodeTelemetryEntry_size 35
|
||||
#define meshtastic_PositionLite_size 28
|
||||
#define meshtastic_UserLite_size 98
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -729,7 +729,7 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
uint32_t readIndex = 0;
|
||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
while (tempNodeInfo != NULL) {
|
||||
if (tempNodeInfo->has_user) {
|
||||
if (nodeInfoLiteHasUser(tempNodeInfo)) {
|
||||
JSONObject node;
|
||||
|
||||
char id[16];
|
||||
@@ -737,26 +737,26 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
node["id"] = new JSONValue(id);
|
||||
node["snr"] = new JSONValue(tempNodeInfo->snr);
|
||||
node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
|
||||
node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
|
||||
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
|
||||
node["position"] = new JSONValue();
|
||||
|
||||
if (nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
JSONObject position;
|
||||
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
|
||||
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
|
||||
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
|
||||
node["position"] = new JSONValue(position);
|
||||
meshtastic_PositionLite posLite;
|
||||
if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
|
||||
JSONObject position;
|
||||
position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
|
||||
position["longitude"] = new JSONValue((float)posLite.longitude_i * 1e-7);
|
||||
position["altitude"] = new JSONValue((int)posLite.altitude);
|
||||
node["position"] = new JSONValue(position);
|
||||
}
|
||||
}
|
||||
|
||||
node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
|
||||
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
|
||||
char macStr[18];
|
||||
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
|
||||
tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
|
||||
tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
|
||||
node["mac_address"] = new JSONValue(macStr);
|
||||
node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
|
||||
node["long_name"] = new JSONValue(tempNodeInfo->long_name);
|
||||
node["short_name"] = new JSONValue(tempNodeInfo->short_name);
|
||||
// mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
|
||||
node["mac_address"] = new JSONValue("00:00:00:00:00:00");
|
||||
node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
|
||||
|
||||
nodesArray.push_back(new JSONValue(node));
|
||||
}
|
||||
@@ -930,4 +930,4 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -37,9 +37,43 @@
|
||||
#define MAX_RX_NOTIFICATION_TOPHONE 2
|
||||
#endif
|
||||
|
||||
/// Verify baseline assumption of node size. If it increases, we need to reevaluate
|
||||
/// the impact of its memory footprint, notably on MAX_NUM_NODES.
|
||||
static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
|
||||
/// Tighten this when the slim header shrinks; loosen only with deliberate
|
||||
/// awareness of MAX_NUM_NODES impact per platform.
|
||||
static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
|
||||
|
||||
// Compile satellite NodeDBs out on STM32WL (and the status DB also follows
|
||||
// MESHTASTIC_EXCLUDE_STATUS).
|
||||
#ifndef MESHTASTIC_EXCLUDE_POSITIONDB
|
||||
#if defined(ARCH_STM32WL)
|
||||
#define MESHTASTIC_EXCLUDE_POSITIONDB 1
|
||||
#else
|
||||
#define MESHTASTIC_EXCLUDE_POSITIONDB 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MESHTASTIC_EXCLUDE_TELEMETRYDB
|
||||
#if defined(ARCH_STM32WL)
|
||||
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 1
|
||||
#else
|
||||
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
||||
#if defined(ARCH_STM32WL)
|
||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 1
|
||||
#else
|
||||
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MESHTASTIC_EXCLUDE_STATUSDB
|
||||
#if defined(ARCH_STM32WL) || defined(MESHTASTIC_EXCLUDE_STATUS)
|
||||
#define MESHTASTIC_EXCLUDE_STATUSDB 1
|
||||
#else
|
||||
#define MESHTASTIC_EXCLUDE_STATUSDB 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// max number of nodes allowed in the nodeDB
|
||||
#ifndef MAX_NUM_NODES
|
||||
|
||||
+15
-14
@@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
|
||||
// Automatically favorite the node that is using the admin key
|
||||
auto remoteNode = nodeDB->getMeshNode(mp.from);
|
||||
if (remoteNode && !remoteNode->is_favorite) {
|
||||
if (remoteNode && !nodeInfoLiteIsFavorite(remoteNode)) {
|
||||
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
||||
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
||||
// without the user doing so deliberately.
|
||||
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
|
||||
} else {
|
||||
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
|
||||
remoteNode->is_favorite = true;
|
||||
nodeInfoLiteSetBit(remoteNode, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -393,7 +393,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Client received set_favorite_node command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
|
||||
if (node != NULL) {
|
||||
node->is_favorite = true;
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||
@@ -404,7 +404,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Client received remove_favorite_node command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
|
||||
if (node != NULL) {
|
||||
node->is_favorite = false;
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||
@@ -415,11 +415,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Client received set_ignored_node command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
|
||||
if (node != NULL) {
|
||||
node->is_ignored = true;
|
||||
node->has_device_metrics = false;
|
||||
node->has_position = false;
|
||||
node->user.public_key.size = 0;
|
||||
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
|
||||
nodeDB->eraseNodeSatellites(node->num);
|
||||
node->public_key.size = 0;
|
||||
memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
}
|
||||
break;
|
||||
@@ -428,7 +427,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Client received remove_ignored_node command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
|
||||
if (node != NULL) {
|
||||
node->is_ignored = false;
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
}
|
||||
break;
|
||||
@@ -437,7 +436,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
LOG_INFO("Client received toggle_muted_node command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
|
||||
if (node != NULL) {
|
||||
node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT);
|
||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_MUTED_MASK, !nodeInfoLiteIsMuted(node));
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
}
|
||||
break;
|
||||
@@ -445,9 +444,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
|
||||
case meshtastic_AdminMessage_set_fixed_position_tag: {
|
||||
LOG_INFO("Client received set_fixed_position command");
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
node->has_position = true;
|
||||
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position);
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
// Route the fixed position through updatePosition so it lands in the
|
||||
// satellite map (or, on builds with PositionDB excluded, just sets
|
||||
// localPosition for the local broadcast path).
|
||||
nodeDB->updatePosition(node->num, r->set_fixed_position, RX_SRC_LOCAL);
|
||||
nodeDB->setLocalPosition(r->set_fixed_position);
|
||||
config.position.fixed_position = true;
|
||||
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
||||
|
||||
@@ -141,7 +141,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
|
||||
static bool returnToCannedList = false;
|
||||
bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
return node && node->has_user && node->user.public_key.size > 0;
|
||||
return nodeInfoLiteHasUser(node) && node->public_key.size > 0;
|
||||
}
|
||||
/**
|
||||
* @brief Items in array this->messages will be set to be pointing on the right
|
||||
@@ -262,10 +262,10 @@ void CannedMessageModule::updateDestinationSelectionList()
|
||||
|
||||
for (size_t i = 0; i < numMeshNodes; ++i) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32)
|
||||
if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32)
|
||||
continue;
|
||||
|
||||
const String &nodeName = node->user.long_name;
|
||||
const String &nodeName = node->long_name;
|
||||
|
||||
if (searchQuery.length() == 0) {
|
||||
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
||||
@@ -1054,7 +1054,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
||||
}
|
||||
|
||||
NodeNum myNodeNum = nodeDB->getNodeNum();
|
||||
if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) {
|
||||
if (node && node->num != myNodeNum && nodeInfoLiteHasUser(node) && node->public_key.size == 32) {
|
||||
p->pki_encrypted = true;
|
||||
p->channel = 0; // force PKI
|
||||
}
|
||||
@@ -1415,8 +1415,8 @@ const char *CannedMessageModule::getNodeName(NodeNum node)
|
||||
return "Broadcast";
|
||||
|
||||
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
||||
if (info && info->has_user && strlen(info->user.long_name) > 0) {
|
||||
return info->user.long_name;
|
||||
if (nodeInfoLiteHasUser(info) && strlen(info->long_name) > 0) {
|
||||
return info->long_name;
|
||||
}
|
||||
|
||||
static char fallback[12];
|
||||
@@ -1723,17 +1723,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
||||
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
||||
if (node) {
|
||||
if (display->getWidth() <= 64) {
|
||||
entryText = node->user.short_name;
|
||||
} else if (node->user.long_name[0]) {
|
||||
entryText = node->user.long_name;
|
||||
entryText = node->short_name;
|
||||
} else if (node->long_name[0]) {
|
||||
entryText = node->long_name;
|
||||
} else {
|
||||
entryText = node->user.short_name;
|
||||
entryText = node->short_name;
|
||||
}
|
||||
}
|
||||
|
||||
int availWidth = display->getWidth() -
|
||||
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
|
||||
((node && node->is_favorite) ? 10 : 0);
|
||||
((nodeInfoLiteIsFavorite(node)) ? 10 : 0);
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
char truncatedEntry[96];
|
||||
@@ -1742,7 +1742,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
||||
entryText = truncatedEntry;
|
||||
|
||||
// Prepend "* " if this is a favorite
|
||||
if (node && node->is_favorite) {
|
||||
if (nodeInfoLiteIsFavorite(node)) {
|
||||
entryText = "* " + entryText;
|
||||
}
|
||||
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
|
||||
|
||||
@@ -372,7 +372,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
|
||||
// If we receive a direct message and the receipent is us, apply DM mute setting
|
||||
// Else we just handle it as not muted.
|
||||
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
|
||||
bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0))
|
||||
bool is_muted = isDmToUs ? nodeInfoLiteIsMuted(sender)
|
||||
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
|
||||
|
||||
const bool buzzerModeIsDirectOnly =
|
||||
|
||||
@@ -10,6 +10,18 @@
|
||||
|
||||
KeyVerificationModule *keyVerificationModule;
|
||||
|
||||
namespace
|
||||
{
|
||||
void copyNodeLongNameOrUnknown(char *dest, size_t destSize, const meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
if (!dest || destSize == 0)
|
||||
return;
|
||||
const char *name = (node && nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : "Unknown";
|
||||
strncpy(dest, name, destSize - 1);
|
||||
dest[destSize - 1] = '\0';
|
||||
}
|
||||
} // namespace
|
||||
|
||||
KeyVerificationModule::KeyVerificationModule()
|
||||
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
|
||||
{
|
||||
@@ -37,7 +49,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -72,9 +85,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
sprintf(cn->message, "Enter Security Number for Key Verification");
|
||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
|
||||
cn->payload_variant.key_verification_number_request.nonce = currentNonce;
|
||||
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc
|
||||
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||
sizeof(cn->payload_variant.key_verification_number_request.remote_longname));
|
||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_request.remote_longname,
|
||||
sizeof(cn->payload_variant.key_verification_number_request.remote_longname),
|
||||
nodeDB->getMeshNode(currentRemoteNode));
|
||||
service->sendClientNotification(cn);
|
||||
LOG_INFO("Received hash2");
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
@@ -95,7 +108,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
[=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -104,9 +118,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
|
||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
||||
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
||||
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
|
||||
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||
sizeof(cn->payload_variant.key_verification_final.remote_longname));
|
||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
|
||||
sizeof(cn->payload_variant.key_verification_final.remote_longname),
|
||||
nodeDB->getMeshNode(currentRemoteNode));
|
||||
cn->payload_variant.key_verification_final.isSender = false;
|
||||
service->sendClientNotification(cn);
|
||||
|
||||
@@ -202,9 +216,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
currentSecurityNumber % 1000);
|
||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
|
||||
cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
|
||||
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc
|
||||
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname));
|
||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_inform.remote_longname,
|
||||
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname),
|
||||
nodeDB->getMeshNode(currentRemoteNode));
|
||||
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
|
||||
service->sendClientNotification(cn);
|
||||
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
|
||||
@@ -219,7 +233,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
LOG_WARN("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) {
|
||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
return; // should we throw an error here?
|
||||
}
|
||||
@@ -232,7 +246,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
|
||||
hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size);
|
||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
hash.reset();
|
||||
@@ -265,9 +279,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
|
||||
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
|
||||
cn->payload_variant.key_verification_final.nonce = currentNonce;
|
||||
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
|
||||
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
|
||||
sizeof(cn->payload_variant.key_verification_final.remote_longname));
|
||||
copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
|
||||
sizeof(cn->payload_variant.key_verification_final.remote_longname),
|
||||
nodeDB->getMeshNode(currentRemoteNode));
|
||||
cn->payload_variant.key_verification_final.isSender = true;
|
||||
service->sendClientNotification(cn);
|
||||
LOG_INFO(message);
|
||||
@@ -310,4 +324,4 @@ void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
||||
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -183,8 +183,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
assert(node->has_position);
|
||||
const meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
|
||||
// configuration of POSITION packet
|
||||
// consider making this a function argument?
|
||||
@@ -194,7 +193,9 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
|
||||
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
|
||||
// if localPosition is totally empty, put our last saved position (lite) in there
|
||||
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
|
||||
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position));
|
||||
meshtastic_PositionLite cachedSelf;
|
||||
if (nodeDB->copyNodePosition(node->num, cachedSelf))
|
||||
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(cachedSelf));
|
||||
}
|
||||
localPosition.seq_number++;
|
||||
|
||||
@@ -420,7 +421,7 @@ int32_t PositionModule::runOnce()
|
||||
doDeepSleep(nightyNightMs, false, false);
|
||||
}
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node == nullptr)
|
||||
return RUNONCE_INTERVAL;
|
||||
|
||||
@@ -445,8 +446,11 @@ int32_t PositionModule::runOnce()
|
||||
} else if (nodeDB->hasValidPosition(node)) {
|
||||
lastGpsSend = now;
|
||||
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
meshtastic_PositionLite selfPos;
|
||||
if (nodeDB->copyNodePosition(node->num, selfPos)) {
|
||||
lastGpsLatitude = selfPos.latitude_i;
|
||||
lastGpsLongitude = selfPos.longitude_i;
|
||||
}
|
||||
|
||||
if (transmitHistory)
|
||||
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
|
||||
@@ -461,7 +465,10 @@ int32_t PositionModule::runOnce()
|
||||
if (nodeDB->hasValidPosition(node2)) {
|
||||
// The minimum time (in seconds) that would pass before we are able to send a new position packet.
|
||||
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||
meshtastic_PositionLite selfPos;
|
||||
if (!nodeDB->copyNodePosition(node->num, selfPos))
|
||||
return RUNONCE_INTERVAL; // Defensive: hasValidPosition should imply this is non-null
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
|
||||
msSinceLastSend = now - lastGpsSend;
|
||||
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
@@ -479,8 +486,8 @@ int32_t PositionModule::runOnce()
|
||||
msSinceLastSend, minimumTimeThreshold);
|
||||
|
||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
lastGpsLatitude = selfPos.latitude_i;
|
||||
lastGpsLongitude = selfPos.longitude_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,11 +570,14 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic
|
||||
|
||||
void PositionModule::handleNewPosition()
|
||||
{
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
|
||||
// We limit our GPS broadcasts to a max rate
|
||||
if (nodeDB->hasValidPosition(node2)) {
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
|
||||
meshtastic_PositionLite selfPos;
|
||||
if (!nodeDB->copyNodePosition(node->num, selfPos))
|
||||
return;
|
||||
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
|
||||
uint32_t msSinceLastSend = millis() - lastGpsSend;
|
||||
if (smartPosition.hasTraveledOverThreshold &&
|
||||
Throttle::execute(
|
||||
@@ -583,8 +593,8 @@ void PositionModule::handleNewPosition()
|
||||
minimumTimeThreshold);
|
||||
|
||||
// Set the current coords as our last ones, after we've compared distance with current and decided to send
|
||||
lastGpsLatitude = node->position.latitude_i;
|
||||
lastGpsLongitude = node->position.longitude_i;
|
||||
lastGpsLatitude = selfPos.latitude_i;
|
||||
lastGpsLongitude = selfPos.longitude_i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,26 +268,36 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
|
||||
fileToAppend.printf("??:??:??,"); // Time
|
||||
}
|
||||
|
||||
fileToAppend.printf("%d,", getFrom(&mp)); // From
|
||||
fileToAppend.printf("%s,", n->user.long_name); // Long Name
|
||||
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat
|
||||
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long
|
||||
fileToAppend.printf("%d,", getFrom(&mp)); // From
|
||||
fileToAppend.printf("%s,", n ? n->long_name : ""); // Long Name
|
||||
meshtastic_PositionLite senderPos;
|
||||
const bool haveSenderPos = nodeDB->copyNodePosition(getFrom(&mp), senderPos);
|
||||
if (haveSenderPos) {
|
||||
fileToAppend.printf("%f,", senderPos.latitude_i * 1e-7); // Sender Lat
|
||||
fileToAppend.printf("%f,", senderPos.longitude_i * 1e-7); // Sender Long
|
||||
} else {
|
||||
fileToAppend.printf("0.0,0.0,");
|
||||
}
|
||||
if (gpsStatus->getIsConnected() || config.position.fixed_position) {
|
||||
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
|
||||
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
|
||||
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
|
||||
} else {
|
||||
// When the phone API is in use, the node info will be updated with position
|
||||
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat
|
||||
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long
|
||||
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude
|
||||
meshtastic_PositionLite usPos;
|
||||
if (nodeDB->copyNodePosition(nodeDB->getNodeNum(), usPos)) {
|
||||
fileToAppend.printf("%f,", usPos.latitude_i * 1e-7); // RX Lat
|
||||
fileToAppend.printf("%f,", usPos.longitude_i * 1e-7); // RX Long
|
||||
fileToAppend.printf("%d,", usPos.altitude); // RX Altitude
|
||||
} else {
|
||||
fileToAppend.printf("0.0,0.0,0,");
|
||||
}
|
||||
}
|
||||
|
||||
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
|
||||
|
||||
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
|
||||
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7,
|
||||
if (haveSenderPos && senderPos.latitude_i && senderPos.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
|
||||
float distance = GeoCoord::latLongToMeter(senderPos.latitude_i * 1e-7, senderPos.longitude_i * 1e-7,
|
||||
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
|
||||
fileToAppend.printf("%f,", distance); // Distance in meters
|
||||
} else {
|
||||
@@ -340,4 +350,4 @@ bool RangeTestModuleRadio::removeFile()
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
|
||||
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
|
||||
if (!maybePKI)
|
||||
return false;
|
||||
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) &&
|
||||
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
|
||||
if (!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.from)) && !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.to)))
|
||||
return false;
|
||||
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
|
||||
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
|
||||
|
||||
@@ -250,9 +250,12 @@ int32_t SerialModule::runOnce()
|
||||
uint32_t readIndex = 0;
|
||||
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
while (tempNodeInfo != NULL) {
|
||||
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
if (nodeInfoLiteHasUser(tempNodeInfo) && nodeDB->hasValidPosition(tempNodeInfo)) {
|
||||
meshtastic_PositionLite pos;
|
||||
if (nodeDB->copyNodePosition(tempNodeInfo->num, pos)) {
|
||||
printWPL(outbuf, sizeof(outbuf), pos, tempNodeInfo->long_name, true);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
}
|
||||
}
|
||||
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
|
||||
}
|
||||
@@ -401,7 +404,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
||||
serialPrint->write(p.payload.bytes, p.payload.size);
|
||||
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
|
||||
const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
|
||||
serialPrint->println();
|
||||
serialPrint->printf("%s: %s", sender, p.payload.bytes);
|
||||
serialPrint->println();
|
||||
@@ -417,9 +420,13 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
|
||||
decoded = &scratch;
|
||||
}
|
||||
// send position packet as WPL to the serial port
|
||||
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name,
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
{
|
||||
meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp));
|
||||
const char *senderName = senderNode ? senderNode->long_name : "";
|
||||
printWPL(outbuf, sizeof(outbuf), *decoded, senderName,
|
||||
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
|
||||
serialPrint->printf("%s", outbuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,4 +721,4 @@ void SerialModule::processWXSerial()
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "StatusMessageModule.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "ProtobufModule.h"
|
||||
|
||||
StatusMessageModule *statusMessageModule;
|
||||
@@ -36,16 +37,8 @@ ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &
|
||||
|
||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||
|
||||
RecentStatus entry;
|
||||
entry.fromNodeId = mp.from;
|
||||
entry.statusText = incomingMessage.status;
|
||||
|
||||
recentReceived.push_back(std::move(entry));
|
||||
|
||||
// Keep only last MAX_RECENT_STATUSMESSAGES
|
||||
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
|
||||
recentReceived.erase(recentReceived.begin()); // drop oldest
|
||||
}
|
||||
if (nodeDB)
|
||||
nodeDB->setNodeStatus(mp.from, incomingMessage);
|
||||
}
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "SinglePortModule.h"
|
||||
#include "configuration.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
@@ -20,29 +18,16 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
||||
this->setInterval(1000 * 12 * 60 * 60);
|
||||
}
|
||||
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
||||
|
||||
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
|
||||
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
|
||||
}
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
struct RecentStatus {
|
||||
uint32_t fromNodeId; // mp.from
|
||||
std::string statusText; // incomingMessage.status
|
||||
};
|
||||
|
||||
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
/** Called to handle a particular incoming message. The cached most-recent
|
||||
* status for each node lives on NodeDB; use nodeDB->copyNodeStatus(num, ...).
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||
std::vector<RecentStatus> recentReceived;
|
||||
};
|
||||
|
||||
extern StatusMessageModule *statusMessageModule;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -515,6 +515,10 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
|
||||
packetPool.release(lastMeasurementPacket);
|
||||
|
||||
lastMeasurementPacket = packetPool.allocCopy(mp);
|
||||
|
||||
// Cache the latest env metrics per node on NodeDB so the phone can
|
||||
// pull last-known values across reboots and replays.
|
||||
nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO);
|
||||
}
|
||||
|
||||
return false; // Let others look at this message also if they want
|
||||
|
||||
@@ -492,12 +492,12 @@ TraceRouteModule::TraceRouteModule()
|
||||
const char *TraceRouteModule::getNodeName(NodeNum node)
|
||||
{
|
||||
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
||||
if (info && info->has_user) {
|
||||
if (strlen(info->user.short_name) > 0) {
|
||||
return info->user.short_name;
|
||||
if (nodeInfoLiteHasUser(info)) {
|
||||
if (strlen(info->short_name) > 0) {
|
||||
return info->short_name;
|
||||
}
|
||||
if (strlen(info->user.long_name) > 0) {
|
||||
return info->user.long_name;
|
||||
if (strlen(info->long_name) > 0) {
|
||||
return info->long_name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1226,9 +1226,9 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke
|
||||
// Fallback only when PSRAM cache is unavailable on this target.
|
||||
// In this mode we use the node-wide table maintained by NodeInfoModule.
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
|
||||
if (!node || !node->has_user)
|
||||
if (!nodeInfoLiteHasUser(node))
|
||||
return false;
|
||||
cachedUser = TypeConversions::ConvertToUser(node->num, node->user);
|
||||
cachedUser = TypeConversions::ConvertToUser(node);
|
||||
}
|
||||
|
||||
if (!sendResponse)
|
||||
|
||||
@@ -100,7 +100,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
char distStr[20] = "";
|
||||
|
||||
// Get our node, to use our own position
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// Match compass sizing/placement to favorite node screen logic.
|
||||
const int w = display->getWidth();
|
||||
@@ -147,8 +147,10 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
const char *statusLine2 = nullptr;
|
||||
|
||||
// Distance only needs our own position fix; compass/bearing additionally needs heading.
|
||||
if (hasOwnPositionFix) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
meshtastic_PositionLite ownPos;
|
||||
const bool haveOwnPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ownPos);
|
||||
if (hasOwnPositionFix && haveOwnPos) {
|
||||
const meshtastic_PositionLite &op = ownPos;
|
||||
const float d =
|
||||
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to);
|
||||
// Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's
|
||||
// likely they discovered each other via a channel we have downlink enabled for
|
||||
if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user))
|
||||
if (isToUs(p.get()) || (nodeInfoLiteHasUser(tx) && nodeInfoLiteHasUser(rx)))
|
||||
router->enqueueReceivedMessage(p.release());
|
||||
} else if (router &&
|
||||
perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key
|
||||
|
||||
@@ -319,10 +319,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
// Lambda function for adding a long name to the route
|
||||
auto addToRoute = [](JSONArray *route, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = node ? node->has_user : false;
|
||||
if (name_known)
|
||||
memcpy(long_name, node->user.long_name, sizeof(long_name));
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = nodeInfoLiteHasUser(node);
|
||||
if (name_known) {
|
||||
const size_t copy_len =
|
||||
(sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1;
|
||||
memcpy(long_name, node->long_name, copy_len);
|
||||
long_name[copy_len] = '\0';
|
||||
}
|
||||
route->push_back(new JSONValue(long_name));
|
||||
};
|
||||
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response)
|
||||
@@ -473,4 +477,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
|
||||
delete value;
|
||||
return jsonStr;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -292,9 +292,13 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
||||
auto addToRoute = [](JsonArray *route, NodeNum num) {
|
||||
char long_name[40] = "Unknown";
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
|
||||
bool name_known = node ? node->has_user : false;
|
||||
if (name_known)
|
||||
memcpy(long_name, node->user.long_name, sizeof(long_name));
|
||||
bool name_known = nodeInfoLiteHasUser(node);
|
||||
if (name_known) {
|
||||
const size_t copy_len =
|
||||
(sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1;
|
||||
memcpy(long_name, node->long_name, copy_len);
|
||||
long_name[copy_len] = '\0';
|
||||
}
|
||||
route->add(long_name);
|
||||
};
|
||||
|
||||
@@ -418,4 +422,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
|
||||
|
||||
return jsonStr;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -113,7 +113,7 @@ void test_DH25519(void)
|
||||
void test_PKC(void)
|
||||
{
|
||||
uint8_t private_key[32];
|
||||
meshtastic_UserLite_public_key_t public_key;
|
||||
meshtastic_NodeInfoLite_public_key_t public_key;
|
||||
uint8_t expected_shared[32];
|
||||
uint8_t expected_decrypted[32];
|
||||
uint8_t radioBytes[128] __attribute__((__aligned__));
|
||||
|
||||
@@ -641,30 +641,12 @@ void test_sender_zero_substituted(void)
|
||||
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
|
||||
}
|
||||
|
||||
void test_uninitialized_wasSeenRecently(void)
|
||||
{
|
||||
// Simulate uninitialized state — create a PacketHistory that looks uninitialized
|
||||
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one
|
||||
PacketHistory h(4);
|
||||
TEST_ASSERT_TRUE(h.initOk()); // sanity check
|
||||
h.~PacketHistory();
|
||||
|
||||
auto p = makePacket(0x1111, 100);
|
||||
TEST_ASSERT_FALSE(h.wasSeenRecently(&p));
|
||||
|
||||
// Reconstruct in place to allow proper destruction
|
||||
new (&h) PacketHistory(4);
|
||||
}
|
||||
|
||||
void test_uninitialized_wasRelayer(void)
|
||||
{
|
||||
PacketHistory h(4);
|
||||
h.~PacketHistory();
|
||||
|
||||
TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111));
|
||||
|
||||
new (&h) PacketHistory(4);
|
||||
}
|
||||
// NOTE: Tests for the !initOk() short-circuit in wasSeenRecently / wasRelayer
|
||||
// were removed: the only way to drive that branch from a test is to destruct
|
||||
// the object and then call methods on it, which is UB and trips ASAN under
|
||||
// `pio test -e coverage`. In practice the guard can only fire if `new[]` throws
|
||||
// (in which case the constructor doesn't return), so the lost coverage is
|
||||
// purely defensive.
|
||||
|
||||
void test_multiple_instances_independent(void)
|
||||
{
|
||||
@@ -819,8 +801,6 @@ void setup()
|
||||
// Group 10 — Edge Cases
|
||||
RUN_TEST(test_packet_id_zero_not_stored);
|
||||
RUN_TEST(test_sender_zero_substituted);
|
||||
RUN_TEST(test_uninitialized_wasSeenRecently);
|
||||
RUN_TEST(test_uninitialized_wasRelayer);
|
||||
RUN_TEST(test_multiple_instances_independent);
|
||||
|
||||
// Group 11 — Hash Table Stress
|
||||
|
||||
@@ -50,7 +50,7 @@ class MockNodeDB : public NodeDB
|
||||
hasCachedNode = true;
|
||||
cachedNodeNum = n;
|
||||
cachedNode.num = n;
|
||||
cachedNode.has_user = true;
|
||||
cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -494,9 +494,9 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
|
||||
TEST_ASSERT_NOT_NULL(requestor);
|
||||
TEST_ASSERT_TRUE(requestor->has_user);
|
||||
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name);
|
||||
TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name);
|
||||
TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
|
||||
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name);
|
||||
TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name);
|
||||
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
Reference in New Issue
Block a user