Compare commits

...
34 Commits
Author SHA1 Message Date
Ben Meadors f6423aca76 Fix code block formatting in copilot instructions 2026-05-09 13:38:19 -05:00
Ben Meadors fc5670cdad Remove tests for uninitialized PacketHistory state due to undefined behavior 2026-05-09 13:19:48 -05:00
Ben Meadors ba81da8de0 Add NodeDB layout v25 documentation and migration guidelines 2026-05-09 12:56:00 -05:00
Ben Meadors 3ca23400ef Change pointer types for mesh node references to const in multiple applets 2026-05-09 12:27:08 -05:00
Ben Meadors b3497cf688 Change pointer type for mesh node in set_favorite function 2026-05-09 11:34:54 -05:00
copilot-swe-agent[bot]andthebentern c873833123 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>
2026-05-09 15:08:23 +00:00
copilot-swe-agent[bot]andthebentern 3c569f6672 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>
2026-05-09 13:44:21 +00:00
Ben Meadors c12a3916ea Trunk 2026-05-09 08:25:48 -05:00
Ben Meadors abbcb57958 Fixes 2026-05-09 08:24:18 -05:00
copilot-swe-agent[bot]andthebentern a0daaf6b5c 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>
2026-05-09 12:45:51 +00:00
Ben Meadors 589634c18d Extract legacy NodeDatabase migration 2026-05-09 07:41:00 -05:00
Ben MeadorsandCopilot Autofix powered by AI 37b3becdcd Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-09 07:38:48 -05:00
Ben Meadors 51e9696f11 Trunk 2026-05-09 06:51:41 -05:00
copilot-swe-agent[bot]andthebentern 796d4552f9 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>
2026-05-09 02:23:46 +00:00
copilot-swe-agent[bot]andthebentern ebc5b63035 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>
2026-05-09 02:18:57 +00:00
Ben Meadors f9ab2b2cc0 Merge branch 'develop' into nodedb-shrink 2026-05-08 20:16:33 -05:00
Ben Meadors 093bc49a73 Develop ref 2026-05-08 20:16:08 -05:00
github-actions[bot]andthebentern f63716c322 Automated version bumps (#10419)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-05-08 19:05:11 -05:00
5e2ca8aed4 LR2021 radio on NRF_Promicro (#10401)
* LR2021 radio on NRF_Promicro

Co-authored-by: Copilot <copilot@github.com>

* Refactor LR2021 interface includes and conditional compilation for improved clarity

Co-authored-by: Copilot <copilot@github.com>

* Refactor LR20x0 interface: remove unused includes and update comments for clarity

* Fix LR2021 max power definitions and add radio type detection tests

* remove potato radio type detection tests

* Include placeholder for DCDC - currently requires godmode

* Added godmode features - not enabled by default

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-07 20:24:39 -05:00
Ben Meadors 36827d6207 Refactor node mute handling to use dedicated functions for clarity and consistency 2026-05-07 12:23:48 -05:00
Ben Meadors ef02cd1dd1 Merge branch 'develop' into nodedb-shrink 2026-05-07 12:12:34 -05:00
Clive BlackledgeandBen Meadors 4553d1e0b1 Skip MQTT allocation when disabled (#10411)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-07 11:51:55 -05:00
Ben Meadors 784e3748e2 Merge remote-tracking branch 'origin/master' into develop 2026-05-07 11:45:04 -05:00
Ben Meadors a2a0417596 Regen 2026-05-07 09:11:49 -05:00
Ben Meadors f5c2c99150 2.8: NodeDB refactor to decouple satellite entries and decrease size 2026-05-07 08:37:56 -05:00
Tom b11d29ff31 fix(mesh): update reconfigure methods to return true instead of RADIOLIB_ERR_NONE (#10407) 2026-05-07 05:57:34 -05:00
renovate[bot] b246bcd72e Update libpax digest to df42474 (#10406)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 20:52:30 -05:00
33e2bb70e6 Enhance GPS search failure handling backoff logic (#10404)
* Enhance GPS search failure handling backoff logic

* Potential fix for pull request finding

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

* Remove stray submodule gitlink for .claude worktree

A 160000 (gitlink) entry for .claude/worktrees/naughty-payne-60fdb7
pointing at f2923590bc was accidentally committed in 9db15780f. The
path isn't a real submodule — it's a Claude Code agent worktree that
shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:44:04 -05:00
Ben Meadors 220bb4d186 Smart pointers and memory management cleanup (#10400)
* Refactor memory management in Syslog and StoreForwardModule

* Implement destructor for Lock

* Refactor RotaryEncoder and PacketHistory to use smart pointers for better memory management

* CH341 should use unique_ptr for improved memory management

* Fix checks in PH

* Improve Syslog::vlogf to handle variable argument lists more safely

* Fix initOk method to use nullptr for null pointer check
2026-05-06 15:33:59 -05:00
jessm33andjessm33 6e810741f3 Fix GPS initialization logic for Portduino configuration (#10395)
Co-authored-by: jessm33 <root@example.com>
2026-05-05 17:28:22 -05:00
jessm33andjessm33 cdc47a2aea Fix GPS initialization logic for Portduino configuration (#10395)
Co-authored-by: jessm33 <root@example.com>
2026-05-05 14:17:04 -05:00
Ben Meadors 603cce2988 Add informSearchFailed method to update GPS power state handling (#10394) 2026-05-05 10:12:50 -05:00
fcef46f4b0 dependency swap - INA3221Sensor (#10379)
* dependency swap - INA3221Sensor
update INA3221 initialization and measurement methods for compatibility with Rob Tillaart's library

Co-authored-by: Copilot <copilot@github.com>

* Addresses copilot review

Co-authored-by: Copilot <copilot@github.com>

* Potential fix for pull request finding

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

* Refine comments on USB detection and INA3221

Updated comments regarding USB detection and INA3221 usage.

* Fix static_assert conditions for INA3221 channel definitions

Co-authored-by: Copilot <copilot@github.com>

* moved macro defines earlier to allow better use.

Co-authored-by: Copilot <copilot@github.com>

* Add raw register read methods for bus voltage and shunt current in INA3221Sensor

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 06:52:07 -05:00
renovate[bot] d559af8477 Update LovyanGFX to v1.2.21 (#10373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:29:04 +02:00
112 changed files with 3448 additions and 793 deletions
+89
View File
@@ -135,6 +135,95 @@ On top of authorization, any remote admin message that **mutates** state (not a
- **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast. - **Channel 0 PSK change** → every peer must re-learn the channel hash; cached NodeInfo becomes temporarily unreachable until the next broadcast.
- **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo. - **`security.private_key` blanked via admin** → regenerates both halves (unless in Ham mode) and propagates the new public key via NodeInfo.
## NodeDB Layout (v25)
`DEVICESTATE_CUR_VER = 25`, `DEVICESTATE_MIN_VER = 24`. The on-device NodeDB was split in v25 into a slim header table plus four optional satellite stores. Older v24 saves auto-migrate at boot. Old training-data instincts (`node->user.long_name`, `node->position.latitude_i`, `node->is_favorite`, `node->device_metrics.battery_level`) are wrong now — the fields aren't there. Read this section before touching anything that walks `nodeDB->meshNodes`.
### Slim `NodeInfoLite`
`UserLite` is flattened onto `NodeInfoLite` (no nested sub-message); `position` and `device_metrics` are removed entirely (tags reserved). MAC address is dropped. Long names are capped at 25 chars (`max_size:25` in `deviceonly.options`); `hw_model` and `role` are `int_size:8`. Encoded size dropped from ~166 B → ~105 B per node.
Booleans are bit-packed into `NodeInfoLite.bitfield`. **Do not read or write the bits directly** — use the inline helpers in `src/mesh/NodeDB.h`:
```cpp
nodeInfoLiteHasUser(n) // bit 5 — user fields populated
nodeInfoLiteIsFavorite(n) // bit 3
nodeInfoLiteIsIgnored(n) // bit 4
nodeInfoLiteIsMuted(n) // bit 1
nodeInfoLiteIsLicensed(n) // bit 6 — Ham mode peer
nodeInfoLiteIsKeyManuallyVerified(n) // bit 0
nodeInfoLiteHasIsUnmessagable(n) // bit 8 — "is_unmessagable was sent"
nodeInfoLiteIsUnmessagable(n) // bit 7
// via_mqtt is bit 2 (mask exposed; predicate uses the mask directly)
nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true); // setter
```
### Satellite stores
Four `std::unordered_map<NodeNum, …>` members on `NodeDB`, each gated by its own build flag:
| Map | Value type | Build flag |
| ----------------- | ------------------------------- | ---------------------------------- |
| `nodePositions` | `meshtastic_PositionLite` | `MESHTASTIC_EXCLUDE_POSITIONDB` |
| `nodeTelemetry` | `meshtastic_DeviceMetrics` | `MESHTASTIC_EXCLUDE_TELEMETRYDB` |
| `nodeEnvironment` | `meshtastic_EnvironmentMetrics` | `MESHTASTIC_EXCLUDE_ENVIRONMENTDB` |
| `nodeStatus` | `meshtastic_StatusMessage` | `MESHTASTIC_EXCLUDE_STATUSDB` |
Defaults are ON (i.e., maps **excluded**) for STM32WL only — see `src/mesh/mesh-pb-constants.h`. On every other arch all four maps are present. When excluded, the map member is absent and the corresponding accessors return `false`.
All four maps are guarded by **`mutable concurrency::Lock satelliteMutex`** — concurrent access from receive threads, the phone API state machine, and the renderer is the rule, not the exception.
### Accessor convention
**Never hand out pointers into the maps.** Use the copy-out accessors on `NodeDB`:
```cpp
bool copyNodePosition(NodeNum, meshtastic_PositionLite &out) const;
bool copyNodeTelemetry(NodeNum, meshtastic_DeviceMetrics &out) const;
bool copyNodeEnvironment(NodeNum, meshtastic_EnvironmentMetrics &out) const;
bool copyNodeStatus(NodeNum, meshtastic_StatusMessage &out) const;
```
Each takes the lock, copies the value if present, returns `false` if the entry is absent or the DB is excluded. Pass-by-out-param is deliberate — pointer-style accessors would invite UAF and lock-leak bugs across the renderer. The "has any X" convenience predicates (`hasValidPosition` etc.) are implemented in terms of these.
Writers go through `setNodeStatus`, `updatePosition`, `updateTelemetry` (which dispatches on `which_variant` for device vs environment metrics) — these own the lock and the eviction hooks.
### Eviction
Every code path that drops a node from the header table must also evict the satellites. The single chokepoint is `eraseNodeSatellites(NodeNum)`; it's already called from `getOrCreateMeshNode`'s oldest-boring eviction, `removeNodeByNum`, both branches of `resetNodes`, `cleanupMeshDB`, `addFromContact`'s ignored-branch, and `AdminModule`'s `set_ignored_node`. Add new eviction sites here, not by calling `.erase()` directly.
### Gradient sync (opt-in via special nonces)
`client_capabilities` is **not** a thing in this branch. Phone clients opt into the new sync flow by sending one of two values in the `ToRadio.want_config_id`:
- `SPECIAL_NONCE_GRADIENT_SYNC` (69422) — full config + thin NodeInfo + replay phases.
- `SPECIAL_NONCE_GRADIENT_ONLY_NODES` (69423) — skip config segments, NodeInfo + replay only.
`PhoneAPI::clientWantsGradientSync()` is the single switch. When true, `STATE_SEND_OTHER_NODEINFOS` is followed by:
```text
STATE_REPLAY_POSITIONS → STATE_REPLAY_TELEMETRY → STATE_REPLAY_ENVIRONMENT → STATE_REPLAY_STATUS
```
Each replay phase walks the corresponding satellite map and emits synthetic `MeshPacket`s on the matching portnum (`POSITION_APP`, `TELEMETRY_APP` for both device + environment variants, `STATUS_MESSAGE_APP`). Legacy clients (no special nonce) get the bundled-NodeInfo path with position/device_metrics joined back in by `ConvertToNodeInfo(lite, pos*, dm*)` — wire bytes are byte-identical to pre-v25 for them.
`ConvertToNodeInfoThin(lite)` is the gradient-sync emitter (no position/telemetry).
### v24 → v25 migration
The legacy migration code lives in **`src/mesh/NodeDBLegacyMigration.cpp`**, not in `NodeDB.cpp`. It owns the `meshtastic_NodeDatabase_Legacy` callback and `NodeDB::migrateLegacyNodeDatabase()`. The legacy proto descriptor is `protobufs/meshtastic/deviceonly_legacy.proto` (only included by the migration TU). The boot path peeks the file's leading version tag, runs the migration if `version < 25`, then re-saves in v25 layout. The legacy descriptor is scheduled for removal once `DEVICESTATE_MIN_VER` is bumped.
### Read-site rules of thumb
- Never `node->position.X` / `node->device_metrics.X` — those fields no longer exist. Pull from the satellite map via `copyNodePosition` / `copyNodeTelemetry`.
- Never `node->user.long_name` — `long_name`, `short_name`, `public_key`, `hw_model`, `role`, `macaddr` (gone), `is_licensed`, `is_unmessagable` are flat on `NodeInfoLite`.
- Never `node->is_favorite` / `node->is_ignored` / `node->via_mqtt` / `node->is_key_manually_verified` — use the bitfield helpers.
- Never assume `nodeDB->getMeshNode(num)->position.time` — call `copyNodePosition` and check the return.
- Don't lock `satelliteMutex` yourself in renderer code; the copy-out accessors already do.
Unit tests for the conversion layer live in `test/test_type_conversions/test_main.cpp` (Unity) — bitfield round-trips, `long_name` truncation, thin-vs-full conversions. Add cases there when extending the schema.
## Project Structure ## Project Structure
``` ```
@@ -87,6 +87,9 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.24" date="2026-05-08">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
</release>
<release version="2.7.23" date="2026-04-14"> <release version="2.7.23" date="2026-04-14">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.23</url>
</release> </release>
+6
View File
@@ -1,3 +1,9 @@
meshtasticd (2.7.24.0) unstable; urgency=medium
* Version 2.7.24
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 08 May 2026 10:44:12 +0000
meshtasticd (2.7.23.0) unstable; urgency=medium meshtasticd (2.7.23.0) unstable; urgency=medium
* Version 2.7.23 * Version 2.7.23
+2 -2
View File
@@ -170,8 +170,8 @@ lib_deps =
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614 # renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow # renovate: datasource=github-tags depName=INA3221_RT packageName=RobTillaart/INA3221_RT
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass # renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU # renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
+15 -18
View File
@@ -26,6 +26,8 @@ SOFTWARE.*/
#include "DebugConfiguration.h" #include "DebugConfiguration.h"
#include <memory>
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/PortduinoGlue.h"
#endif #endif
@@ -119,27 +121,22 @@ bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args)
bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args) bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args)
{ {
char *message; // First measure the formatted length using a copy of args; passing args directly
size_t initialLen; // to vsnprintf consumes it, and reusing a consumed va_list is undefined behavior.
size_t len; va_list args_measure;
bool result; va_copy(args_measure, args);
int needed = vsnprintf(nullptr, 0, fmt, args_measure);
va_end(args_measure);
initialLen = strlen(fmt); if (needed < 0)
return false; // encoding error
message = new char[initialLen + 1]; auto message = std::unique_ptr<char[]>(new char[static_cast<size_t>(needed) + 1]);
int written = vsnprintf(message.get(), static_cast<size_t>(needed) + 1, fmt, args);
if (written < 0)
return false;
len = vsnprintf(message, initialLen + 1, fmt, args); return this->_sendLog(pri, appName, message.get());
if (len > initialLen) {
delete[] message;
message = new char[len + 1];
vsnprintf(message, len + 1, fmt, args);
}
result = this->_sendLog(pri, appName, message);
delete[] message;
return result;
} }
inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message) inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message)
+3 -6
View File
@@ -53,8 +53,7 @@ class GPSStatus : public Status
int32_t getLatitude() const int32_t getLatitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.latitude_i;
return node->position.latitude_i;
} else { } else {
return p.latitude_i; return p.latitude_i;
} }
@@ -63,8 +62,7 @@ class GPSStatus : public Status
int32_t getLongitude() const int32_t getLongitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.longitude_i;
return node->position.longitude_i;
} else { } else {
return p.longitude_i; return p.longitude_i;
} }
@@ -73,8 +71,7 @@ class GPSStatus : public Status
int32_t getAltitude() const int32_t getAltitude() const
{ {
if (config.position.fixed_position) { if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); return localPosition.altitude;
return node->position.altitude;
} else { } else {
return p.altitude; return p.altitude;
} }
+7
View File
@@ -14,6 +14,11 @@ Lock::Lock() : handle(xSemaphoreCreateBinary())
} }
} }
Lock::~Lock()
{
vSemaphoreDelete(handle);
}
void Lock::lock() void Lock::lock()
{ {
if (xSemaphoreTake(handle, portMAX_DELAY) == false) { if (xSemaphoreTake(handle, portMAX_DELAY) == false) {
@@ -30,6 +35,8 @@ void Lock::unlock()
#else #else
Lock::Lock() {} Lock::Lock() {}
Lock::~Lock() {}
void Lock::lock() {} void Lock::lock() {}
void Lock::unlock() {} void Lock::unlock() {}
+1
View File
@@ -12,6 +12,7 @@ class Lock
{ {
public: public:
Lock(); Lock();
~Lock();
Lock(const Lock &) = delete; Lock(const Lock &) = delete;
Lock &operator=(const Lock &) = delete; Lock &operator=(const Lock &) = delete;
+2 -1
View File
@@ -11,7 +11,8 @@ enum LoRaRadioType {
SX1280_RADIO, SX1280_RADIO,
LR1110_RADIO, LR1110_RADIO,
LR1120_RADIO, LR1120_RADIO,
LR1121_RADIO LR1121_RADIO,
LR2021_RADIO
}; };
extern LoRaRadioType radioType; extern LoRaRadioType radioType;
+11 -3
View File
@@ -1025,10 +1025,13 @@ void GPS::up()
setPowerState(GPS_ACTIVE); setPowerState(GPS_ACTIVE);
} }
// We've got a GPS lock. Enter a low power state, potentially. // We've finished a GPS search cycle (lock or timeout). Enter a low power state, potentially.
void GPS::down() void GPS::down()
{ {
scheduling.informGotLock(); if (hasValidLocation)
scheduling.informGotLock();
else
scheduling.informSearchFailed();
uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs(); uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs();
uint32_t sleepTime = scheduling.msUntilNextSearch(); uint32_t sleepTime = scheduling.msUntilNextSearch();
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
@@ -1553,7 +1556,12 @@ std::unique_ptr<GPS> GPS::createGps()
_en_gpio = PIN_GPS_EN; _en_gpio = PIN_GPS_EN;
#endif #endif
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
if (!portduino_config.has_gps) if (portduino_config.has_gps) {
// These need to set as flags so later checks will pass on native and GPS will work.
// They are not used for any hardware access.
_rx_gpio = 1;
_tx_gpio = 1;
} else
return nullptr; return nullptr;
#endif #endif
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
+34 -2
View File
@@ -15,6 +15,19 @@ void GPSUpdateScheduling::informGotLock()
searchEndedMs = millis(); searchEndedMs = millis();
LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000); LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000);
updateLockTimePrediction(); updateLockTimePrediction();
consecutiveFailures = 0; // Drop back to fast cadence as soon as we acquire any fix
}
// Search finished without obtaining a fix. We still need to mark the end time so
// the next sleep is timed correctly, but we must not feed the timeout duration
// into predictedMsToGetLock — doing so poisons msUntilNextSearch() and causes
// down() to fall into GPS_IDLE, leaving the chip awake on subsequent indoor cycles.
void GPSUpdateScheduling::informSearchFailed()
{
searchEndedMs = millis();
consecutiveFailures++;
LOG_DEBUG("GPS search ended without fix after %us (consecutive failures: %u)", (searchEndedMs - searchStartedMs) / 1000,
consecutiveFailures);
} }
// Clear old lock-time prediction data. // Clear old lock-time prediction data.
@@ -25,6 +38,7 @@ void GPSUpdateScheduling::reset()
searchEndedMs = 0; searchEndedMs = 0;
searchCount = 0; searchCount = 0;
predictedMsToGetLock = 0; predictedMsToGetLock = 0;
consecutiveFailures = 0;
} }
// How many milliseconds before we should next search for GPS position // How many milliseconds before we should next search for GPS position
@@ -36,6 +50,20 @@ uint32_t GPSUpdateScheduling::msUntilNextSearch()
// Target interval (seconds), between GPS updates // Target interval (seconds), between GPS updates
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval); uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);
// After a failed search, back off: indoors / no-sky environments will keep failing,
// so wake at most once per broadcast interval rather than once per gps_update_interval.
// Capped at 1 hour so a user-configured very-long broadcast interval still retries
// periodically (in case conditions change). Reset on any successful lock.
if (consecutiveFailures > 0) {
constexpr uint32_t failureRetryCapMs = 60UL * 60UL * 1000UL; // 1 hour cap
uint32_t failureSleepMs =
Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs);
if (failureSleepMs > failureRetryCapMs)
failureSleepMs = failureRetryCapMs;
if (updateInterval < failureSleepMs)
updateInterval = failureSleepMs;
}
// Check how long until we should start searching, to hopefully hit our target interval // Check how long until we should start searching, to hopefully hit our target interval
uint32_t dueAtMs = searchEndedMs + updateInterval; uint32_t dueAtMs = searchEndedMs + updateInterval;
uint32_t compensatedStart = dueAtMs - predictedMsToGetLock; uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
@@ -71,14 +99,18 @@ bool GPSUpdateScheduling::isUpdateDue()
bool GPSUpdateScheduling::searchedTooLong() bool GPSUpdateScheduling::searchedTooLong()
{ {
constexpr uint32_t oneMinuteMs = 60UL * 1000UL; constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
constexpr uint32_t postFailureSearchMs = 5UL * oneMinuteMs; // Tighter dwell once we know the environment is hostile
uint32_t elapsed = elapsedSearchMs(); uint32_t elapsed = elapsedSearchMs();
// Anything over 15 minutes is too long, regardless of the broadcast interval. // Anything over 15 minutes is too long, regardless of the broadcast interval.
// TODO: Make a smarter algorithm that backs off the search dwell time when not getting a lock.
if (elapsed > maxSearchClampMs) if (elapsed > maxSearchClampMs)
return true; return true;
// After a prior failed search, shorten the dwell
if (consecutiveFailures > 0 && elapsed > postFailureSearchMs)
return true;
uint32_t minimumOrConfiguredSecs = uint32_t minimumOrConfiguredSecs =
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs); Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs); uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
+3 -1
View File
@@ -8,7 +8,8 @@ class GPSUpdateScheduling
public: public:
// Marks the time of these events, for calculation use // Marks the time of these events, for calculation use
void informSearching(); void informSearching();
void informGotLock(); // Predicted lock-time is recalculated here void informGotLock(); // Predicted lock-time is recalculated here
void informSearchFailed(); // Search ended without a fix; prediction is left untouched
void reset(); // Reset the prediction - after GPS::disable() / GPS::enable() void reset(); // Reset the prediction - after GPS::disable() / GPS::enable()
bool isUpdateDue(); // Is it time to begin searching for a GPS position? bool isUpdateDue(); // Is it time to begin searching for a GPS position?
@@ -24,6 +25,7 @@ class GPSUpdateScheduling
uint32_t searchEndedMs = 0; uint32_t searchEndedMs = 0;
uint32_t searchCount = 0; uint32_t searchCount = 0;
uint32_t predictedMsToGetLock = 0; uint32_t predictedMsToGetLock = 0;
uint32_t consecutiveFailures = 0; // Count of search cycles that ended without a fix; reset on lock
const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time". const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time".
}; };
+2 -2
View File
@@ -1285,7 +1285,7 @@ void Screen::setFrames(FrameFocus focus)
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { if (n && n->num != nodeDB->getNodeNum() && nodeInfoLiteIsFavorite(n)) {
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode); favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
} }
} }
@@ -1670,7 +1670,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel = const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; const char *longName = nodeInfoLiteHasUser(node) ? node->long_name : nullptr;
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes); const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
+18 -22
View File
@@ -845,10 +845,10 @@ void menuHandler::messageViewModeMenu()
// Encode peers // Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) { for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i]; uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer); const auto *node = nodeDB->getMeshNode(peer);
std::string name; std::string name;
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
name = sanitizeString(node->user.long_name).substr(0, 15); name = sanitizeString(node->long_name).substr(0, 15);
else { else {
char buf[20]; char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer); snprintf(buf, sizeof(buf), "Node %08X", peer);
@@ -1355,14 +1355,14 @@ void menuHandler::manageNodeMenu()
static int optionsEnumArray[enumEnd] = {Back}; static int optionsEnumArray[enumEnd] = {Back};
int options = 1; int options = 1;
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
optionsArray[options] = "Unfavorite"; optionsArray[options] = "Unfavorite";
} else { } else {
optionsArray[options] = "Favorite"; optionsArray[options] = "Favorite";
} }
optionsEnumArray[options++] = Favorite; optionsEnumArray[options++] = Favorite;
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
if (isMuted) { if (isMuted) {
optionsArray[options] = "Unmute Notifications"; optionsArray[options] = "Unmute Notifications";
} else { } else {
@@ -1376,7 +1376,7 @@ void menuHandler::manageNodeMenu()
optionsArray[options] = "Key Verification"; optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = KeyVerification; optionsEnumArray[options++] = KeyVerification;
if (node->is_ignored) { if (nodeInfoLiteIsIgnored(node)) {
optionsArray[options] = "Unignore Node"; optionsArray[options] = "Unignore Node";
} else { } else {
optionsArray[options] = "Ignore Node"; optionsArray[options] = "Ignore Node";
@@ -1386,8 +1386,8 @@ void menuHandler::manageNodeMenu()
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
std::string title = ""; std::string title = "";
if (node->has_user && node->user.long_name && node->user.long_name[0]) { if (nodeInfoLiteHasUser(node) && node->long_name[0]) {
title += sanitizeString(node->user.long_name).substr(0, 15); title += sanitizeString(node->long_name).substr(0, 15);
} else { } else {
char buf[20]; char buf[20];
snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num); snprintf(buf, sizeof(buf), "%08X", (unsigned int)node->num);
@@ -1409,7 +1409,7 @@ void menuHandler::manageNodeMenu()
if (!n) { if (!n) {
return; return;
} }
if (n->is_favorite) { if (nodeInfoLiteIsFavorite(n)) {
LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum); LOG_INFO("Removing node %08X from favorites", menuHandler::pickedNodeNum);
nodeDB->set_favorite(false, menuHandler::pickedNodeNum); nodeDB->set_favorite(false, menuHandler::pickedNodeNum);
} else { } else {
@@ -1426,13 +1426,9 @@ void menuHandler::manageNodeMenu()
return; return;
} }
if (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) { const bool wasMuted = nodeInfoLiteIsMuted(n);
n->bitfield &= ~NODEINFO_BITFIELD_IS_MUTED_MASK; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_MUTED_MASK, !wasMuted);
LOG_INFO("Unmuted node %08X", menuHandler::pickedNodeNum); LOG_INFO(wasMuted ? "Unmuted node %08X" : "Muted node %08X", menuHandler::pickedNodeNum);
} else {
n->bitfield |= NODEINFO_BITFIELD_IS_MUTED_MASK;
LOG_INFO("Muted node %08X", menuHandler::pickedNodeNum);
}
nodeDB->notifyObservers(true); nodeDB->notifyObservers(true);
nodeDB->saveToDisk(); nodeDB->saveToDisk();
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1461,11 +1457,11 @@ void menuHandler::manageNodeMenu()
return; return;
} }
if (n->is_ignored) { if (nodeInfoLiteIsIgnored(n)) {
n->is_ignored = false; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum); LOG_INFO("Unignoring node %08X", menuHandler::pickedNodeNum);
} else { } else {
n->is_ignored = true; nodeInfoLiteSetBit(n, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum); LOG_INFO("Ignoring node %08X", menuHandler::pickedNodeNum);
} }
nodeDB->notifyObservers(true); nodeDB->notifyObservers(true);
@@ -2079,9 +2075,9 @@ void menuHandler::removeFavoriteMenu()
static const char *optionsArray[] = {"Back", "Yes"}; static const char *optionsArray[] = {"Back", "Yes"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
std::string message = "Unfavorite This Node?\n"; std::string message = "Unfavorite This Node?\n";
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum); const auto *node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
message += sanitizeString(node->user.long_name).substr(0, 15); message += sanitizeString(node->long_name).substr(0, 15);
} }
bannerOptions.message = message.c_str(); bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
+18 -19
View File
@@ -462,8 +462,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} }
case ThreadMode::DIRECT: { case ThreadMode::DIRECT: {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
if (node && node->has_user && node->user.short_name[0]) { if (nodeInfoLiteHasUser(node) && node->short_name[0]) {
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); snprintf(titleStr, sizeof(titleStr), "@%s", node->short_name);
} else { } else {
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
} }
@@ -585,11 +585,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
char senderName[64] = ""; char senderName[64] = "";
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
if (node->user.long_name[0]) { if (node->long_name[0]) {
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); strncpy(senderName, node->long_name, sizeof(senderName) - 1);
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); strncpy(senderName, node->short_name, sizeof(senderName) - 1);
} }
senderName[sizeof(senderName) - 1] = '\0'; senderName[sizeof(senderName) - 1] = '\0';
} }
@@ -599,18 +599,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// If this is *our own* message, override senderName to who the recipient was // If this is *our own* message, override senderName to who the recipient was
bool mine = (m.sender == nodeDB->getNodeNum()); bool mine = (m.sender == nodeDB->getNodeNum());
if (mine && node_recipient && node_recipient->has_user) { if (mine && nodeInfoLiteHasUser(node_recipient)) {
if (node_recipient->user.long_name[0]) { if (node_recipient->long_name[0]) {
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); strncpy(senderName, node_recipient->long_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0'; senderName[sizeof(senderName) - 1] = '\0';
} else if (node_recipient->user.short_name[0]) { } else if (node_recipient->short_name[0]) {
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); strncpy(senderName, node_recipient->short_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0'; senderName[sizeof(senderName) - 1] = '\0';
} }
} }
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages. // If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
if (mine && (!node_recipient || !node_recipient->has_user || if (mine && (!nodeInfoLiteHasUser(node_recipient) || (!node_recipient->long_name[0] && !node_recipient->short_name[0]))) {
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
} }
@@ -1073,12 +1072,12 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
// Banner logic // Banner logic
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
char longName[64] = "?"; char longName[64] = "?";
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
if (node->user.long_name[0]) { if (node->long_name[0]) {
strncpy(longName, node->user.long_name, sizeof(longName) - 1); strncpy(longName, node->long_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0'; longName[sizeof(longName) - 1] = '\0';
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
strncpy(longName, node->user.short_name, sizeof(longName) - 1); strncpy(longName, node->short_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0'; longName[sizeof(longName) - 1] = '\0';
} }
} }
+46 -44
View File
@@ -98,29 +98,23 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
// 1) Choose target candidate (long vs short) only if present // 1) Choose target candidate (long vs short) only if present
const char *raw = nullptr; const char *raw = nullptr;
#if !MESHTASTIC_EXCLUDE_STATUS #if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
// If long-name mode is enabled, and we have a recent status for this node, // If long-name mode is enabled, and we have a recent status for this node,
// prefer "(short_name) statusText" as the raw candidate. // prefer "(short_name) statusText" as the raw candidate. Pull straight out
// of NodeDB's per-NodeNum cache instead of scanning a FIFO.
std::string composedFromStatus; std::string composedFromStatus;
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) { if (config.display.use_long_node_name && nodeInfoLiteHasUser(node) && nodeDB) {
const auto &recent = statusMessageModule->getRecentReceived(); meshtastic_StatusMessage cachedStatus;
const StatusMessageModule::RecentStatus *found = nullptr; if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
for (auto it = recent.rbegin(); it != recent.rend(); ++it) { const char *shortName = node->short_name;
if (it->fromNodeId == node->num && !it->statusText.empty()) { const size_t statusLen = std::strlen(cachedStatus.status);
found = &(*it); composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + statusLen);
break;
}
}
if (found) {
const char *shortName = node->user.short_name;
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
composedFromStatus += "("; composedFromStatus += "(";
if (shortName && *shortName) { if (shortName && *shortName) {
composedFromStatus += shortName; composedFromStatus += shortName;
} }
composedFromStatus += ") "; composedFromStatus += ") ";
composedFromStatus += found->statusText; composedFromStatus += cachedStatus.status;
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
} }
@@ -129,8 +123,8 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
// If we didn't compose from status, use normal long/short selection // If we didn't compose from status, use normal long/short selection
if (!raw) { if (!raw) {
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; raw = config.display.use_long_node_name ? node->long_name : node->short_name;
} }
} }
@@ -218,7 +212,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName, static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
int16_t nameX, int16_t y, int nameMaxWidth) int16_t nameX, int16_t y, int nameMaxWidth)
{ {
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) { if (!display || !node || !nodeInfoLiteIsFavorite(node) || !isTFTColoringEnabled() || !nodeName) {
return; return;
} }
@@ -259,7 +253,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
char timeStr[10]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -279,14 +273,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -321,20 +315,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -401,15 +395,19 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { meshtastic_PositionLite ourPos;
double lat1 = ourNode->position.latitude_i * 1e-7; meshtastic_PositionLite theirPos;
double lon1 = ourNode->position.longitude_i * 1e-7; const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
double lat2 = node->position.latitude_i * 1e-7; const bool haveTheirPos = nodeDB->copyNodePosition(node->num, theirPos);
double lon2 = node->position.longitude_i * 1e-7; if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveOurPos && haveTheirPos) {
double lat1 = ourPos.latitude_i * 1e-7;
double lon1 = ourPos.longitude_i * 1e-7;
double lat2 = theirPos.latitude_i * 1e-7;
double lon2 = theirPos.longitude_i * 1e-7;
double earthRadiusKm = 6371.0; double earthRadiusKm = 6371.0;
double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLat = (lat2 - lat1) * DEG_TO_RAD;
@@ -455,14 +453,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -509,19 +507,19 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
#if GRAPHICS_TFT_COLORING_ENABLED #if GRAPHICS_TFT_COLORING_ENABLED
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth); applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
#endif #endif
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = nodeInfoLiteIsMuted(node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false);
if (node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else { } else {
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
} }
} }
if (node->is_ignored || isMuted) { if (nodeInfoLiteIsIgnored(node) || isMuted) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8); display->drawLine(x + 8, y + 8, (isLeftCol ? 0 : x - 4) + nameMaxWidth - 17, y + 8);
} else { } else {
@@ -542,8 +540,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
int centerX = x + columnWidth - arrowXOffset; int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2; int centerY = y + FONT_HEIGHT_SMALL / 2;
double nodeLat = node->position.latitude_i * 1e-7; meshtastic_PositionLite nodePos;
double nodeLon = node->position.longitude_i * 1e-7; if (!nodeDB->copyNodePosition(node->num, nodePos))
return;
double nodeLat = nodePos.latitude_i * 1e-7;
double nodeLon = nodePos.longitude_i * 1e-7;
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian); float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing); float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
@@ -652,7 +653,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
continue; continue;
if (n->num == nodeDB->getNodeNum()) if (n->num == nodeDB->getNodeNum())
continue; continue;
if (locationScreen && !n->has_position) if (locationScreen && !nodeDB->hasNodePosition(n->num))
continue; continue;
drawList.push_back(n->num); drawList.push_back(n->num);
@@ -883,14 +884,15 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
float headingRadian = 0.0f; float headingRadian = 0.0f;
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const auto *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) { meshtastic_PositionLite ourSelfPos;
if (!ourNode || !nodeDB->hasValidPosition(ourNode) || !nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0); drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
return; return;
} }
double lat = DegD(ourNode->position.latitude_i); double lat = DegD(ourSelfPos.latitude_i);
double lon = DegD(ourNode->position.longitude_i); double lon = DegD(ourSelfPos.longitude_i);
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
display->clear(); display->clear();
+5 -5
View File
@@ -317,12 +317,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char tempName[48] = {0}; char tempName[48] = {0};
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
if (node && node->has_user) { if (nodeInfoLiteHasUser(node)) {
const char *rawName = nullptr; const char *rawName = nullptr;
if (node->user.long_name[0]) { if (node->long_name[0]) {
rawName = node->user.long_name; rawName = node->long_name;
} else if (node->user.short_name[0]) { } else if (node->short_name[0]) {
rawName = node->user.short_name; rawName = node->short_name;
} }
if (rawName) { if (rawName) {
const int arrowWidth = (currentResolution == ScreenResolution::High) const int arrowWidth = (currentResolution == ScreenResolution::High)
+42 -44
View File
@@ -464,11 +464,11 @@ static bool computeBottomCompassPlacement(OLEDDisplay *display, int16_t xOffset,
return true; return true;
} }
static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const std::string &statusText) static void drawTruncatedStatusLine(OLEDDisplay *display, int16_t x, int16_t y, const char *statusText)
{ {
// Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down. // Fixed-buffer truncate helper replaces iterative std::string chopping to keep code size down.
char rawStatus[96]; char rawStatus[96];
snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText.c_str()); snprintf(rawStatus, sizeof(rawStatus), " Status: %s", statusText ? statusText : "");
char clippedStatus[96]; char clippedStatus[96];
UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth()); UIRenderer::truncateStringWithEmotes(display, rawStatus, clippedStatus, sizeof(clippedStatus), display->getWidth());
@@ -495,7 +495,7 @@ void graphics::UIRenderer::rebuildFavoritedNodes()
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (!n || n->num == nodeDB->getNodeNum()) if (!n || n->num == nodeDB->getNodeNum())
continue; continue;
if (n->is_favorite) if (nodeInfoLiteIsFavorite(n))
favoritedNodes.push_back(n); favoritedNodes.push_back(n);
} }
@@ -750,7 +750,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
return; return;
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) if (!node || node->num == nodeDB->getNodeNum() || !nodeInfoLiteIsFavorite(node))
return; return;
display->clear(); display->clear();
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
@@ -763,7 +763,7 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
#endif #endif
currentFavoriteNodeNum = node->num; currentFavoriteNodeNum = node->num;
// === Create the shortName and title string === // === Create the shortName and title string ===
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; const char *shortName = (nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "Node";
char titlestr[40]; char titlestr[40];
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
@@ -781,9 +781,9 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
// === 1. Long Name (always try to show first) === // === 1. Long Name (always try to show first) ===
const char *username; const char *username;
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->short_name : nullptr;
} else { } else {
username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; username = (nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : nullptr;
} }
// Print node's long name (e.g. "Backpack Node") // Print node's long name (e.g. "Backpack Node")
@@ -796,23 +796,14 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
} }
#if !MESHTASTIC_EXCLUDE_STATUS #if !MESHTASTIC_EXCLUDE_STATUS && !MESHTASTIC_EXCLUDE_STATUSDB
// === Optional: Last received StatusMessage line for this node === // === Optional: Last received StatusMessage line for this node ===
// Display it directly under the username line (if we have one). // Display it directly under the username line (if we have one). The cache
if (statusMessageModule) { // lives on NodeDB now, keyed by NodeNum, so this is an O(1) lookup.
const auto &recent = statusMessageModule->getRecentReceived(); if (nodeDB) {
const StatusMessageModule::RecentStatus *found = nullptr; meshtastic_StatusMessage cachedStatus;
if (nodeDB->copyNodeStatus(node->num, cachedStatus) && cachedStatus.status[0]) {
// Search newest-to-oldest drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], cachedStatus.status);
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
if (it->fromNodeId == node->num && !it->statusText.empty()) {
found = &(*it);
break;
}
}
if (found) {
drawTruncatedStatusLine(display, x, getTextPositions(display)[line++], found->statusText);
} }
} }
#endif #endif
@@ -971,25 +962,30 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
#if !defined(M5STACK_UNITC6L) #if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) === // === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = ""; char uptimeStr[32] = "";
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { meshtastic_DeviceMetrics nodeMetrics;
const bool haveNodeMetrics = nodeDB->copyNodeTelemetry(node->num, nodeMetrics);
if (haveNodeMetrics && nodeMetrics.has_uptime_seconds) {
char upPrefix[12]; // enough for leftSideSpacing + "Up:" char upPrefix[12]; // enough for leftSideSpacing + "Up:"
snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing);
getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); getUptimeStr(nodeMetrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr));
} }
if (uptimeStr[0]) { if (uptimeStr[0]) {
display->drawString(x, getTextPositions(display)[line++], uptimeStr); display->drawString(x, getTextPositions(display)[line++], uptimeStr);
} }
// === 5. Distance (only if both nodes have GPS position) === // === 5. Distance (only if both nodes have GPS position) ===
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
char distStr[24] = ""; // Make buffer big enough for any string char distStr[24] = ""; // Make buffer big enough for any string
bool haveDistance = false; bool haveDistance = false;
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { meshtastic_PositionLite nodePos;
meshtastic_PositionLite ourPos;
const bool haveNodePos = nodeDB->copyNodePosition(node->num, nodePos);
const bool haveOurPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ourPos);
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node) && haveNodePos && haveOurPos) {
// Use shared meter conversion, then format display units with lightweight integer rounding. // Use shared meter conversion, then format display units with lightweight integer rounding.
const float distanceMeters = const float distanceMeters = GeoCoord::latLongToMeter(DegD(nodePos.latitude_i), DegD(nodePos.longitude_i),
GeoCoord::latLongToMeter(DegD(node->position.latitude_i), DegD(node->position.longitude_i), DegD(ourPos.latitude_i), DegD(ourPos.longitude_i));
DegD(ourNode->position.latitude_i), DegD(ourNode->position.longitude_i));
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f); const int feet = static_cast<int>((distanceMeters * METERS_TO_FEET) + 0.5f);
if (feet > 0 && feet < 1000) { if (feet > 0 && feet < 1000) {
@@ -1024,19 +1020,19 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
char batLine[32] = ""; char batLine[32] = "";
bool haveBatLine = false; bool haveBatLine = false;
if (node->has_device_metrics) { if (haveNodeMetrics) {
bool hasPct = node->device_metrics.has_battery_level; bool hasPct = nodeMetrics.has_battery_level;
bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; bool hasVolt = nodeMetrics.has_voltage && nodeMetrics.voltage > 0.001f;
int pct = 0; int pct = 0;
float volt = 0.0f; float volt = 0.0f;
if (hasPct) { if (hasPct) {
pct = (int)node->device_metrics.battery_level; pct = (int)nodeMetrics.battery_level;
} }
if (hasVolt) { if (hasVolt) {
volt = node->device_metrics.voltage; volt = nodeMetrics.voltage;
} }
if (hasPct && pct > 0 && pct <= 100) { if (hasPct && pct > 0 && pct <= 100) {
@@ -1076,11 +1072,11 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
const bool hasNodePositionFix = nodeDB->hasValidPosition(node); const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
const char *statusLine1 = nullptr; const char *statusLine1 = nullptr;
const char *statusLine2 = nullptr; const char *statusLine2 = nullptr;
if (hasOwnPositionFix && hasNodePositionFix) { if (hasOwnPositionFix && hasNodePositionFix && haveOurPos && haveNodePos) {
const auto &op = ourNode->position; const auto &op = ourPos;
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading); showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
if (showCompass) { if (showCompass) {
const auto &p = node->position; const auto &p = nodePos;
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading); bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
} else { } else {
@@ -1130,7 +1126,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int line = 1; int line = 1;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header === // === Header ===
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
@@ -1270,7 +1266,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; const char *longName = (nodeInfoLiteHasUser(ourNode) && ourNode->long_name[0]) ? ourNode->long_name : "";
const char *shortName = owner.short_name ? owner.short_name : ""; const char *shortName = owner.short_name ? owner.short_name : "";
char combinedName[96]; char combinedName[96];
if (longName[0] && shortName[0]) { if (longName[0] && shortName[0]) {
@@ -1597,7 +1593,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
int32_t(gpsStatus->getAltitude())); int32_t(gpsStatus->getAltitude()));
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode)); const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
const bool hasLiveGpsFix = const bool hasLiveGpsFix =
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0)); (gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
@@ -1613,9 +1609,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
headingLat = DegD(gpsStatus->getLatitude()); headingLat = DegD(gpsStatus->getLatitude());
headingLon = DegD(gpsStatus->getLongitude()); headingLon = DegD(gpsStatus->getLongitude());
} else if (hasOwnPositionFix) { } else if (hasOwnPositionFix) {
const auto &op = ourNode->position; meshtastic_PositionLite ownPos;
headingLat = DegD(op.latitude_i); if (nodeDB->copyNodePosition(ourNode->num, ownPos)) {
headingLon = DegD(op.longitude_i); headingLat = DegD(ownPos.latitude_i);
headingLon = DegD(ownPos.longitude_i);
}
} }
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading); validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
} }
+2 -2
View File
@@ -362,8 +362,8 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
assert(node); assert(node);
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
if (node->has_user) { if (nodeInfoLiteHasUser(node)) {
std::string parsed = parse(node->user.short_name); std::string parsed = parse(node->short_name);
if (isPrintable(parsed)) if (isPrintable(parsed))
return parsed; return parsed;
} }
@@ -136,9 +136,10 @@ void InkHUD::MapApplet::onRender(bool full)
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE); printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
// Draw our node LAST with full white fill + outline // Draw our node LAST with full white fill + outline
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) { meshtastic_PositionLite ourSelfPos;
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0); if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
Marker self = calculateMarker(ourSelfPos.latitude_i * 1e-7, ourSelfPos.longitude_i * 1e-7, false, 0);
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx); int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx); int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
@@ -168,10 +169,11 @@ void InkHUD::MapApplet::onRender(bool full)
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
{ {
// If we have a valid position for our own node, use that as the anchor // If we have a valid position for our own node, use that as the anchor
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) { meshtastic_PositionLite ourSelfPos;
*lat = ourNode->position.latitude_i * 1e-7; if (ourNode && nodeDB->hasValidPosition(ourNode) && nodeDB->copyNodePosition(ourNode->num, ourSelfPos)) {
*lng = ourNode->position.longitude_i * 1e-7; *lat = ourSelfPos.latitude_i * 1e-7;
*lng = ourSelfPos.longitude_i * 1e-7;
} else { } else {
// Find mean lat long coords // Find mean lat long coords
// ============================ // ============================
@@ -201,9 +203,13 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
if (!shouldDrawNode(node)) if (!shouldDrawNode(node))
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Latitude and Longitude of node, in radians // Latitude and Longitude of node, in radians
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD; float latRad = pos.latitude_i * (1e-7) * DEG_TO_RAD;
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD; float lngRad = pos.longitude_i * (1e-7) * DEG_TO_RAD;
// Convert to cartesian points, with center of earth at 0, 0, 0 // Convert to cartesian points, with center of earth at 0, 0, 0
// Exact distance from center is irrelevant, as we're only interested in the vector // Exact distance from center is irrelevant, as we're only interested in the vector
@@ -300,13 +306,17 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
if (!shouldDrawNode(node)) if (!shouldDrawNode(node))
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Check for a new top or bottom latitude // Check for a new top or bottom latitude
float latNode = node->position.latitude_i * 1e-7; float latNode = pos.latitude_i * 1e-7;
northernmost = max(northernmost, latNode); northernmost = max(northernmost, latNode);
southernmost = min(southernmost, latNode); southernmost = min(southernmost, latNode);
// Longitude is trickier // Longitude is trickier
float lngNode = node->position.longitude_i * 1e-7; float lngNode = pos.longitude_i * 1e-7;
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward) if (degEastward < degWestward)
@@ -372,10 +382,13 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
{ {
// Find x and y position based on node's position in nodeDB // Find x and y position based on node's position in nodeDB
assert(nodeDB->hasValidPosition(node)); assert(nodeDB->hasValidPosition(node));
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style meshtastic_PositionLite pos;
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style const bool hasPos = nodeDB->copyNodePosition(node->num, pos);
node->has_hops_away, // Is the hopsAway number valid assert(hasPos);
node->hops_away // Hops away Marker m = calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
); );
// Convert to pixel coords // Convert to pixel coords
@@ -516,13 +529,16 @@ void InkHUD::MapApplet::calculateAllMarkers()
if (node->num == nodeDB->getNodeNum()) if (node->num == nodeDB->getNodeNum())
continue; continue;
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(node->num, pos))
continue;
// Calculate marker and store it // Calculate marker and store it
markers.push_back( markers.push_back(calculateMarker(pos.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style pos.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style node->has_hops_away, // Is the hopsAway number valid
node->has_hops_away, // Is the hopsAway number valid node->hops_away // Hops away
node->hops_away // Hops away ));
));
} }
} }
@@ -50,21 +50,23 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi); c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
// Assemble info: from nodeDB (needed to detect changes) // Assemble info: from nodeDB (needed to detect changes)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node) { if (node) {
if (node->has_hops_away) if (node->has_hops_away)
c.hopsAway = node->hops_away; c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float meshtastic_PositionLite ourPos;
// Meshtastic stores these as integers internally meshtastic_PositionLite theirPos;
float ourLat = ourNode->position.latitude_i * 1e-7; if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
float ourLong = ourNode->position.longitude_i * 1e-7; float ourLat = ourPos.latitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7; float ourLong = ourPos.longitude_i * 1e-7;
float theirLong = node->position.longitude_i * 1e-7; float theirLat = theirPos.latitude_i * 1e-7;
float theirLong = theirPos.longitude_i * 1e-7;
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
}
} }
} }
@@ -179,8 +181,8 @@ void InkHUD::NodeListApplet::onRender(bool full)
// -- Longname -- // -- Longname --
// Parse special chars in long name // Parse special chars in long name
// Use node id if unknown // Use node id if unknown
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
longName = parse(node->user.long_name); // Found in nodeDB longName = parse(node->long_name); // Found in nodeDB
else { else {
// Not found in nodeDB, show a hex nodeid instead // Not found in nodeDB, show a hex nodeid instead
longName = hexifyNodeNum(nodeNum); longName = hexifyNodeNum(nodeNum);
@@ -2090,7 +2090,7 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Count favorites // Count favorites
for (uint32_t i = 0; i < nodeCount; i++) { for (uint32_t i = 0; i < nodeCount; i++) {
if (nodeDB->getMeshNodeByIndex(i)->is_favorite) if (nodeInfoLiteIsFavorite(nodeDB->getMeshNodeByIndex(i)))
favoriteCount++; favoriteCount++;
} }
@@ -2098,10 +2098,10 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Don't want some monstrous list that takes 100 clicks to reach exit // Don't want some monstrous list that takes 100 clicks to reach exit
if (favoriteCount < 20) { if (favoriteCount < 20) {
for (uint32_t i = 0; i < nodeCount; i++) { for (uint32_t i = 0; i < nodeCount; i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
// Skip node if not a favorite // Skip node if not a favorite
if (!node->is_favorite) if (!nodeInfoLiteIsFavorite(node))
continue; continue;
CannedMessages::RecipientItem r; CannedMessages::RecipientItem r;
@@ -2111,8 +2111,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Set a label for the menu item // Set a label for the menu item
r.label = "DM: "; r.label = "DM: ";
if (node->has_user) if (nodeInfoLiteHasUser(node))
r.label += parse(node->user.long_name); r.label += parse(node->long_name);
else else
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
@@ -241,7 +241,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += msgIsBroadcast ? "From:" : "DM: "; text += msgIsBroadcast ? "From:" : "DM: ";
// Sender id // Sender id
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
text += parseShortName(node); text += parseShortName(node);
else else
text += hexifyNodeNum(message->sender); text += hexifyNodeNum(message->sender);
@@ -255,7 +255,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += msgIsBroadcast ? "Msg from " : "DM from "; text += msgIsBroadcast ? "Msg from " : "DM from ";
// Sender id // Sender id
if (node && node->has_user) if (nodeInfoLiteHasUser(node))
text += parseShortName(node); text += parseShortName(node);
else else
text += hexifyNodeNum(message->sender); text += hexifyNodeNum(message->sender);
@@ -70,10 +70,10 @@ void InkHUD::AllMessageApplet::onRender(bool full)
// - short name and long name, if available, or // - short name and long name, if available, or
// - node id // - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
if (sender && sender->has_user) { if (nodeInfoLiteHasUser(sender)) {
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " ("; header += " (";
header += parse(sender->user.long_name); header += parse(sender->long_name);
header += ")"; header += ")";
} else } else
header += hexifyNodeNum(message->sender); header += hexifyNodeNum(message->sender);
@@ -66,10 +66,10 @@ void InkHUD::DMApplet::onRender(bool full)
// - shortname and long name, if available, or // - shortname and long name, if available, or
// - node id // - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
if (sender && sender->has_user) { if (nodeInfoLiteHasUser(sender)) {
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " ("; header += " (";
header += parse(sender->user.long_name); header += parse(sender->long_name);
header += ")"; header += ")";
} else } else
header += hexifyNodeNum(latestMessage->dm.sender); header += hexifyNodeNum(latestMessage->dm.sender);
@@ -8,7 +8,7 @@ using namespace NicheGraphics;
bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node)
{ {
// Keep our own node available as map anchor/center; all others must be favorited. // Keep our own node available as map anchor/center; all others must be favorited.
return node && (node->num == nodeDB->getNodeNum() || node->is_favorite); return node && (node->num == nodeDB->getNodeNum() || nodeInfoLiteIsFavorite(node));
} }
void InkHUD::FavoritesMapApplet::onRender(bool full) void InkHUD::FavoritesMapApplet::onRender(bool full)
@@ -25,7 +25,7 @@ void InkHUD::FavoritesMapApplet::onRender(bool full)
// Draw our latest "node of interest" as a special marker. // Draw our latest "node of interest" as a special marker.
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers()) if (node && nodeInfoLiteIsFavorite(node) && nodeDB->hasValidPosition(node) && enoughMarkers())
drawLabeledMarker(node); drawLabeledMarker(node);
} }
@@ -74,7 +74,7 @@ ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshP
} else { } else {
// For non-local packets, this applet only reacts to favorited nodes. // For non-local packets, this applet only reacts to favorited nodes.
const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from);
if (!sender || !sender->is_favorite) if (!nodeInfoLiteIsFavorite(sender))
return ProcessMessage::CONTINUE; return ProcessMessage::CONTINUE;
// Check if this position is from someone different than our previous position packet. // Check if this position is from someone different than our previous position packet.
@@ -80,8 +80,8 @@ void InkHUD::HeardApplet::populateFromNodeDB()
ordered.resize(maxCards()); ordered.resize(maxCards());
// Create card info for these (stale) node observations // Create card info for these (stale) node observations
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
for (meshtastic_NodeInfoLite *node : ordered) { for (const meshtastic_NodeInfoLite *node : ordered) {
CardInfo c; CardInfo c;
c.nodeNum = node->num; c.nodeNum = node->num;
@@ -89,14 +89,16 @@ void InkHUD::HeardApplet::populateFromNodeDB()
c.hopsAway = node->hops_away; c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) { if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float meshtastic_PositionLite ourPos;
// Meshtastic stores these as integers internally meshtastic_PositionLite theirPos;
float ourLat = ourNode->position.latitude_i * 1e-7; if (nodeDB->copyNodePosition(ourNode->num, ourPos) && nodeDB->copyNodePosition(node->num, theirPos)) {
float ourLong = ourNode->position.longitude_i * 1e-7; float ourLat = ourPos.latitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7; float ourLong = ourPos.longitude_i * 1e-7;
float theirLong = node->position.longitude_i * 1e-7; float theirLat = theirPos.latitude_i * 1e-7;
float theirLong = theirPos.longitude_i * 1e-7;
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong); c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
}
} }
// Insert into the card collection (member of base class) // Insert into the card collection (member of base class)
@@ -122,4 +124,4 @@ std::string InkHUD::HeardApplet::getHeaderText()
return text; return text;
} }
#endif #endif
+3 -8
View File
@@ -13,7 +13,6 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl() RotaryEncoderImpl::RotaryEncoderImpl()
{ {
rotary = nullptr;
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
isFirstInit = true; isFirstInit = true;
#endif #endif
@@ -23,11 +22,6 @@ RotaryEncoderImpl::~RotaryEncoderImpl()
{ {
LOG_DEBUG("RotaryEncoderImpl destructor"); LOG_DEBUG("RotaryEncoderImpl destructor");
detachRotaryEncoderInterrupts(); detachRotaryEncoderInterrupts();
if (rotary != nullptr) {
delete rotary;
rotary = nullptr;
}
} }
bool RotaryEncoderImpl::init() bool RotaryEncoderImpl::init()
@@ -43,8 +37,9 @@ bool RotaryEncoderImpl::init()
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press); eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
if (rotary == nullptr) { if (rotary == nullptr) {
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, rotary.reset(new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_press); moduleConfig.canned_message.inputbroker_pin_b,
moduleConfig.canned_message.inputbroker_pin_press));
} }
attachRotaryEncoderInterrupts(); attachRotaryEncoderInterrupts();
+2 -1
View File
@@ -5,6 +5,7 @@
#include "InputBroker.h" #include "InputBroker.h"
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "mesh/NodeDB.h" #include "mesh/NodeDB.h"
#include <memory>
class RotaryEncoder; class RotaryEncoder;
@@ -28,7 +29,7 @@ class RotaryEncoderImpl final : public InputPollable
input_broker_event eventCcw = INPUT_BROKER_NONE; input_broker_event eventCcw = INPUT_BROKER_NONE;
input_broker_event eventPressed = INPUT_BROKER_NONE; input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary; std::unique_ptr<RotaryEncoder> rotary;
private: private:
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
+2 -2
View File
@@ -112,7 +112,7 @@ bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, mes
* @param bytes Buffer containing plaintext input. * @param bytes Buffer containing plaintext input.
* @param bytesOut Output buffer to be populated with encrypted ciphertext. * @param bytesOut Output buffer to be populated with encrypted ciphertext.
*/ */
bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
{ {
uint8_t *auth; uint8_t *auth;
@@ -152,7 +152,7 @@ bool CryptoEngine::encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtas
* @param bytes Buffer containing ciphertext input. * @param bytes Buffer containing ciphertext input.
* @param bytesOut Output buffer to be populated with decrypted plaintext. * @param bytesOut Output buffer to be populated with decrypted plaintext.
*/ */
bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, bool CryptoEngine::decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut) size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut)
{ {
const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text? const uint8_t *auth = bytes + numBytes - 12; // set to last 8 bytes of text?
+5 -2
View File
@@ -40,9 +40,12 @@ class CryptoEngine
#endif #endif
void setDHPrivateKey(uint8_t *_private_key); void setDHPrivateKey(uint8_t *_private_key);
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, // The remotePublic key parameter takes the public_key bytes container from
// a stored node header. NodeInfoLite is the on-device storage type since
// the slim refactor flattened UserLite into it.
virtual bool encryptCurve25519(uint32_t toNode, uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic,
uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); uint64_t packetNum, size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_UserLite_public_key_t remotePublic, uint64_t packetNum, virtual bool decryptCurve25519(uint32_t fromNode, meshtastic_NodeInfoLite_public_key_t remotePublic, uint64_t packetNum,
size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut); size_t numBytes, const uint8_t *bytes, uint8_t *bytesOut);
virtual bool setDHPublicKey(uint8_t *publicKey); virtual bool setDHPublicKey(uint8_t *publicKey);
virtual void hash(uint8_t *bytes, size_t numBytes); virtual void hash(uint8_t *bytes, size_t numBytes);
+7
View File
@@ -1,5 +1,9 @@
#include "configuration.h"
#include "LR11x0Interface.cpp" #include "LR11x0Interface.cpp"
#include "LR11x0Interface.h" #include "LR11x0Interface.h"
#include "LR20x0Interface.cpp"
#include "LR20x0Interface.h"
#include "SX126xInterface.cpp" #include "SX126xInterface.cpp"
#include "SX126xInterface.h" #include "SX126xInterface.h"
#include "SX128xInterface.cpp" #include "SX128xInterface.cpp"
@@ -21,6 +25,9 @@ template class LR11x0Interface<LR1110>;
template class LR11x0Interface<LR1120>; template class LR11x0Interface<LR1120>;
template class LR11x0Interface<LR1121>; template class LR11x0Interface<LR1121>;
#endif #endif
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
template class LR20x0Interface<LR2021>;
#endif
#ifdef ARCH_STM32WL #ifdef ARCH_STM32WL
template class SX126xInterface<STM32WLx>; template class SX126xInterface<STM32WLx>;
#endif #endif
+20 -6
View File
@@ -57,16 +57,19 @@ template <typename T> bool LR11x0Interface<T>::init()
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
#elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) #elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE);
// (DIO3 is not free to be used as an IRQ)
#elif defined(TCXO_OPTIONAL)
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
LOG_DEBUG("TCXO_OPTIONAL: no LR11X0_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
#else
float tcxoVoltage = float tcxoVoltage =
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
// (DIO3 is free to be used as an IRQ) // (DIO3 is free to be used as an IRQ)
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
#else
float tcxoVoltage = LR11X0_DIO3_TCXO_VOLTAGE;
LOG_DEBUG("LR11X0_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR11X0_DIO3_TCXO_VOLTAGE);
// (DIO3 is not free to be used as an IRQ)
#endif #endif
RadioLibInterface::init(); RadioLibInterface::init();
@@ -101,6 +104,17 @@ template <typename T> bool LR11x0Interface<T>::init()
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
} }
#if defined(TCXO_OPTIONAL)
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
LOG_WARN("LR11x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
tcxoVoltage = 0;
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
if (res == RADIOLIB_ERR_NONE)
LOG_INFO("LR11x0 init success without TCXO (XTAL mode)");
}
#endif
// \todo Display actual typename of the adapter, not just `LR11x0` // \todo Display actual typename of the adapter, not just `LR11x0`
LOG_INFO("LR11x0 init result %d", res); LOG_INFO("LR11x0 init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
@@ -198,7 +212,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
startReceive(); // restart receiving startReceive(); // restart receiving
return RADIOLIB_ERR_NONE; return true;
} }
template <typename T> void LR11x0Interface<T>::disableInterrupt() template <typename T> void LR11x0Interface<T>::disableInterrupt()
+18
View File
@@ -0,0 +1,18 @@
#include "configuration.h"
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
#include "LR2021Interface.h"
#include "error.h"
LR2021Interface::LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy)
: LR20x0Interface(hal, cs, irq, rst, busy)
{
}
bool LR2021Interface::wideLora()
{
return true;
}
#endif
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#if RADIOLIB_EXCLUDE_LR2021 != 1
#include "LR20x0Interface.h"
/**
* Our adapter for LR2021 radios
*/
class LR2021Interface : public LR20x0Interface<LR2021>
{
public:
LR2021Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy);
bool wideLora() override;
};
#endif
+399
View File
@@ -0,0 +1,399 @@
#include "configuration.h"
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
#include "LR20x0Interface.h"
#include "error.h"
#include "mesh/NodeDB.h"
// Keep LR20x0 naming while RadioLib exposes LR2021 symbols.
#ifndef LR20x0
#define LR20x0 LR2021
#endif
#ifdef LR2021_DIO_AS_RF_SWITCH
#include "rfswitch.h"
#elif ARCH_PORTDUINO
#include "PortduinoGlue.h"
#define lr20x0_rfswitch_dio_pins portduino_config.rfswitch_dio_pins
#define lr20x0_rfswitch_table portduino_config.rfswitch_table
#else
static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = {
{LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}},
{LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE,
};
#endif
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
// specified (may be dangerous if using external PA and LR20x0 power config forgotten)
#if ARCH_PORTDUINO
#define LR2021_MAX_POWER portduino_config.lr2021_max_power
#endif
#ifndef LR2021_MAX_POWER
#define LR2021_MAX_POWER 22
#endif
// the 2.4G part maxes at 12dBm
#if ARCH_PORTDUINO
#define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf
#endif
#ifndef LR2021_MAX_POWER_HF
#define LR2021_MAX_POWER_HF 12
#endif
template <typename T>
LR20x0Interface<T>::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy)
: RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module)
{
LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy);
}
/// Initialise the Driver transport hardware and software.
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
template <typename T> bool LR20x0Interface<T>::init()
{
#ifdef LR2021_POWER_EN
pinMode(LR2021_POWER_EN, OUTPUT);
digitalWrite(LR2021_POWER_EN, HIGH);
#endif
#if ARCH_PORTDUINO
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE
#elif defined(LR2021_DIO3_TCXO_VOLTAGE)
float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE;
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE);
// (DIO3 is not free to be used as an IRQ)
#elif defined(TCXO_OPTIONAL)
float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure
LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first");
#else
float tcxoVoltage =
0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per
// https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104
// (DIO3 is free to be used as an IRQ)
LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage");
#endif
RadioLibInterface::init();
#ifdef LR2021_IRQ_DIO_NUM
lora.irqDioNum = LR2021_IRQ_DIO_NUM;
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
#elif defined(IRQ_DIO_NUM)
lora.irqDioNum = IRQ_DIO_NUM;
LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum);
#else
LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum);
#endif
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
limitPower(LR2021_MAX_POWER_HF);
} else {
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
}
#ifdef LR2021_RF_SWITCH_SUBGHZ
pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW);
LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
#endif
#ifdef LR2021_RF_SWITCH_2_4GHZ
pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT);
digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH);
LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz");
#endif
// Allow extra time for TCXO to stabilize after power-on
delay(10);
int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
// Retry if we get SPI command failed - some units need extra TCXO stabilization time
if (res == RADIOLIB_ERR_SPI_CMD_FAILED) {
LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res);
delay(100);
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
}
#if defined(TCXO_OPTIONAL)
// If init failed for any reason other than chip not found, retry without TCXO (XTAL mode)
if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) {
LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res);
tcxoVoltage = 0;
res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage);
if (res == RADIOLIB_ERR_NONE)
LOG_INFO("LR20x0 init success without TCXO (XTAL mode)");
}
#endif
// \todo Display actual typename of the adapter, not just `LR20x0`
LOG_INFO("LR20x0 init result %d", res);
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
return false;
// Some basic info about the module's explicit firmware version - no other info available
// Currently requires radiolib godmode
#if RADIOLIB_GODMODE
uint8_t fwMajor = 0;
uint8_t fwMinor = 0;
int versionRes = lora.getVersion(&fwMajor, &fwMinor);
if (versionRes == RADIOLIB_ERR_NONE)
LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor);
#endif
LOG_INFO("Frequency set to %f", getFreq());
LOG_INFO("Bandwidth set to %f", bw);
LOG_INFO("Power output set to %d", power);
if (res == RADIOLIB_ERR_NONE)
res = lora.setCRC(2);
// Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024)
// Currently requires radiolib godmode
#if RADIOLIB_GODMODE
if (res == RADIOLIB_ERR_NONE) {
uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions
res = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes);
if (res != RADIOLIB_ERR_NONE)
LOG_WARN("LR2021 setRegMode failed: %d", res);
}
#endif
#ifdef LR2021_DIO_AS_RF_SWITCH
bool dioAsRfSwitch = true;
#elif defined(ARCH_PORTDUINO)
bool dioAsRfSwitch = portduino_config.has_rfswitch_table;
#else
bool dioAsRfSwitch = false;
#endif
if (dioAsRfSwitch) {
lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table);
LOG_DEBUG("Set DIO RF switch");
}
if (res == RADIOLIB_ERR_NONE) {
if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate
res = lora.setRxBoostedGainMode(true);
LOG_INFO("Set RX gain to boosted mode; result: %d", res);
} else {
res = lora.setRxBoostedGainMode(false);
LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res);
}
}
if (res == RADIOLIB_ERR_NONE)
startReceive(); // start receiving
return res == RADIOLIB_ERR_NONE;
}
template <typename T> bool LR20x0Interface<T>::reconfigure()
{
RadioLibInterface::reconfigure();
// set mode to standby
setStandby();
// configure publicly accessible settings
int err = lora.setSpreadingFactor(sf);
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setBandwidth(bw); // different form than LR11xx
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setSyncWord(syncWord);
assert(err == RADIOLIB_ERR_NONE);
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range
limitPower(LR2021_MAX_POWER_HF);
} else {
limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range
}
err = lora.setPreambleLength(preambleLength);
assert(err == RADIOLIB_ERR_NONE);
err = lora.setFrequency(getFreq());
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
err = lora.setOutputPower(power);
assert(err == RADIOLIB_ERR_NONE);
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
if (err != RADIOLIB_ERR_NONE)
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
startReceive(); // restart receiving
return true;
}
template <typename T> void LR20x0Interface<T>::disableInterrupt()
{
lora.clearIrqAction();
}
template <typename T> void LR20x0Interface<T>::setStandby()
{
checkNotification(); // handle any pending interrupts before we force standby
int err = lora.standby();
if (err != RADIOLIB_ERR_NONE) {
LOG_DEBUG("LR20x0 standby failed with error %d", err);
}
assert(err == RADIOLIB_ERR_NONE);
isReceiving = false; // If we were receiving, not any more
activeReceiveStart = 0;
disableInterrupt();
completeSending(); // If we were sending, not anymore
RadioLibInterface::setStandby();
}
/**
* Add SNR data to received messages
*/
template <typename T> void LR20x0Interface<T>::addReceiveMetadata(meshtastic_MeshPacket *mp)
{
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
mp->rx_snr = lora.getSNR();
mp->rx_rssi = lround(lora.getRSSI());
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
// too(!)
}
/** We override to turn on transmitter power as needed.
*/
template <typename T> void LR20x0Interface<T>::configHardwareForSend()
{
RadioLibInterface::configHardwareForSend();
}
// For power draw measurements, helpful to force radio to stay sleeping
// #define SLEEP_ONLY
template <typename T> void LR20x0Interface<T>::startReceive()
{
#ifdef SLEEP_ONLY
sleep();
#else
setStandby();
lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed.
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
int err =
lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
if (err)
LOG_ERROR("StartReceive error: %d", err);
assert(err == RADIOLIB_ERR_NONE);
RadioLibInterface::startReceive();
// Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits
enableInterrupt(isrRxLevel0);
checkRxDoneIrqFlag();
#endif
}
/** Is the channel currently active? */
template <typename T> bool LR20x0Interface<T>::isChannelActive()
{
// check if we can detect a LoRa preamble on the current channel
ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD,
.detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
.detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
.exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT,
.timeout = 0,
.irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS,
.irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}};
int16_t result;
setStandby();
result = lora.scanChannel(cfg);
if (result == RADIOLIB_LORA_DETECTED)
return true;
assert(result != RADIOLIB_ERR_WRONG_MODEM);
return false;
}
/** Could we send right now (i.e. either not actively receiving or transmitting)? */
template <typename T> bool LR20x0Interface<T>::isActivelyReceiving()
{
// The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet
// received and handled the interrupt for reading the packet/handling errors.
return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED);
}
#ifdef LR20X0_AGC_RESET
template <typename T> void LR20x0Interface<T>::resetAGC()
{
// Safety: don't reset mid-packet
if (sendingPacket != NULL || (isReceiving && isActivelyReceiving()))
return;
LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)");
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
lora.sleep(true, 0);
// 2. Wake to RC standby for stable calibration
lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true);
// 3. Calibrate all blocks (PLL, ADC, image, RC oscillators)
// calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation)
uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL;
module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true);
// 4. Re-calibrate image rejection for actual operating frequency
// Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions.
lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f);
// 5. Re-apply RX boosted gain mode
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
// 6. Resume receiving
startReceive();
}
#endif
template <typename T> bool LR20x0Interface<T>::sleep()
{
// \todo Display actual typename of the adapter, not just `LR20x0`
LOG_DEBUG("LR20x0 entering sleep mode");
setStandby(); // Stop any pending operations
// turn off TCXO if it was powered
lora.setTCXO(0);
// put chipset into sleep mode (we've already disabled interrupts by now)
bool keepConfig = false;
lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed
#ifdef LR2021_POWER_EN
digitalWrite(LR2021_POWER_EN, LOW);
#endif
return true;
}
#endif
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#if RADIOLIB_EXCLUDE_LR2021 != 1
#include "RadioLibInterface.h"
/**
* \brief Adapter for LR20x0 radio family. Implements common logic for child classes.
* \tparam T RadioLib module type for LR20x0, e.g. LR2021.
*/
template <class T> class LR20x0Interface : public RadioLibInterface
{
public:
LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
RADIOLIB_PIN_TYPE busy);
/// Initialise the Driver transport hardware and software.
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
virtual bool init() override;
/// Apply any radio provisioning changes
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
virtual bool reconfigure() override;
/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() override;
bool isIRQPending() override { return lora.getIrqFlags() != 0; }
#ifdef LR20X0_AGC_RESET
void resetAGC() override;
#endif
protected:
/**
* Specific module instance
*/
T lora;
/**
* Glue functions called from ISR land
*/
virtual void disableInterrupt() override;
/**
* Enable a particular ISR callback glue function
*/
virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); }
/** can we detect a LoRa preamble on the current channel? */
virtual bool isChannelActive() override;
/** are we actively receiving a packet (only called during receiving state) */
virtual bool isActivelyReceiving() override;
/**
* Start waiting to receive a message
*/
virtual void startReceive() override;
/**
* We override to turn on transmitter power as needed.
*/
virtual void configHardwareForSend() override;
/**
* Add SNR data to received messages
*/
virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override;
virtual void setStandby() override;
uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); }
};
#endif
+12 -12
View File
@@ -94,8 +94,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
// ignore our request for its NodeInfo // ignore our request for its NodeInfo
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp->from)) && nodeInfoModule && !isPreferredRebroadcaster &&
!nodeDB->isFull()) {
if (airTime->isTxAllowedChannelUtil(true)) { if (airTime->isTxAllowedChannelUtil(true)) {
const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit);
if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) {
@@ -392,19 +393,16 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
assert(node); assert(node);
// We might not have a position yet for our local node, in that case, at least try to send the time
if (!node->has_position) {
memset(&node->position, 0, sizeof(node->position));
node->has_position = true;
}
meshtastic_PositionLite &position = node->position;
// Update our local node info with our time (even if we don't decide to update anyone else) // Update our local node info with our time (even if we don't decide to update anyone else)
node->last_heard = node->last_heard =
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid
position.time = getValidTime(RTCQualityFromNet); #if !MESHTASTIC_EXCLUDE_POSITIONDB
// Make sure our own NodeNum has a slot in the position map so subsequent
// updates (and the bundled NodeInfo emission to the phone) have somewhere
// to read from. Insert a default-zero entry on first call.
nodeDB->touchNodePositionTime(node->num, getValidTime(RTCQualityFromNet));
#endif
if (powerStatus->getHasBattery() == 1) { if (powerStatus->getHasBattery() == 1) {
updateBatteryLevel(powerStatus->getBatteryChargePercent()); updateBatteryLevel(powerStatus->getBatteryChargePercent());
@@ -432,7 +430,9 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
// Used fixed position if configured regardless of GPS lock // Used fixed position if configured regardless of GPS lock
if (config.position.fixed_position) { if (config.position.fixed_position) {
LOG_WARN("Use fixed position"); LOG_WARN("Use fixed position");
pos = TypeConversions::ConvertToPosition(node->position); meshtastic_PositionLite fixedSlot;
if (nodeDB->copyNodePosition(node->num, fixedSlot))
pos = TypeConversions::ConvertToPosition(fixedSlot);
} }
// Add a fresh timestamp // Add a fresh timestamp
+553 -125
View File
@@ -22,6 +22,7 @@
#include "error.h" #include "error.h"
#include "main.h" #include "main.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include "meshUtils.h" #include "meshUtils.h"
#include "modules/NeighborInfoModule.h" #include "modules/NeighborInfoModule.h"
#include <ErriezCRC32.h> #include <ErriezCRC32.h>
@@ -151,24 +152,103 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
#endif #endif
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field) bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
{ {
if (ostream) { const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData; switch (iter->tag) {
for (auto item : *vec) { case meshtastic_NodeDatabase_nodes_tag: {
if (!pb_encode_tag_for_field(ostream, field)) if (ostream) {
return false; const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item); for (auto item : *vec) {
if (!pb_encode_tag_for_field(ostream, iter))
return false;
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item))
return false;
}
} }
if (istream) {
meshtastic_NodeInfoLite node;
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
vec->push_back(node);
}
return true;
} }
if (istream) { case meshtastic_NodeDatabase_positions_tag: {
meshtastic_NodeInfoLite node; // this gets good data if (ostream) {
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData; const auto *vec = static_cast<const std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
for (auto item : *vec) {
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) if (!pb_encode_tag_for_field(ostream, iter))
vec->push_back(node); return false;
if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodePositionEntry entry;
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry))
vec->push_back(entry);
}
return true;
}
case meshtastic_NodeDatabase_telemetry_tag: {
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
for (auto item : *vec) {
if (!pb_encode_tag_for_field(ostream, iter))
return false;
if (!pb_encode_submessage(ostream, meshtastic_NodeTelemetryEntry_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodeTelemetryEntry entry;
auto *vec = static_cast<std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry))
vec->push_back(entry);
}
return true;
}
case meshtastic_NodeDatabase_status_tag: {
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
for (auto item : *vec) {
if (!pb_encode_tag_for_field(ostream, iter))
return false;
if (!pb_encode_submessage(ostream, meshtastic_NodeStatusEntry_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodeStatusEntry entry;
auto *vec = static_cast<std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry))
vec->push_back(entry);
}
return true;
}
case meshtastic_NodeDatabase_environment_tag: {
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
for (auto item : *vec) {
if (!pb_encode_tag_for_field(ostream, iter))
return false;
if (!pb_encode_submessage(ostream, meshtastic_NodeEnvironmentEntry_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodeEnvironmentEntry entry;
auto *vec = static_cast<std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry))
vec->push_back(entry);
}
return true;
}
default:
return true;
} }
return true;
} }
/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings /** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
@@ -298,8 +378,7 @@ NodeDB::NodeDB()
#endif #endif
// Include our owner in the node db under our nodenum // Include our owner in the node db under our nodenum
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
info->user = TypeConversions::ConvertToUserLite(owner); TypeConversions::CopyUserToNodeInfoLite(info, owner);
info->has_user = true;
// If node database has not been saved for the first time, save it now // If node database has not been saved for the first time, save it now
#ifdef FSCom #ifdef FSCom
@@ -442,8 +521,12 @@ NodeDB::NodeDB()
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON) #if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL; fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
config.has_position = true; config.has_position = true;
info->has_position = true; #if !MESHTASTIC_EXCLUDE_POSITIONDB
info->position = TypeConversions::ConvertToPositionLite(fixedGPS); {
concurrency::LockGuard guard(&satelliteMutex);
nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS);
}
#endif
nodeDB->setLocalPosition(fixedGPS); nodeDB->setLocalPosition(fixedGPS);
config.position.fixed_position = true; config.position.fixed_position = true;
#endif #endif
@@ -479,6 +562,29 @@ bool isBroadcast(uint32_t dest)
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA; return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
} }
namespace
{
template <typename Map, typename Value> bool copySatelliteEntry(const Map &map, NodeNum n, Value &out)
{
auto it = map.find(n);
if (it == map.end())
return false;
out = it->second;
return true;
}
template <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map &map, NodeNum exclude)
{
std::vector<NodeNum> result;
result.reserve(map.size());
for (const auto &kv : map) {
if (kv.first != exclude)
result.push_back(kv.first);
}
return result;
}
} // namespace
void NodeDB::resetRadioConfig(bool is_fresh_install) void NodeDB::resetRadioConfig(bool is_fresh_install)
{ {
if (is_fresh_install) { if (is_fresh_install) {
@@ -548,6 +654,19 @@ void NodeDB::installDefaultNodeDatabase()
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES); nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
numMeshNodes = 0; numMeshNodes = 0;
meshNodes = &nodeDatabase.nodes; meshNodes = &nodeDatabase.nodes;
concurrency::LockGuard satelliteGuard(&satelliteMutex);
#if !MESHTASTIC_EXCLUDE_POSITIONDB
nodePositions.clear();
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
nodeTelemetry.clear();
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
nodeEnvironment.clear();
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
nodeStatus.clear();
#endif
} }
void NodeDB::installDefaultConfig(bool preserveKey = false) void NodeDB::installDefaultConfig(bool preserveKey = false)
@@ -1019,12 +1138,14 @@ void NodeDB::resetNodes(bool keepFavorites)
{ {
if (!config.position.fixed_position) if (!config.position.fixed_position)
clearLocalPosition(); clearLocalPosition();
NodeNum ourNum = getNodeNum();
numMeshNodes = 1; numMeshNodes = 1;
if (keepFavorites) { if (keepFavorites) {
LOG_INFO("Clearing node database - preserving favorites"); LOG_INFO("Clearing node database - preserving favorites");
for (size_t i = 0; i < meshNodes->size(); i++) { for (size_t i = 0; i < meshNodes->size(); i++) {
meshtastic_NodeInfoLite &node = meshNodes->at(i); meshtastic_NodeInfoLite &node = meshNodes->at(i);
if (i > 0 && !node.is_favorite) { if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
eraseNodeSatellites(node.num);
node = meshtastic_NodeInfoLite(); node = meshtastic_NodeInfoLite();
} else { } else {
numMeshNodes += 1; numMeshNodes += 1;
@@ -1032,8 +1153,14 @@ void NodeDB::resetNodes(bool keepFavorites)
}; };
} else { } else {
LOG_INFO("Clearing node database - removing favorites"); LOG_INFO("Clearing node database - removing favorites");
for (size_t i = 1; i < meshNodes->size(); i++) {
const NodeNum gone = meshNodes->at(i).num;
if (gone)
eraseNodeSatellites(gone);
}
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite()); std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
} }
(void)ourNum;
devicestate.has_rx_text_message = false; devicestate.has_rx_text_message = false;
devicestate.has_rx_waypoint = false; devicestate.has_rx_waypoint = false;
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
@@ -1054,36 +1181,173 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
numMeshNodes -= removed; numMeshNodes -= removed;
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
meshtastic_NodeInfoLite()); meshtastic_NodeInfoLite());
if (removed)
eraseNodeSatellites(nodeNum);
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed); LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
} }
void NodeDB::clearLocalPosition() void NodeDB::clearLocalPosition()
{ {
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum()); #if !MESHTASTIC_EXCLUDE_POSITIONDB
node->position.latitude_i = 0; concurrency::LockGuard guard(&satelliteMutex);
node->position.longitude_i = 0; nodePositions.erase(getNodeNum());
node->position.altitude = 0; #endif
node->position.time = 0;
setLocalPosition(meshtastic_Position_init_default); setLocalPosition(meshtastic_Position_init_default);
localPositionUpdatedSinceBoot = false; localPositionUpdatedSinceBoot = false;
} }
bool NodeDB::copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
(void)n;
(void)out;
return false;
#else
concurrency::LockGuard guard(&satelliteMutex);
return copySatelliteEntry(nodePositions, n, out);
#endif
}
bool NodeDB::copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const
{
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
(void)n;
(void)out;
return false;
#else
concurrency::LockGuard guard(&satelliteMutex);
return copySatelliteEntry(nodeTelemetry, n, out);
#endif
}
bool NodeDB::copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const
{
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
(void)n;
(void)out;
return false;
#else
concurrency::LockGuard guard(&satelliteMutex);
return copySatelliteEntry(nodeEnvironment, n, out);
#endif
}
bool NodeDB::copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const
{
#if MESHTASTIC_EXCLUDE_STATUSDB
(void)n;
(void)out;
return false;
#else
concurrency::LockGuard guard(&satelliteMutex);
return copySatelliteEntry(nodeStatus, n, out);
#endif
}
std::vector<NodeNum> NodeDB::snapshotPositionNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodePositions, exclude);
#endif
}
std::vector<NodeNum> NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodeTelemetry, exclude);
#endif
}
std::vector<NodeNum> NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodeEnvironment, exclude);
#endif
}
std::vector<NodeNum> NodeDB::snapshotStatusNodeNums(NodeNum exclude) const
{
#if MESHTASTIC_EXCLUDE_STATUSDB
(void)exclude;
return {};
#else
concurrency::LockGuard guard(&satelliteMutex);
return snapshotSatelliteNodeNums(nodeStatus, exclude);
#endif
}
void NodeDB::setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status)
{
#if MESHTASTIC_EXCLUDE_STATUSDB
(void)n;
(void)status;
#else
concurrency::LockGuard guard(&satelliteMutex);
nodeStatus[n] = status;
#endif
}
void NodeDB::touchNodePositionTime(NodeNum n, uint32_t time)
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
(void)n;
(void)time;
#else
concurrency::LockGuard guard(&satelliteMutex);
nodePositions[n].time = time;
#endif
}
void NodeDB::eraseNodeSatellites(NodeNum n)
{
concurrency::LockGuard guard(&satelliteMutex);
#if !MESHTASTIC_EXCLUDE_POSITIONDB
nodePositions.erase(n);
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
nodeTelemetry.erase(n);
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
nodeEnvironment.erase(n);
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
nodeStatus.erase(n);
#endif
}
void NodeDB::cleanupMeshDB() void NodeDB::cleanupMeshDB()
{ {
int newPos = 0, removed = 0; int newPos = 0, removed = 0;
for (int i = 0; i < numMeshNodes; i++) { for (int i = 0; i < numMeshNodes; i++) {
if (meshNodes->at(i).has_user) { meshtastic_NodeInfoLite &n = meshNodes->at(i);
if (meshNodes->at(i).user.public_key.size > 0) { if (nodeInfoLiteHasUser(&n)) {
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) { if (n.public_key.size > 0) {
meshNodes->at(i).user.public_key.size = 0; if (memfll(n.public_key.bytes, 0, n.public_key.size)) {
n.public_key.size = 0;
} }
} }
if (newPos != i) if (newPos != i)
meshNodes->at(newPos++) = meshNodes->at(i); meshNodes->at(newPos++) = n;
else else
newPos++; newPos++;
} else { } else {
// No user info - drop this node and its satellites
const NodeNum gone = n.num;
if (gone)
eraseNodeSatellites(gone);
removed++; removed++;
} }
} }
@@ -1141,14 +1405,22 @@ void NodeDB::pickNewNodeNum()
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5]; nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
} }
// Identity check via public key (or "empty slot?" when no keys yet);
// macaddr no longer lives on the slim header.
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
if (!n)
return false;
if (owner.public_key.size == 32 && n->public_key.size == 32)
return memcmp(n->public_key.bytes, owner.public_key.bytes, 32) == 0;
return !nodeInfoLiteHasUser(n);
};
meshtastic_NodeInfoLite *found; meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) || while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) { (nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found) if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so " LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
"trying for 0x%x",
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
nodeNum = candidate; nodeNum = candidate;
} }
LOG_DEBUG("Use nodenum 0x%x ", nodeNum); LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
@@ -1169,7 +1441,8 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
if (f) { if (f) {
LOG_INFO("Load %s", filename); LOG_INFO("Load %s", filename);
pb_istream_t stream = {&readcb, &f, protoSize}; pb_istream_t stream = {&readcb, &f, protoSize};
if (fields != &meshtastic_NodeDatabase_msg) // contains a vector object if (fields != &meshtastic_NodeDatabase_msg &&
fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members
memset(dest_struct, 0, objSize); memset(dest_struct, 0, objSize);
if (!pb_decode(&stream, fields, dest_struct)) { if (!pb_decode(&stream, fields, dest_struct)) {
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
@@ -1242,9 +1515,57 @@ void NodeDB::loadFromDisk()
if (nodeDatabase.version < DEVICESTATE_MIN_VER) { if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version); LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
installDefaultNodeDatabase(); installDefaultNodeDatabase();
} else if (nodeDatabase.version < DEVICESTATE_CUR_VER) {
if (migrateLegacyNodeDatabase())
saveNodeDatabaseToDisk();
else
installDefaultNodeDatabase();
} else { } else {
meshNodes = &nodeDatabase.nodes; meshNodes = &nodeDatabase.nodes;
numMeshNodes = nodeDatabase.nodes.size(); numMeshNodes = nodeDatabase.nodes.size();
// Hydrate the satellite maps; the on-disk vectors stay empty in steady
// state and are repopulated only at save time.
concurrency::LockGuard guard(&satelliteMutex);
#if !MESHTASTIC_EXCLUDE_POSITIONDB
nodePositions.clear();
nodePositions.reserve(nodeDatabase.positions.size());
for (const auto &entry : nodeDatabase.positions) {
if (entry.has_position)
nodePositions[entry.num] = entry.position;
}
nodeDatabase.positions.clear();
nodeDatabase.positions.shrink_to_fit();
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
nodeTelemetry.clear();
nodeTelemetry.reserve(nodeDatabase.telemetry.size());
for (const auto &entry : nodeDatabase.telemetry) {
if (entry.has_device_metrics)
nodeTelemetry[entry.num] = entry.device_metrics;
}
nodeDatabase.telemetry.clear();
nodeDatabase.telemetry.shrink_to_fit();
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
nodeEnvironment.clear();
nodeEnvironment.reserve(nodeDatabase.environment.size());
for (const auto &entry : nodeDatabase.environment) {
if (entry.has_environment_metrics)
nodeEnvironment[entry.num] = entry.environment_metrics;
}
nodeDatabase.environment.clear();
nodeDatabase.environment.shrink_to_fit();
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
nodeStatus.clear();
nodeStatus.reserve(nodeDatabase.status.size());
for (const auto &entry : nodeDatabase.status) {
if (entry.has_status)
nodeStatus[entry.num] = entry.status;
}
nodeDatabase.status.clear();
nodeDatabase.status.shrink_to_fit();
#endif
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size()); LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
} }
@@ -1272,16 +1593,16 @@ void NodeDB::loadFromDisk()
// Attempt recovery of owner fields from our own NodeDB entry if available. // Attempt recovery of owner fields from our own NodeDB entry if available.
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum()); meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
if (us && us->has_user) { if (nodeInfoLiteHasUser(us)) {
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x", LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
us->num); us->num);
memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name)); memcpy(owner.long_name, us->long_name, sizeof(owner.long_name));
owner.long_name[sizeof(owner.long_name) - 1] = '\0'; owner.long_name[sizeof(owner.long_name) - 1] = '\0';
memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name)); memcpy(owner.short_name, us->short_name, sizeof(owner.short_name));
owner.short_name[sizeof(owner.short_name) - 1] = '\0'; owner.short_name[sizeof(owner.short_name) - 1] = '\0';
owner.is_licensed = us->user.is_licensed; owner.is_licensed = nodeInfoLiteIsLicensed(us);
owner.has_is_unmessagable = us->user.has_is_unmessagable; owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us);
owner.is_unmessagable = us->user.is_unmessagable; owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us);
// Save the recovered owner to device state on disk // Save the recovered owner to device state on disk
saveToDisk(SEGMENT_DEVICESTATE); saveToDisk(SEGMENT_DEVICESTATE);
@@ -1528,9 +1849,75 @@ bool NodeDB::saveNodeDatabaseToDisk()
FSCom.mkdir("/prefs"); FSCom.mkdir("/prefs");
spiLock->unlock(); spiLock->unlock();
#endif #endif
// Project the maps into the on-disk vectors just before encoding; cleared
// again on the way out so we don't carry duplicate state.
concurrency::LockGuard guard(&satelliteMutex);
#if !MESHTASTIC_EXCLUDE_POSITIONDB
nodeDatabase.positions.clear();
nodeDatabase.positions.reserve(nodePositions.size());
for (const auto &kv : nodePositions) {
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_default;
entry.num = kv.first;
entry.has_position = true;
entry.position = kv.second;
nodeDatabase.positions.push_back(entry);
}
#else
nodeDatabase.positions.clear();
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
nodeDatabase.telemetry.clear();
nodeDatabase.telemetry.reserve(nodeTelemetry.size());
for (const auto &kv : nodeTelemetry) {
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_default;
entry.num = kv.first;
entry.has_device_metrics = true;
entry.device_metrics = kv.second;
nodeDatabase.telemetry.push_back(entry);
}
#else
nodeDatabase.telemetry.clear();
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
nodeDatabase.environment.clear();
nodeDatabase.environment.reserve(nodeEnvironment.size());
for (const auto &kv : nodeEnvironment) {
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_default;
entry.num = kv.first;
entry.has_environment_metrics = true;
entry.environment_metrics = kv.second;
nodeDatabase.environment.push_back(entry);
}
#else
nodeDatabase.environment.clear();
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
nodeDatabase.status.clear();
nodeDatabase.status.reserve(nodeStatus.size());
for (const auto &kv : nodeStatus) {
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_default;
entry.num = kv.first;
entry.has_status = true;
entry.status = kv.second;
nodeDatabase.status.push_back(entry);
}
#else
nodeDatabase.status.clear();
#endif
size_t nodeDatabaseSize; size_t nodeDatabaseSize;
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase); pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false); bool ok = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
nodeDatabase.positions.clear();
nodeDatabase.positions.shrink_to_fit();
nodeDatabase.telemetry.clear();
nodeDatabase.telemetry.shrink_to_fit();
nodeDatabase.environment.clear();
nodeDatabase.environment.shrink_to_fit();
nodeDatabase.status.clear();
nodeDatabase.status.shrink_to_fit();
return ok;
} }
bool NodeDB::saveToDiskNoRetry(int saveWhat) bool NodeDB::saveToDiskNoRetry(int saveWhat)
@@ -1699,7 +2086,7 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
// FIXME this implementation is kinda expensive // FIXME this implementation is kinda expensive
for (int i = 0; i < numMeshNodes; i++) { for (int i = 0; i < numMeshNodes; i++) {
if (localOnly && meshNodes->at(i).via_mqtt) if (localOnly && nodeInfoLiteViaMqtt(&meshNodes->at(i)))
continue; continue;
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS) if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
numseen++; numseen++;
@@ -1736,62 +2123,94 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
return; return;
} }
#if MESHTASTIC_EXCLUDE_POSITIONDB
// Build flag opted out: header still tracks last_heard via updateFrom; we
// simply don't cache the position payload anywhere on this device.
if (src == RX_SRC_LOCAL) { if (src == RX_SRC_LOCAL) {
// Local packet, fully authoritative LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i,
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
p.altitude); p.altitude);
setLocalPosition(p); setLocalPosition(p);
info->position = TypeConversions::ConvertToPositionLite(p);
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
// (stop-gap fix for issue #900)
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
info->position.time = p.time;
} else {
// Be careful to only update fields that have been set by the REMOTE sender
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
// recorded based on the packet rxTime
//
// FIXME perhaps handle RX_SRC_USER separately?
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
// First, back up fields that we want to protect from overwrite
uint32_t tmp_time = info->position.time;
// Next, update atomically
info->position = TypeConversions::ConvertToPositionLite(p);
// Last, restore any fields that may have been overwritten
if (!info->position.time)
info->position.time = tmp_time;
} }
info->has_position = true; (void)nodeId;
updateGUIforNode = info;
notifyObservers(true);
#else
{
concurrency::LockGuard guard(&satelliteMutex);
meshtastic_PositionLite &slot = nodePositions[nodeId]; // creates default-zero entry if missing
if (src == RX_SRC_LOCAL) {
// Local packet, fully authoritative
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
p.altitude);
setLocalPosition(p);
slot = TypeConversions::ConvertToPositionLite(p);
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
// (stop-gap fix for issue #900)
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
slot.time = p.time;
} else {
// Be careful to only update fields that have been set by the REMOTE sender
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
// recorded based on the packet rxTime
//
// FIXME perhaps handle RX_SRC_USER separately?
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
// First, back up fields that we want to protect from overwrite
uint32_t tmp_time = slot.time;
// Next, update atomically
slot = TypeConversions::ConvertToPositionLite(p);
// Last, restore any fields that may have been overwritten
if (!slot.time)
slot.time = tmp_time;
}
}
updateGUIforNode = info; updateGUIforNode = info;
notifyObservers(true); // Force an update whether or not our node counts have changed notifyObservers(true); // Force an update whether or not our node counts have changed
#endif
} }
/** Update telemetry info for this node based on received metrics /** Update telemetry info for this node based on received metrics. Stores
* We only care about device telemetry here * device_metrics and environment_metrics into their respective satellite
* maps; other variants (air_quality, power, local_stats, health) are
* intentionally not retained per-node.
*/ */
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src) void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
{ {
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId); meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
// Environment metrics should never go to NodeDb but we'll safegaurd anyway if (!info)
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
return; return;
}
if (src == RX_SRC_LOCAL) { if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
// Local packet, fully authoritative if (src == RX_SRC_LOCAL) {
LOG_DEBUG("updateTelemetry LOCAL"); LOG_DEBUG("updateTelemetry LOCAL device");
} else {
LOG_DEBUG("updateTelemetry REMOTE device node=0x%x", nodeId);
}
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
concurrency::LockGuard guard(&satelliteMutex);
nodeTelemetry[nodeId] = t.variant.device_metrics;
#endif
} else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) {
if (src == RX_SRC_LOCAL) {
LOG_DEBUG("updateTelemetry LOCAL env");
} else {
LOG_DEBUG("updateTelemetry REMOTE env node=0x%x", nodeId);
}
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
concurrency::LockGuard guard(&satelliteMutex);
nodeEnvironment[nodeId] = t.variant.environment_metrics;
#endif
} else { } else {
LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId); return; // air_quality / power / local_stats / health: not stored per-node
} }
info->device_metrics = t.variant.device_metrics;
info->has_device_metrics = true;
updateGUIforNode = info; updateGUIforNode = info;
notifyObservers(true); // Force an update whether or not our node counts have changed notifyObservers(true);
} }
/** /**
@@ -1806,24 +2225,22 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
// If the local node has this node marked as manually verified // If the local node has this node marked as manually verified
// and the client does not, do not allow the client to update the // and the client does not, do not allow the client to update the
// saved public key. // saved public key.
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) { if (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) {
if (contact.user.public_key.size != info->user.public_key.size || if (contact.user.public_key.size != info->public_key.size ||
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) { memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) {
return; return;
} }
} }
info->num = contact.node_num; info->num = contact.node_num;
info->has_user = true; TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
info->user = TypeConversions::ConvertToUserLite(contact.user);
if (contact.should_ignore) { if (contact.should_ignore) {
// If should_ignore is set, // If should_ignore is set,
// we need to clear the public key and other cruft, in addition to setting the node as ignored // we need to clear the public key and other cruft, in addition to setting the node as ignored
info->is_ignored = true; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
info->is_favorite = false; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
info->has_device_metrics = false; eraseNodeSatellites(contact.node_num);
info->has_position = false; info->public_key.size = 0;
info->user.public_key.size = 0; memset(info->public_key.bytes, 0, sizeof(info->public_key.bytes));
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
} else { } else {
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
@@ -1842,12 +2259,12 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
} else { } else {
// Normal case: set is_favorite to prevent expiration. // Normal case: set is_favorite to prevent expiration.
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB). // last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
info->is_favorite = true; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
} }
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified // As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
if (contact.manually_verified) { if (contact.manually_verified) {
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK, true);
} }
// Mark the node's key as manually verified to indicate trustworthiness. // Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info; updateGUIforNode = info;
@@ -1887,9 +2304,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
return false; return false;
} }
} }
if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
// if the key doesn't match, don't update nodeDB at all. // if the key doesn't match, don't update nodeDB at all.
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) { if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) {
LOG_WARN("Public Key mismatch, dropping NodeInfo"); LOG_WARN("Public Key mismatch, dropping NodeInfo");
return false; return false;
} }
@@ -1902,19 +2319,22 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
// Always ensure user.id is derived from nodeId, regardless of what was received // Always ensure user.id is derived from nodeId, regardless of what was received
snprintf(p.id, sizeof(p.id), "!%08x", nodeId); snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
// Both of info->user and p start as filled with zero so I think this is okay meshtastic_NodeInfoLite before = *info;
auto lite = TypeConversions::ConvertToUserLite(p); TypeConversions::CopyUserToNodeInfoLite(info, p);
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex); bool changed =
(memcmp(before.long_name, info->long_name, sizeof(info->long_name)) != 0) ||
(memcmp(before.short_name, info->short_name, sizeof(info->short_name)) != 0) || (before.hw_model != info->hw_model) ||
(before.role != info->role) || (before.public_key.size != info->public_key.size) ||
(info->public_key.size > 0 && memcmp(before.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) ||
(before.bitfield != info->bitfield) || (info->channel != channelIndex);
info->user = lite; if (info->public_key.size == 32) {
if (info->user.public_key.size == 32) { printBytes("Saved Pubkey: ", info->public_key.bytes, 32);
printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32);
} }
if (nodeId != getNodeNum()) if (nodeId != getNodeNum())
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel) info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId, LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->long_name, info->short_name, nodeId,
info->channel); info->channel);
info->has_user = true;
if (changed) { if (changed) {
updateGUIforNode = info; updateGUIforNode = info;
@@ -1956,7 +2376,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
if (mp.rx_snr) if (mp.rx_snr)
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node. info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
mp.via_mqtt); // Store if we received this packet via MQTT
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
const int8_t hopsAway = getHopsAway(mp); const int8_t hopsAway = getHopsAway(mp);
@@ -1971,8 +2392,8 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
{ {
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite && lite->is_favorite != is_favorite) { if (lite && nodeInfoLiteIsFavorite(lite) != is_favorite) {
lite->is_favorite = is_favorite; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite);
sortMeshDB(); sortMeshDB();
saveNodeDatabaseToDisk(); saveNodeDatabaseToDisk();
} }
@@ -1986,10 +2407,10 @@ bool NodeDB::isFavorite(uint32_t nodeId)
if (nodeId == NODENUM_BROADCAST) if (nodeId == NODENUM_BROADCAST)
return false; return false;
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite) { if (lite) {
return lite->is_favorite; return nodeInfoLiteIsFavorite(lite);
} }
return false; return false;
} }
@@ -2014,14 +2435,14 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
lite = &meshNodes->at(i); lite = &meshNodes->at(i);
if (lite->num == p.from) { if (lite->num == p.from) {
if (lite->is_favorite) if (nodeInfoLiteIsFavorite(lite))
return true; return true;
seenFrom = true; seenFrom = true;
} }
if (lite->num == p.to) { if (lite->num == p.to) {
if (lite->is_favorite) if (nodeInfoLiteIsFavorite(lite))
return true; return true;
seenTo = true; seenTo = true;
@@ -2057,10 +2478,10 @@ void NodeDB::sortMeshDB()
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash // TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
std::swap(meshNodes->at(i), meshNodes->at(i - 1)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true; changed = true;
} else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) { } else if (nodeInfoLiteIsFavorite(&meshNodes->at(i)) && !nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true; changed = true;
} else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) { } else if (!nodeInfoLiteIsFavorite(&meshNodes->at(i)) && nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
// noop // noop
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) { } else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1)); std::swap(meshNodes->at(i), meshNodes->at(i - 1));
@@ -2120,17 +2541,18 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
int oldestIndex = -1; int oldestIndex = -1;
int oldestBoringIndex = -1; int oldestBoringIndex = -1;
for (int i = 1; i < numMeshNodes; i++) { for (int i = 1; i < numMeshNodes; i++) {
const meshtastic_NodeInfoLite *cand = &meshNodes->at(i);
const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand);
const bool isIgnored = nodeInfoLiteIsIgnored(cand);
const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand);
// Simply the oldest non-favorite, non-ignored, non-verified node // Simply the oldest non-favorite, non-ignored, non-verified node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && oldest = cand->last_heard;
meshNodes->at(i).last_heard < oldest) {
oldest = meshNodes->at(i).last_heard;
oldestIndex = i; oldestIndex = i;
} }
// The oldest "boring" node // The oldest "boring" node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 && if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
meshNodes->at(i).last_heard < oldestBoring) { oldestBoring = cand->last_heard;
oldestBoring = meshNodes->at(i).last_heard;
oldestBoringIndex = i; oldestBoringIndex = i;
} }
} }
@@ -2140,6 +2562,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
} }
if (oldestIndex != -1) { if (oldestIndex != -1) {
eraseNodeSatellites(meshNodes->at(oldestIndex).num);
// Shove the remaining nodes down the chain // Shove the remaining nodes down the chain
for (int i = oldestIndex; i < numMeshNodes - 1; i++) { for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
meshNodes->at(i) = meshNodes->at(i + 1); meshNodes->at(i) = meshNodes->at(i + 1);
@@ -2163,18 +2586,23 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
/// valid lat/lon /// valid lat/lon
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n) bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
{ {
return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0); if (!n)
return false;
if (n->num == getNodeNum()) {
return localPosition.latitude_i != 0 || localPosition.longitude_i != 0;
}
meshtastic_PositionLite pos;
return copyNodePosition(n->num, pos) && (pos.latitude_i != 0 || pos.longitude_i != 0);
} }
/// If we have a node / user and they report is_licensed = true /// If we have a node / user and they report is_licensed = true
/// we consider them licensed /// we consider them licensed
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
{ {
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum); const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
if (!info || !info->has_user) { if (!nodeInfoLiteHasUser(info))
return UserLicenseStatus::NotKnown; return UserLicenseStatus::NotKnown;
} return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
} }
#if !defined(MESHTASTIC_EXCLUDE_PKI) #if !defined(MESHTASTIC_EXCLUDE_PKI)
+134 -4
View File
@@ -6,10 +6,12 @@
#include <assert.h> #include <assert.h>
#include <pb_encode.h> #include <pb_encode.h>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include "MeshTypes.h" #include "MeshTypes.h"
#include "NodeStatus.h" #include "NodeStatus.h"
#include "concurrency/Lock.h"
#include "configuration.h" #include "configuration.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode #include "mesh/generated/meshtastic/mesh.pb.h" // For CriticalErrorCode
@@ -82,7 +84,9 @@ DeviceState versions used to be defined in the .proto file but really only this
#define SEGMENT_CHANNELS 8 #define SEGMENT_CHANNELS 8
#define SEGMENT_NODEDATABASE 16 #define SEGMENT_NODEDATABASE 16
#define DEVICESTATE_CUR_VER 24 #define DEVICESTATE_CUR_VER 25
// Lowest on-disk version we still know how to load. v24 saves are migrated
// at boot via the parallel deviceonly_legacy descriptor and re-saved as v25.
#define DEVICESTATE_MIN_VER 24 #define DEVICESTATE_MIN_VER 24
extern meshtastic_DeviceState devicestate; extern meshtastic_DeviceState devicestate;
@@ -166,6 +170,21 @@ class NodeDB
Observable<const meshtastic::NodeStatus *> newStatus; Observable<const meshtastic::NodeStatus *> newStatus;
pb_size_t numMeshNodes; pb_size_t numMeshNodes;
// Satellite per-NodeNum maps for data we used to inline into NodeInfoLite,
// gated by MESHTASTIC_EXCLUDE_*DB so STM32WL can omit them.
#if !MESHTASTIC_EXCLUDE_POSITIONDB
std::unordered_map<NodeNum, meshtastic_PositionLite> nodePositions;
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
std::unordered_map<NodeNum, meshtastic_DeviceMetrics> nodeTelemetry;
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
std::unordered_map<NodeNum, meshtastic_EnvironmentMetrics> nodeEnvironment;
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
std::unordered_map<NodeNum, meshtastic_StatusMessage> nodeStatus;
#endif
bool keyIsLowEntropy = false; bool keyIsLowEntropy = false;
bool hasWarned = false; bool hasWarned = false;
@@ -277,6 +296,43 @@ class NodeDB
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n); virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
size_t getNumMeshNodes() { return numMeshNodes; } size_t getNumMeshNodes() { return numMeshNodes; }
// Thread-safe satellite-map accessors. Return false if absent or the
// corresponding DB is compiled out.
bool copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const;
bool copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const;
bool copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const;
bool copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const;
std::vector<NodeNum> snapshotPositionNodeNums(NodeNum exclude) const;
std::vector<NodeNum> snapshotTelemetryNodeNums(NodeNum exclude) const;
std::vector<NodeNum> snapshotEnvironmentNodeNums(NodeNum exclude) const;
std::vector<NodeNum> snapshotStatusNodeNums(NodeNum exclude) const;
void setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status);
void touchNodePositionTime(NodeNum n, uint32_t time);
bool hasNodePosition(NodeNum n) const
{
meshtastic_PositionLite scratch;
return copyNodePosition(n, scratch);
}
bool hasNodeTelemetry(NodeNum n) const
{
meshtastic_DeviceMetrics scratch;
return copyNodeTelemetry(n, scratch);
}
bool hasNodeEnvironment(NodeNum n) const
{
meshtastic_EnvironmentMetrics scratch;
return copyNodeEnvironment(n, scratch);
}
bool hasNodeStatus(NodeNum n) const
{
meshtastic_StatusMessage scratch;
return copyNodeStatus(n, scratch);
}
void eraseNodeSatellites(NodeNum n);
UserLicenseStatus getLicenseStatus(uint32_t nodeNum); UserLicenseStatus getLicenseStatus(uint32_t nodeNum);
size_t getMaxNodesAllocatedSize() size_t getMaxNodesAllocatedSize()
@@ -285,7 +341,11 @@ class NodeDB
emptyNodeDatabase.version = DEVICESTATE_CUR_VER; emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
size_t nodeDatabaseSize; size_t nodeDatabaseSize;
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase); pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size); // Always include satellite slots so backups from higher-cap peers
// decode without truncation, even when our build excludes the DBs.
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size) +
(MAX_NUM_NODES * meshtastic_NodePositionEntry_size) + (MAX_NUM_NODES * meshtastic_NodeTelemetryEntry_size) +
(MAX_NUM_NODES * meshtastic_NodeEnvironmentEntry_size) + (MAX_NUM_NODES * meshtastic_NodeStatusEntry_size);
} }
// returns true if the maximum number of nodes is reached or we are running low on memory // returns true if the maximum number of nodes is reached or we are running low on memory
@@ -329,6 +389,7 @@ class NodeDB
} }
private: private:
mutable concurrency::Lock satelliteMutex;
bool duplicateWarned = false; bool duplicateWarned = false;
bool localPositionUpdatedSinceBoot = false; bool localPositionUpdatedSinceBoot = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
@@ -363,6 +424,11 @@ class NodeDB
bool saveDeviceStateToDisk(); bool saveDeviceStateToDisk();
bool saveNodeDatabaseToDisk(); bool saveNodeDatabaseToDisk();
void sortMeshDB(); void sortMeshDB();
// Defined in NodeDBLegacyMigration.cpp. Decodes /prefs/nodes.proto via
// the legacy descriptor and copies entries into the v25 layout. Caller
// is responsible for save / install-default on the result.
bool migrateLegacyNodeDatabase();
}; };
extern NodeDB *nodeDB; extern NodeDB *nodeDB;
@@ -397,10 +463,74 @@ extern meshtastic_CriticalErrorCode error_code;
* A numeric error address (nonzero if available) * A numeric error address (nonzero if available)
*/ */
extern uint32_t error_address; extern uint32_t error_address;
// Bit assignments for meshtastic_NodeInfoLite.bitfield.
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0 #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT 0
#define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1 << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT) #define NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK (1u << NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_SHIFT)
#define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1 #define NODEINFO_BITFIELD_IS_MUTED_SHIFT 1
#define NODEINFO_BITFIELD_IS_MUTED_MASK (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT) #define NODEINFO_BITFIELD_IS_MUTED_MASK (1u << NODEINFO_BITFIELD_IS_MUTED_SHIFT)
#define NODEINFO_BITFIELD_VIA_MQTT_SHIFT 2
#define NODEINFO_BITFIELD_VIA_MQTT_MASK (1u << NODEINFO_BITFIELD_VIA_MQTT_SHIFT)
#define NODEINFO_BITFIELD_IS_FAVORITE_SHIFT 3
#define NODEINFO_BITFIELD_IS_FAVORITE_MASK (1u << NODEINFO_BITFIELD_IS_FAVORITE_SHIFT)
#define NODEINFO_BITFIELD_IS_IGNORED_SHIFT 4
#define NODEINFO_BITFIELD_IS_IGNORED_MASK (1u << NODEINFO_BITFIELD_IS_IGNORED_SHIFT)
#define NODEINFO_BITFIELD_HAS_USER_SHIFT 5
#define NODEINFO_BITFIELD_HAS_USER_MASK (1u << NODEINFO_BITFIELD_HAS_USER_SHIFT)
#define NODEINFO_BITFIELD_IS_LICENSED_SHIFT 6
#define NODEINFO_BITFIELD_IS_LICENSED_MASK (1u << NODEINFO_BITFIELD_IS_LICENSED_SHIFT)
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT 7
#define NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_IS_UNMESSAGABLE_SHIFT)
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT 8
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
// Bits 9..31 reserved for future single-bit flags.
// Convenience accessors so call sites read like the old struct fields.
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK);
}
inline bool nodeInfoLiteViaMqtt(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK);
}
inline bool nodeInfoLiteIsFavorite(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK);
}
inline bool nodeInfoLiteIsIgnored(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK);
}
inline bool nodeInfoLiteIsLicensed(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK);
}
inline bool nodeInfoLiteHasIsUnmessagable(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK);
}
inline bool nodeInfoLiteIsUnmessagable(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK);
}
inline bool nodeInfoLiteIsMuted(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK);
}
inline bool nodeInfoLiteIsKeyManuallyVerified(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK);
}
inline void nodeInfoLiteSetBit(meshtastic_NodeInfoLite *n, uint32_t mask, bool value)
{
if (!n)
return;
if (value)
n->bitfield |= mask;
else
n->bitfield &= ~mask;
}
#define Module_Config_size \ #define Module_Config_size \
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \ (ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
+129
View File
@@ -0,0 +1,129 @@
// Migration from the legacy (pre-v25) NodeDatabase shape to the slim header +
// satellite maps layout. Lives in its own translation unit so the bulk
// fixed-shape decode + field-by-field copy doesn't clutter NodeDB.cpp.
//
// Caller (NodeDB::loadFromDisk) decides what to do with the result:
// - true -> persist via saveNodeDatabaseToDisk()
// - false -> reset via installDefaultNodeDatabase()
//
// This file (and the deviceonly_legacy proto) can be removed once
// DEVICESTATE_MIN_VER advances past 24.
#include "NodeDB.h"
#include "concurrency/LockGuard.h"
#include "configuration.h"
#include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include <algorithm>
#include <cstring>
#include <pb_decode.h>
#include <pb_encode.h>
bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
{
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
for (auto item : *vec) {
if (!pb_encode_tag_for_field(ostream, iter))
return false;
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_Legacy_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodeInfoLite_Legacy node;
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite_Legacy> *>(iter->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_Legacy_fields, &node))
vec->push_back(node);
}
return true;
}
bool NodeDB::migrateLegacyNodeDatabase()
{
LOG_WARN("NodeDatabase v%u: migrating to v%u", nodeDatabase.version, DEVICESTATE_CUR_VER);
// _init_zero brace-inits the embedded std::vector via its explicit
// (size_type, allocator) ctor, so default-construct instead.
meshtastic_NodeDatabase_Legacy legacyDb{};
legacyDb.version = 0;
meshtastic_NodeDatabase_Legacy legacyEmpty{};
legacyEmpty.version = DEVICESTATE_CUR_VER;
size_t legacyEmptyEncoded = 0;
pb_get_encoded_size(&legacyEmptyEncoded, meshtastic_NodeDatabase_Legacy_fields, &legacyEmpty);
const size_t legacyBufSize = legacyEmptyEncoded + (MAX_NUM_NODES * meshtastic_NodeInfoLite_Legacy_size);
auto legacyState = loadProto(nodeDatabaseFileName, legacyBufSize, sizeof(meshtastic_NodeDatabase_Legacy),
&meshtastic_NodeDatabase_Legacy_msg, &legacyDb);
if (legacyState != LoadFileResult::LOAD_SUCCESS) {
LOG_ERROR("Failed to load NodeDatabase via legacy descriptor; installing default");
return false;
}
nodeDatabase.nodes.clear();
const size_t maxToMigrate = std::min<size_t>(legacyDb.nodes.size(), MAX_NUM_NODES);
nodeDatabase.nodes.reserve(maxToMigrate);
size_t posCount = 0, telCount = 0;
{
concurrency::LockGuard guard(&satelliteMutex);
for (size_t i = 0; i < maxToMigrate; ++i) {
const auto &legacy = legacyDb.nodes[i];
meshtastic_NodeInfoLite slim = meshtastic_NodeInfoLite_init_default;
slim.num = legacy.num;
slim.snr = legacy.snr;
slim.last_heard = legacy.last_heard;
slim.channel = legacy.channel;
slim.has_hops_away = legacy.has_hops_away;
slim.hops_away = legacy.hops_away;
slim.next_hop = legacy.next_hop;
slim.bitfield = legacy.bitfield;
if (legacy.via_mqtt)
slim.bitfield |= NODEINFO_BITFIELD_VIA_MQTT_MASK;
if (legacy.is_favorite)
slim.bitfield |= NODEINFO_BITFIELD_IS_FAVORITE_MASK;
if (legacy.is_ignored)
slim.bitfield |= NODEINFO_BITFIELD_IS_IGNORED_MASK;
if (legacy.has_user) {
slim.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
strncpy(slim.long_name, legacy.user.long_name, sizeof(slim.long_name));
slim.long_name[sizeof(slim.long_name) - 1] = '\0';
strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name));
slim.short_name[sizeof(slim.short_name) - 1] = '\0';
slim.hw_model = legacy.user.hw_model;
slim.role = legacy.user.role;
if (legacy.user.is_licensed)
slim.bitfield |= NODEINFO_BITFIELD_IS_LICENSED_MASK;
slim.public_key.size = legacy.user.public_key.size;
memcpy(slim.public_key.bytes, legacy.user.public_key.bytes, sizeof(slim.public_key.bytes));
if (legacy.user.has_is_unmessagable) {
slim.bitfield |= NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK;
if (legacy.user.is_unmessagable)
slim.bitfield |= NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK;
}
// macaddr deprecated since 1.2.11 - dropped from slim header.
}
nodeDatabase.nodes.push_back(slim);
#if !MESHTASTIC_EXCLUDE_POSITIONDB
if (legacy.has_position)
nodePositions[legacy.num] = legacy.position;
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
if (legacy.has_device_metrics)
nodeTelemetry[legacy.num] = legacy.device_metrics;
#endif
}
#if !MESHTASTIC_EXCLUDE_POSITIONDB
posCount = nodePositions.size();
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
telCount = nodeTelemetry.size();
#endif
}
nodeDatabase.version = DEVICESTATE_CUR_VER;
meshNodes = &nodeDatabase.nodes;
numMeshNodes = nodeDatabase.nodes.size();
LOG_INFO("Migrated %u nodes from legacy -> v%u (positions: %u, telemetry: %u)", (unsigned)numMeshNodes, DEVICESTATE_CUR_VER,
(unsigned)posCount, (unsigned)telCount);
return true;
}
+26 -41
View File
@@ -17,7 +17,7 @@
#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging #define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging
#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots #define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initialize members
{ {
if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense
LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX);
@@ -34,7 +34,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
// Allocate memory for the recent packets array // Allocate memory for the recent packets array
recentPacketsCapacity = size; recentPacketsCapacity = size;
recentPackets = new PacketRecord[recentPacketsCapacity]; recentPackets.reset(new PacketRecord[recentPacketsCapacity]);
if (!recentPackets) { // No logging here, console/log probably uninitialized yet. if (!recentPackets) { // No logging here, console/log probably uninitialized yet.
LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size,
sizeof(PacketRecord) * recentPacketsCapacity); sizeof(PacketRecord) * recentPacketsCapacity);
@@ -43,33 +43,20 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
} }
// Initialize the recent packets array to zero // Initialize the recent packets array to zero
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH #if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Allocate hash index with load factor <= 0.5 for short probe chains // Allocate hash index with load factor <= 0.5 for short probe chains
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2); hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
hashMask = hashCapacity - 1; hashMask = hashCapacity - 1;
hashIndex = new uint16_t[hashCapacity]; hashIndex.reset(new uint16_t[hashCapacity]);
if (!hashIndex) { if (!hashIndex) {
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity); LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
hashCapacity = 0; hashCapacity = 0;
hashMask = 0; hashMask = 0;
return; return;
} }
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF) memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
#endif
}
PacketHistory::~PacketHistory()
{
recentPacketsCapacity = 0;
delete[] recentPackets;
recentPackets = NULL;
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
delete[] hashIndex;
hashIndex = NULL;
hashCapacity = 0;
hashMask = 0;
#endif #endif
} }
@@ -285,7 +272,7 @@ void PacketHistory::hashRebuild()
{ {
if (!hashIndex) if (!hashIndex)
return; return;
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity);
for (uint32_t i = 0; i < recentPacketsCapacity; i++) { for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
if (recentPackets[i].rxTimeMsec != 0) if (recentPackets[i].rxTimeMsec != 0)
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i); hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
@@ -332,7 +319,8 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
#endif #endif
// Linear scan (sole path when hash excluded, fallback when hash allocation failed) // Linear scan (sole path when hash excluded, fallback when hash allocation failed)
for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { PacketRecord *base = recentPackets.get();
for (PacketRecord *it = base; it < (base + recentPacketsCapacity); ++it) {
if (it->id == id && it->sender == sender) { if (it->id == id && it->sender == sender) {
return it; return it;
} }
@@ -346,39 +334,38 @@ void PacketHistory::insert(const PacketRecord &r)
{ {
uint32_t now_millis = millis(); // Should not jump with time changes uint32_t now_millis = millis(); // Should not jump with time changes
uint32_t OldtrxTimeMsec = 0; uint32_t OldtrxTimeMsec = 0;
PacketRecord *base = recentPackets.get();
PacketRecord *tu = NULL; // Will insert here. PacketRecord *tu = NULL; // Will insert here.
PacketRecord *it = NULL; PacketRecord *it = NULL;
// Find a free, matching or oldest used slot in the recentPackets array // Find a free, matching or oldest used slot in the recentPackets array
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { for (it = base; it < (base + recentPacketsCapacity); ++it) {
if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty
tu = it; // Remember the free slot tu = it; // Remember the free slot
#if VERBOSE_PACKET_HISTORY >= 2 #if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - base, recentPacketsCapacity);
#endif #endif
// We have that, Exit the loop // We have that, Exit the loop
it = (recentPackets + recentPacketsCapacity); it = (base + recentPacketsCapacity);
} else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert
tu = it; // Remember the matching slot tu = it; // Remember the matching slot
OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age
#if VERBOSE_PACKET_HISTORY >= 2 #if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
OldtrxTimeMsec);
#endif #endif
// We have that, Exit the loop // We have that, Exit the loop
it = (recentPackets + recentPacketsCapacity); it = (base + recentPacketsCapacity);
} else { } else {
if (it->rxTimeMsec == 0) { if (it->rxTimeMsec == 0) {
LOG_WARN( LOG_WARN(
"Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!",
it->sender, it->id, it - recentPackets, recentPacketsCapacity); it->sender, it->id, it - base, recentPacketsCapacity);
} }
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
OldtrxTimeMsec = now_millis - it->rxTimeMsec; OldtrxTimeMsec = now_millis - it->rxTimeMsec;
tu = it; // remember the oldest packet tu = it; // remember the oldest packet
#if VERBOSE_PACKET_HISTORY >= 2 #if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
OldtrxTimeMsec);
#endif #endif
} }
// keep looking for oldest till entire array is checked // keep looking for oldest till entire array is checked
@@ -393,13 +380,11 @@ void PacketHistory::insert(const PacketRecord &r)
#if VERBOSE_PACKET_HISTORY #if VERBOSE_PACKET_HISTORY
if (tu->id == 0 && tu->sender == 0) { if (tu->id == 0 && tu->sender == 0) {
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - base, recentPacketsCapacity);
} else if (tu->id == r.id && tu->sender == r.sender) { } else if (tu->id == r.id && tu->sender == r.sender) {
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
OldtrxTimeMsec);
} else { } else {
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
OldtrxTimeMsec);
} }
#endif #endif
@@ -432,9 +417,9 @@ void PacketHistory::insert(const PacketRecord &r)
#endif #endif
#if VERBOSE_PACKET_HISTORY #if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - base,
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
tu->relayed_by[2], tu->rxTimeMsec); tu->rxTimeMsec);
#endif #endif
if (r.rxTimeMsec == 0) { if (r.rxTimeMsec == 0) {
@@ -454,16 +439,16 @@ void PacketHistory::insert(const PacketRecord &r)
*tu = r; // store the packet *tu = r; // store the packet
if (!isMatchingSlot) { if (!isMatchingSlot) {
hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets)); hashInsert(r.sender, r.id, (uint16_t)(tu - base));
} }
#else #else
*tu = r; // store the packet *tu = r; // store the packet
#endif #endif
#if VERBOSE_PACKET_HISTORY #if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - base,
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
tu->relayed_by[2], tu->rxTimeMsec); tu->rxTimeMsec);
#endif #endif
} }
+4 -6
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "NodeDB.h" #include "NodeDB.h"
#include <memory>
// Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes // Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes
#define NUM_RELAYERS 6 #define NUM_RELAYERS 6
@@ -26,7 +27,7 @@ class PacketHistory
uint32_t recentPacketsCapacity = uint32_t recentPacketsCapacity =
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. std::unique_ptr<PacketRecord[]> recentPackets; // Simple and fixed in size. Debloat.
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH #if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan. // Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
@@ -34,7 +35,7 @@ class PacketHistory
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity), // The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot). // and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
static constexpr uint16_t HASH_EMPTY = 0xFFFF; static constexpr uint16_t HASH_EMPTY = 0xFFFF;
uint16_t *hashIndex = NULL; std::unique_ptr<uint16_t[]> hashIndex;
uint32_t hashCapacity = 0; // Always a power of 2 uint32_t hashCapacity = 0; // Always a power of 2
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
@@ -64,11 +65,8 @@ class PacketHistory
uint8_t getOurTxHopLimit(const PacketRecord &r); uint8_t getOurTxHopLimit(const PacketRecord &r);
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
PacketHistory(const PacketHistory &); // non construction-copyable
PacketHistory &operator=(const PacketHistory &); // non copyable
public: public:
explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX
~PacketHistory();
/** /**
* Update recentBroadcasts and return true if we have already seen this packet * Update recentBroadcasts and return true if we have already seen this packet
@@ -100,5 +98,5 @@ class PacketHistory
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
// To check if the PacketHistory was initialized correctly by constructor // To check if the PacketHistory was initialized correctly by constructor
bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } bool initOk(void) { return recentPackets != nullptr && recentPacketsCapacity != 0; }
}; };
+419 -7
View File
@@ -62,7 +62,7 @@ void PhoneAPI::handleStartConfig()
onConfigStart(); onConfigStart();
// even if we were already connected - restart our state machine // even if we were already connected - restart our state machine
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
// If client only wants node info, jump directly to sending nodes // If client only wants node info, jump directly to sending nodes
state = STATE_SEND_OWN_NODEINFO; state = STATE_SEND_OWN_NODEINFO;
LOG_INFO("Client only wants node info, skipping other config"); LOG_INFO("Client only wants node info, skipping other config");
@@ -81,6 +81,15 @@ void PhoneAPI::handleStartConfig()
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoForPhone = {}; nodeInfoForPhone = {};
nodeInfoQueue.clear(); nodeInfoQueue.clear();
replayQueue.clear();
replayPositionOrder.clear();
replayTelemetryOrder.clear();
replayEnvironmentOrder.clear();
replayStatusOrder.clear();
replayPositionIndex = 0;
replayTelemetryIndex = 0;
replayEnvironmentIndex = 0;
replayStatusIndex = 0;
} }
resetReadIndex(); resetReadIndex();
} }
@@ -120,6 +129,15 @@ void PhoneAPI::close()
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoForPhone = {}; nodeInfoForPhone = {};
nodeInfoQueue.clear(); nodeInfoQueue.clear();
replayQueue.clear();
replayPositionOrder.clear();
replayTelemetryOrder.clear();
replayEnvironmentOrder.clear();
replayStatusOrder.clear();
replayPositionIndex = 0;
replayTelemetryIndex = 0;
replayEnvironmentIndex = 0;
replayStatusIndex = 0;
} }
packetForPhone = NULL; packetForPhone = NULL;
filesManifest.clear(); filesManifest.clear();
@@ -302,7 +320,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
nodeInfoForPhone.num = 0; nodeInfoForPhone.num = 0;
} }
} }
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { if (config_nonce == SPECIAL_NONCE_ONLY_NODES || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
// If client only wants node info, jump directly to sending nodes // If client only wants node info, jump directly to sending nodes
state = STATE_SEND_OTHER_NODEINFOS; state = STATE_SEND_OTHER_NODEINFOS;
onNowHasData(0); onNowHasData(0);
@@ -529,8 +547,124 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis());
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
nodeInfoQueue.clear(); nodeInfoQueue.clear();
// Replay states no-op for legacy clients / excluded DBs.
state = STATE_REPLAY_POSITIONS;
return getFromRadio(buf);
}
break;
}
case STATE_REPLAY_POSITIONS: {
if (replayPositionOrder.empty() && replayPositionIndex == 0)
beginReplayPositions();
prefetchReplayPositions();
meshtastic_MeshPacket pkt = {};
bool havePkt = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
if (!replayQueue.empty()) {
pkt = replayQueue.front();
replayQueue.pop_front();
havePkt = true;
}
}
if (havePkt) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = pkt;
} else {
LOG_DEBUG("Done replaying positions count=%u millis=%u", (unsigned)replayPositionIndex, millis());
state = STATE_REPLAY_TELEMETRY;
return getFromRadio(buf);
}
break;
}
case STATE_REPLAY_TELEMETRY: {
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
beginReplayTelemetry();
prefetchReplayTelemetry();
meshtastic_MeshPacket pkt = {};
bool havePkt = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
if (!replayQueue.empty()) {
pkt = replayQueue.front();
replayQueue.pop_front();
havePkt = true;
}
}
if (havePkt) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = pkt;
} else {
LOG_DEBUG("Done replaying telemetry count=%u millis=%u", (unsigned)replayTelemetryIndex, millis());
state = STATE_REPLAY_ENVIRONMENT;
return getFromRadio(buf);
}
break;
}
case STATE_REPLAY_ENVIRONMENT: {
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
beginReplayEnvironment();
prefetchReplayEnvironment();
meshtastic_MeshPacket pkt = {};
bool havePkt = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
if (!replayQueue.empty()) {
pkt = replayQueue.front();
replayQueue.pop_front();
havePkt = true;
}
}
if (havePkt) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = pkt;
} else {
LOG_DEBUG("Done replaying environment count=%u millis=%u", (unsigned)replayEnvironmentIndex, millis());
state = STATE_REPLAY_STATUS;
return getFromRadio(buf);
}
break;
}
case STATE_REPLAY_STATUS: {
if (replayStatusOrder.empty() && replayStatusIndex == 0)
beginReplayStatus();
prefetchReplayStatus();
meshtastic_MeshPacket pkt = {};
bool havePkt = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
if (!replayQueue.empty()) {
pkt = replayQueue.front();
replayQueue.pop_front();
havePkt = true;
}
}
if (havePkt) {
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
fromRadioScratch.packet = pkt;
} else {
LOG_DEBUG("Done replaying status count=%u millis=%u", (unsigned)replayStatusIndex, millis());
replayPositionOrder.clear();
replayPositionOrder.shrink_to_fit();
replayTelemetryOrder.clear();
replayTelemetryOrder.shrink_to_fit();
replayEnvironmentOrder.clear();
replayEnvironmentOrder.shrink_to_fit();
replayStatusOrder.clear();
replayStatusOrder.shrink_to_fit();
state = STATE_SEND_FILEMANIFEST; state = STATE_SEND_FILEMANIFEST;
// Go ahead and send that ID right now
return getFromRadio(buf); return getFromRadio(buf);
} }
break; break;
@@ -538,9 +672,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
case STATE_SEND_FILEMANIFEST: { case STATE_SEND_FILEMANIFEST: {
LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST"); LOG_DEBUG("FromRadio=STATE_SEND_FILEMANIFEST");
// last element // ONLY_NODES variants skip the manifest.
if (config_state == filesManifest.size() || if (config_state == filesManifest.size() || config_nonce == SPECIAL_NONCE_ONLY_NODES ||
config_nonce == SPECIAL_NONCE_ONLY_NODES) { // also handles an empty filesManifest config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES) {
config_state = 0; config_state = 0;
filesManifest.clear(); filesManifest.clear();
// Skip to complete packet // Skip to complete packet
@@ -653,6 +787,7 @@ void PhoneAPI::prefetchNodeInfos()
{ {
bool added = false; bool added = false;
bool wasEmpty = false; bool wasEmpty = false;
const bool gradient = clientWantsGradientSync();
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
{ {
concurrency::LockGuard guard(&nodeInfoMutex); concurrency::LockGuard guard(&nodeInfoMutex);
@@ -662,7 +797,8 @@ void PhoneAPI::prefetchNodeInfos()
if (!nextNode) if (!nextNode)
break; break;
auto info = TypeConversions::ConvertToNodeInfo(nextNode); auto info =
gradient ? TypeConversions::ConvertToNodeInfoThin(nextNode) : TypeConversions::ConvertToNodeInfo(nextNode);
bool isUs = info.num == nodeDB->getNodeNum(); bool isUs = info.num == nodeDB->getNodeNum();
info.hops_away = isUs ? 0 : info.hops_away; info.hops_away = isUs ? 0 : info.hops_away;
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;
@@ -682,6 +818,257 @@ void PhoneAPI::prefetchNodeInfos()
onNowHasData(0); onNowHasData(0);
} }
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
{
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
pkt.from = num;
pkt.to = nodeDB->getNodeNum();
pkt.id = generatePacketId();
pkt.rx_time = pos.time;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
pkt.decoded.portnum = meshtastic_PortNum_POSITION_APP;
meshtastic_Position fullPos = TypeConversions::ConvertToPosition(pos);
size_t len =
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Position_msg, &fullPos);
pkt.decoded.payload.size = (pb_size_t)len;
return pkt;
}
meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const meshtastic_DeviceMetrics &metrics)
{
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
pkt.from = num;
pkt.to = nodeDB->getNodeNum();
pkt.id = generatePacketId();
// No native timestamp on telemetry packets here; use last_heard.
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
fullTel.time = pkt.rx_time;
fullTel.which_variant = meshtastic_Telemetry_device_metrics_tag;
fullTel.variant.device_metrics = metrics;
size_t len =
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
pkt.decoded.payload.size = (pb_size_t)len;
return pkt;
}
void PhoneAPI::beginReplayPositions()
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
// Build excluded entirely - leave the order list empty so the state arm
// immediately drains and advances.
replayPositionOrder.clear();
replayPositionIndex = 0;
#else
if (!clientWantsGradientSync()) {
replayPositionOrder.clear();
replayPositionIndex = 0;
return;
}
// Snapshot the keyset at phase start so concurrent inserts/erases on the
// map don't invalidate iteration. Skip our own node - the phone already
// got our position bundled in STATE_SEND_OWN_NODEINFO.
replayPositionOrder = nodeDB->snapshotPositionNodeNums(nodeDB->getNodeNum());
replayPositionIndex = 0;
LOG_INFO("Begin position replay: %u entries millis=%u", (unsigned)replayPositionOrder.size(), millis());
#endif
}
void PhoneAPI::prefetchReplayPositions()
{
#if MESHTASTIC_EXCLUDE_POSITIONDB
return;
#else
if (!clientWantsGradientSync())
return;
bool added = false;
bool wasEmpty = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = replayQueue.empty();
while (replayQueue.size() < kReplayPrefetchDepth && replayPositionIndex < replayPositionOrder.size()) {
NodeNum num = replayPositionOrder[replayPositionIndex++];
meshtastic_PositionLite pos;
if (!nodeDB->copyNodePosition(num, pos))
continue; // entry was evicted between snapshot and now
replayQueue.push_back(makeReplayPositionPacket(num, pos));
added = true;
}
}
if (added && wasEmpty)
onNowHasData(0);
#endif
}
void PhoneAPI::beginReplayTelemetry()
{
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
replayTelemetryOrder.clear();
replayTelemetryIndex = 0;
#else
if (!clientWantsGradientSync()) {
replayTelemetryOrder.clear();
replayTelemetryIndex = 0;
return;
}
replayTelemetryOrder = nodeDB->snapshotTelemetryNodeNums(nodeDB->getNodeNum());
replayTelemetryIndex = 0;
LOG_INFO("Begin telemetry replay: %u entries millis=%u", (unsigned)replayTelemetryOrder.size(), millis());
#endif
}
void PhoneAPI::prefetchReplayTelemetry()
{
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
return;
#else
if (!clientWantsGradientSync())
return;
bool added = false;
bool wasEmpty = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = replayQueue.empty();
while (replayQueue.size() < kReplayPrefetchDepth && replayTelemetryIndex < replayTelemetryOrder.size()) {
NodeNum num = replayTelemetryOrder[replayTelemetryIndex++];
meshtastic_DeviceMetrics dm;
if (!nodeDB->copyNodeTelemetry(num, dm))
continue;
replayQueue.push_back(makeReplayTelemetryPacket(num, dm));
added = true;
}
}
if (added && wasEmpty)
onNowHasData(0);
#endif
}
meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env)
{
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
pkt.from = num;
pkt.to = nodeDB->getNodeNum();
pkt.id = generatePacketId();
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
pkt.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP;
meshtastic_Telemetry fullTel = meshtastic_Telemetry_init_default;
fullTel.time = pkt.rx_time;
fullTel.which_variant = meshtastic_Telemetry_environment_metrics_tag;
fullTel.variant.environment_metrics = env;
size_t len =
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_Telemetry_msg, &fullTel);
pkt.decoded.payload.size = (pb_size_t)len;
return pkt;
}
void PhoneAPI::beginReplayEnvironment()
{
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
replayEnvironmentOrder.clear();
replayEnvironmentIndex = 0;
#else
if (!clientWantsGradientSync()) {
replayEnvironmentOrder.clear();
replayEnvironmentIndex = 0;
return;
}
replayEnvironmentOrder = nodeDB->snapshotEnvironmentNodeNums(nodeDB->getNodeNum());
replayEnvironmentIndex = 0;
LOG_INFO("Begin environment replay: %u entries millis=%u", (unsigned)replayEnvironmentOrder.size(), millis());
#endif
}
void PhoneAPI::prefetchReplayEnvironment()
{
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
return;
#else
if (!clientWantsGradientSync())
return;
bool added = false;
bool wasEmpty = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = replayQueue.empty();
while (replayQueue.size() < kReplayPrefetchDepth && replayEnvironmentIndex < replayEnvironmentOrder.size()) {
NodeNum num = replayEnvironmentOrder[replayEnvironmentIndex++];
meshtastic_EnvironmentMetrics env;
if (!nodeDB->copyNodeEnvironment(num, env))
continue;
replayQueue.push_back(makeReplayEnvironmentPacket(num, env));
added = true;
}
}
if (added && wasEmpty)
onNowHasData(0);
#endif
}
meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status)
{
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default;
pkt.from = num;
pkt.to = nodeDB->getNodeNum();
pkt.id = generatePacketId();
// StatusMessage has no native timestamp; use last_heard.
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.rx_time = header ? header->last_heard : 0;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
size_t len =
pb_encode_to_bytes(pkt.decoded.payload.bytes, sizeof(pkt.decoded.payload.bytes), &meshtastic_StatusMessage_msg, &status);
pkt.decoded.payload.size = (pb_size_t)len;
return pkt;
}
void PhoneAPI::beginReplayStatus()
{
#if MESHTASTIC_EXCLUDE_STATUSDB
replayStatusOrder.clear();
replayStatusIndex = 0;
#else
if (!clientWantsGradientSync()) {
replayStatusOrder.clear();
replayStatusIndex = 0;
return;
}
replayStatusOrder = nodeDB->snapshotStatusNodeNums(nodeDB->getNodeNum());
replayStatusIndex = 0;
LOG_INFO("Begin status replay: %u entries millis=%u", (unsigned)replayStatusOrder.size(), millis());
#endif
}
void PhoneAPI::prefetchReplayStatus()
{
#if MESHTASTIC_EXCLUDE_STATUSDB
return;
#else
if (!clientWantsGradientSync())
return;
bool added = false;
bool wasEmpty = false;
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = replayQueue.empty();
while (replayQueue.size() < kReplayPrefetchDepth && replayStatusIndex < replayStatusOrder.size()) {
NodeNum num = replayStatusOrder[replayStatusIndex++];
meshtastic_StatusMessage status;
if (!nodeDB->copyNodeStatus(num, status) || status.status[0] == '\0')
continue;
replayQueue.push_back(makeReplayStatusPacket(num, status));
added = true;
}
}
if (added && wasEmpty)
onNowHasData(0);
#endif
}
void PhoneAPI::releaseMqttClientProxyPhonePacket() void PhoneAPI::releaseMqttClientProxyPhonePacket()
{ {
if (mqttClientProxyMessageForPhone) { if (mqttClientProxyMessageForPhone) {
@@ -728,6 +1115,31 @@ bool PhoneAPI::available()
PREFETCH_NODEINFO: PREFETCH_NODEINFO:
prefetchNodeInfos(); prefetchNodeInfos();
return true; return true;
case STATE_REPLAY_POSITIONS: {
// Prime the iterator if we haven't yet, then top up the queue.
if (replayPositionOrder.empty() && replayPositionIndex == 0)
beginReplayPositions();
prefetchReplayPositions();
return true; // Always advance state machine; arm itself transitions when drained
}
case STATE_REPLAY_TELEMETRY: {
if (replayTelemetryOrder.empty() && replayTelemetryIndex == 0)
beginReplayTelemetry();
prefetchReplayTelemetry();
return true;
}
case STATE_REPLAY_ENVIRONMENT: {
if (replayEnvironmentOrder.empty() && replayEnvironmentIndex == 0)
beginReplayEnvironment();
prefetchReplayEnvironment();
return true;
}
case STATE_REPLAY_STATUS: {
if (replayStatusOrder.empty() && replayStatusIndex == 0)
beginReplayStatus();
prefetchReplayStatus();
return true;
}
case STATE_SEND_PACKETS: { case STATE_SEND_PACKETS: {
if (!queueStatusPacketForPhone) if (!queueStatusPacketForPhone)
queueStatusPacketForPhone = service->getQueueStatusForPhone(); queueStatusPacketForPhone = service->getQueueStatusForPhone();
+44 -1
View File
@@ -4,12 +4,16 @@
#include "concurrency/Lock.h" #include "concurrency/Lock.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshtastic/portnums.pb.h" #include "meshtastic/portnums.pb.h"
#include <cstdint>
#include <deque> #include <deque>
#include <iterator> #include <iterator>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
// NodeNum stored as raw uint32_t below; including MeshTypes.h here breaks the
// LOG_WARN include order via Arduino.h/MemoryPool.h.
// Make sure that we never let our packets grow too large for one BLE packet // Make sure that we never let our packets grow too large for one BLE packet
#define MAX_TO_FROM_RADIO_SIZE 512 #define MAX_TO_FROM_RADIO_SIZE 512
@@ -22,6 +26,9 @@
#define SPECIAL_NONCE_ONLY_CONFIG 69420 #define SPECIAL_NONCE_ONLY_CONFIG 69420
#define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°) #define SPECIAL_NONCE_ONLY_NODES 69421 // ( ͡° ͜ʖ ͡°)
// Gradient sync: phone sends one of these to opt into thin-header + replay.
#define SPECIAL_NONCE_GRADIENT_SYNC 69422
#define SPECIAL_NONCE_GRADIENT_ONLY_NODES 69423
/** /**
* Provides our protobuf based API which phone/PC clients can use to talk to our device * Provides our protobuf based API which phone/PC clients can use to talk to our device
@@ -45,7 +52,13 @@ class PhoneAPI
STATE_SEND_CONFIG, // Replacement for the old Radioconfig STATE_SEND_CONFIG, // Replacement for the old Radioconfig
STATE_SEND_MODULECONFIG, // Send Module specific config STATE_SEND_MODULECONFIG, // Send Module specific config
STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client STATE_SEND_OTHER_NODEINFOS, // states progress in this order as the device sends to to the client
STATE_SEND_FILEMANIFEST, // Send file manifest // Drain satellite DBs as synthetic POSITION_APP / TELEMETRY_APP /
// NODE_STATUS_APP packets when the phone opted into gradient sync.
STATE_REPLAY_POSITIONS,
STATE_REPLAY_TELEMETRY,
STATE_REPLAY_ENVIRONMENT,
STATE_REPLAY_STATUS,
STATE_SEND_FILEMANIFEST, // Send file manifest
STATE_SEND_COMPLETE_ID, STATE_SEND_COMPLETE_ID,
STATE_SEND_PACKETS // send packets or debug strings STATE_SEND_PACKETS // send packets or debug strings
}; };
@@ -88,6 +101,20 @@ class PhoneAPI
// Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task.
concurrency::Lock nodeInfoMutex; concurrency::Lock nodeInfoMutex;
// Synthetic-packet replay queue (paced via prefetch).
std::deque<meshtastic_MeshPacket> replayQueue;
static constexpr size_t kReplayPrefetchDepth = 4;
// NodeNum snapshots taken at each replay phase start so concurrent
// satellite-map mutations don't invalidate iteration.
std::vector<uint32_t> replayPositionOrder;
std::vector<uint32_t> replayTelemetryOrder;
std::vector<uint32_t> replayEnvironmentOrder;
std::vector<uint32_t> replayStatusOrder;
size_t replayPositionIndex = 0;
size_t replayTelemetryIndex = 0;
size_t replayEnvironmentIndex = 0;
size_t replayStatusIndex = 0;
meshtastic_ToRadio toRadioScratch = { meshtastic_ToRadio toRadioScratch = {
0}; // this is a static scratch object, any data must be copied elsewhere before returning 0}; // this is a static scratch object, any data must be copied elsewhere before returning
@@ -137,6 +164,10 @@ class PhoneAPI
bool isConnected() { return state != STATE_SEND_NOTHING; } bool isConnected() { return state != STATE_SEND_NOTHING; }
bool isSendingPackets() { return state == STATE_SEND_PACKETS; } bool isSendingPackets() { return state == STATE_SEND_PACKETS; }
bool clientWantsGradientSync() const
{
return config_nonce == SPECIAL_NONCE_GRADIENT_SYNC || config_nonce == SPECIAL_NONCE_GRADIENT_ONLY_NODES;
}
protected: protected:
/// Our fromradio packet while it is being assembled /// Our fromradio packet while it is being assembled
@@ -185,6 +216,18 @@ class PhoneAPI
void releaseQueueStatusPhonePacket(); void releaseQueueStatusPhonePacket();
void prefetchNodeInfos(); void prefetchNodeInfos();
void beginReplayPositions();
void prefetchReplayPositions();
void beginReplayTelemetry();
void prefetchReplayTelemetry();
void beginReplayEnvironment();
void prefetchReplayEnvironment();
void beginReplayStatus();
void prefetchReplayStatus();
meshtastic_MeshPacket makeReplayPositionPacket(uint32_t num, const meshtastic_PositionLite &pos);
meshtastic_MeshPacket makeReplayTelemetryPacket(uint32_t num, const meshtastic_DeviceMetrics &metrics);
meshtastic_MeshPacket makeReplayEnvironmentPacket(uint32_t num, const meshtastic_EnvironmentMetrics &env);
meshtastic_MeshPacket makeReplayStatusPacket(uint32_t num, const meshtastic_StatusMessage &status);
void releaseMqttClientProxyPhonePacket(); void releaseMqttClientProxyPhonePacket();
+2 -2
View File
@@ -58,7 +58,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
const char *getSenderShortName(const meshtastic_MeshPacket &mp) const char *getSenderShortName(const meshtastic_MeshPacket &mp)
{ {
auto node = nodeDB->getMeshNode(getFrom(&mp)); auto node = nodeDB->getMeshNode(getFrom(&mp));
const char *sender = (node) ? node->user.short_name : "???"; const char *sender = (node && nodeInfoLiteHasUser(node) && node->short_name[0]) ? node->short_name : "???";
return sender; return sender;
} }
@@ -121,4 +121,4 @@ template <class T> class ProtobufModule : protected SinglePortModule
return alterReceivedProtobuf(mp, decoded); return alterReceivedProtobuf(mp, decoded);
} }
} }
}; };
+1 -1
View File
@@ -252,7 +252,7 @@ bool RF95Interface::reconfigure()
startReceive(); // restart receiving startReceive(); // restart receiving
return RADIOLIB_ERR_NONE; return true;
} }
/** /**
+15
View File
@@ -5,6 +5,7 @@
#include "LR1110Interface.h" #include "LR1110Interface.h"
#include "LR1120Interface.h" #include "LR1120Interface.h"
#include "LR1121Interface.h" #include "LR1121Interface.h"
#include "LR2021Interface.h"
#include "MeshRadio.h" #include "MeshRadio.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h" #include "NodeDB.h"
@@ -478,6 +479,20 @@ std::unique_ptr<RadioInterface> initLoRa()
} }
#endif #endif
#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1
if (!rIf) {
rIf = std::unique_ptr<LR2021Interface>(
new LR2021Interface(loraHal, LR2021_SPI_NSS_PIN, LR2021_IRQ_PIN, LR2021_NRESET_PIN, LR2021_BUSY_PIN));
if (!rIf->init()) {
LOG_WARN("No LR2021 radio");
rIf = nullptr;
} else {
LOG_INFO("LR2021 init success");
radioType = LR2021_RADIO;
}
}
#endif
#if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1
if (!rIf) { if (!rIf) {
rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY)); rIf = std::unique_ptr<SX1280Interface>(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY));
+1 -1
View File
@@ -115,7 +115,7 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
} }
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->public_key.size == 0)) {
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
routingModule->getHopLimitForResponse(*p)); routingModule->getHopLimitForResponse(*p));
+16 -16
View File
@@ -120,17 +120,17 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
if (!node) if (!node)
continue; continue;
// Check 1: is_favorite (cheapest - single bool) // Check 1: is_favorite (cheapest - single bit test)
if (!node->is_favorite) if (!nodeInfoLiteIsFavorite(node))
continue; continue;
// Check 2: has_user (cheap - single bool) // Check 2: has_user (cheap - single bit test)
if (!node->has_user) if (!nodeInfoLiteHasUser(node))
continue; continue;
// Check 3: role check (moderate cost - multiple comparisons) // Check 3: role check (moderate cost - multiple comparisons)
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
continue; continue;
} }
@@ -433,7 +433,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
concurrency::LockGuard g(cryptLock); concurrency::LockGuard g(cryptLock);
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY && if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
(nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) { !nodeInfoLiteHasUser(nodeDB->getMeshNode(p->from))) {
LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from); LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
return DecodeState::DECODE_FAILURE; return DecodeState::DECODE_FAILURE;
} }
@@ -451,11 +451,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
#if !(MESHTASTIC_EXCLUDE_PKI) #if !(MESHTASTIC_EXCLUDE_PKI)
// Attempt PKI decryption first // Attempt PKI decryption first
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr && if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 && nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
rawSize > MESHTASTIC_PKC_OVERHEAD) { nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
LOG_DEBUG("Attempt PKI decryption"); LOG_DEBUG("Attempt PKI decryption");
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes, if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
bytes)) { bytes)) {
LOG_INFO("PKI Decryption worked!"); LOG_INFO("PKI Decryption worked!");
@@ -467,7 +467,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
decrypted = true; decrypted = true;
LOG_INFO("Packet decrypted using PKI!"); LOG_INFO("Packet decrypted using PKI!");
p->pki_encrypted = true; p->pki_encrypted = true;
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32); memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
p->public_key.size = 32; p->public_key.size = 32;
p->decoded = decodedtmp; p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
@@ -665,18 +665,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN) if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE; return meshtastic_Routing_Error_TOO_LARGE;
// Check for a known public key for the destination // Check for a known public key for the destination
if (node == nullptr || node->user.public_key.size != 32) { if (node == nullptr || node->public_key.size != 32) {
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to, LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
p->decoded.portnum); p->decoded.portnum);
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY; return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
} }
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
memcmp(p->public_key.bytes, node->user.public_key.bytes, 32) != 0) { memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) {
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes, LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
*node->user.public_key.bytes); *node->public_key.bytes);
return meshtastic_Routing_Error_PKI_FAILED; return meshtastic_Routing_Error_PKI_FAILED;
} }
crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes); crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
numbytes += MESHTASTIC_PKC_OVERHEAD; numbytes += MESHTASTIC_PKC_OVERHEAD;
p->channel = 0; p->channel = 0;
p->pki_encrypted = true; p->pki_encrypted = true;
@@ -860,7 +860,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
} }
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from); meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
if (node != NULL && node->is_ignored) { if (nodeInfoLiteIsIgnored(node)) {
LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from); LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
packetPool.release(p); packetPool.release(p);
return; return;
+1 -1
View File
@@ -262,7 +262,7 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
startReceive(); // restart receiving startReceive(); // restart receiving
return RADIOLIB_ERR_NONE; return true;
} }
template <typename T> void SX126xInterface<T>::disableInterrupt() template <typename T> void SX126xInterface<T>::disableInterrupt()
+1 -1
View File
@@ -153,7 +153,7 @@ template <typename T> bool SX128xInterface<T>::reconfigure()
startReceive(); // restart receiving startReceive(); // restart receiving
return RADIOLIB_ERR_NONE; return true;
} }
template <typename T> void SX128xInterface<T>::disableInterrupt() template <typename T> void SX128xInterface<T>::disableInterrupt()
+69 -49
View File
@@ -3,7 +3,9 @@
#include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h"
#include "meshUtils.h" #include "meshUtils.h"
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite) meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite,
const meshtastic_PositionLite *position,
const meshtastic_DeviceMetrics *deviceMetrics)
{ {
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default; meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
@@ -11,42 +13,56 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
info.snr = lite->snr; info.snr = lite->snr;
info.last_heard = lite->last_heard; info.last_heard = lite->last_heard;
info.channel = lite->channel; info.channel = lite->channel;
info.via_mqtt = lite->via_mqtt; info.via_mqtt = nodeInfoLiteViaMqtt(lite);
info.is_favorite = lite->is_favorite; info.is_favorite = nodeInfoLiteIsFavorite(lite);
info.is_ignored = lite->is_ignored; info.is_ignored = nodeInfoLiteIsIgnored(lite);
info.is_key_manually_verified = lite->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; info.is_key_manually_verified = nodeInfoLiteIsKeyManuallyVerified(lite);
info.is_muted = lite->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK; info.is_muted = nodeInfoLiteIsMuted(lite);
if (lite->has_hops_away) { if (lite->has_hops_away) {
info.has_hops_away = true; info.has_hops_away = true;
info.hops_away = lite->hops_away; info.hops_away = lite->hops_away;
} }
if (lite->has_position) { if (position) {
info.has_position = true; info.has_position = true;
if (lite->position.latitude_i != 0) if (position->latitude_i != 0)
info.position.has_latitude_i = true; info.position.has_latitude_i = true;
info.position.latitude_i = lite->position.latitude_i; info.position.latitude_i = position->latitude_i;
if (lite->position.longitude_i != 0) if (position->longitude_i != 0)
info.position.has_longitude_i = true; info.position.has_longitude_i = true;
info.position.longitude_i = lite->position.longitude_i; info.position.longitude_i = position->longitude_i;
if (lite->position.altitude != 0) if (position->altitude != 0)
info.position.has_altitude = true; info.position.has_altitude = true;
info.position.altitude = lite->position.altitude; info.position.altitude = position->altitude;
info.position.location_source = lite->position.location_source; info.position.location_source = position->location_source;
info.position.time = lite->position.time; info.position.time = position->time;
} }
if (lite->has_user) { if (nodeInfoLiteHasUser(lite)) {
info.has_user = true; info.has_user = true;
info.user = ConvertToUser(lite->num, lite->user); info.user = ConvertToUser(lite);
} }
if (lite->has_device_metrics) { if (deviceMetrics) {
info.has_device_metrics = true; info.has_device_metrics = true;
info.device_metrics = lite->device_metrics; info.device_metrics = *deviceMetrics;
} }
return info; return info;
} }
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
{
meshtastic_PositionLite posScratch;
meshtastic_DeviceMetrics dmScratch;
const meshtastic_PositionLite *pos = (nodeDB && nodeDB->copyNodePosition(lite->num, posScratch)) ? &posScratch : nullptr;
const meshtastic_DeviceMetrics *dm = (nodeDB && nodeDB->copyNodeTelemetry(lite->num, dmScratch)) ? &dmScratch : nullptr;
return ConvertToNodeInfo(lite, pos, dm);
}
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite)
{
return ConvertToNodeInfo(lite, nullptr, nullptr);
}
meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position) meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position)
{ {
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default; meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
@@ -77,46 +93,50 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
return position; return position;
} }
meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) void TypeConversions::CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user)
{ {
meshtastic_UserLite lite = meshtastic_UserLite_init_default; if (!lite)
return;
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name)); strncpy(lite->long_name, user.long_name, sizeof(lite->long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0'; lite->long_name[sizeof(lite->long_name) - 1] = '\0';
sanitizeUtf8(lite.long_name, sizeof(lite.long_name)); sanitizeUtf8(lite->long_name, sizeof(lite->long_name));
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name)); strncpy(lite->short_name, user.short_name, sizeof(lite->short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0'; lite->short_name[sizeof(lite->short_name) - 1] = '\0';
sanitizeUtf8(lite.short_name, sizeof(lite.short_name)); sanitizeUtf8(lite->short_name, sizeof(lite->short_name));
lite.hw_model = user.hw_model; lite->hw_model = user.hw_model;
lite.role = user.role; lite->role = user.role;
lite.is_licensed = user.is_licensed; memcpy(lite->public_key.bytes, user.public_key.bytes, sizeof(lite->public_key.bytes));
memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); lite->public_key.size = user.public_key.size;
memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes));
lite.public_key.size = user.public_key.size; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_LICENSED_MASK, user.is_licensed);
lite.has_is_unmessagable = user.has_is_unmessagable; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable);
lite.is_unmessagable = user.is_unmessagable; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK, user.has_is_unmessagable && user.is_unmessagable);
return lite; nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_USER_MASK, true);
} }
meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite) meshtastic_User TypeConversions::ConvertToUser(const meshtastic_NodeInfoLite *lite)
{ {
meshtastic_User user = meshtastic_User_init_default; meshtastic_User user = meshtastic_User_init_default;
if (!lite)
return user;
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum); snprintf(user.id, sizeof(user.id), "!%08x", lite->num);
strncpy(user.long_name, lite.long_name, sizeof(user.long_name)); strncpy(user.long_name, lite->long_name, sizeof(user.long_name));
user.long_name[sizeof(user.long_name) - 1] = '\0'; user.long_name[sizeof(user.long_name) - 1] = '\0';
sanitizeUtf8(user.long_name, sizeof(user.long_name)); sanitizeUtf8(user.long_name, sizeof(user.long_name));
strncpy(user.short_name, lite.short_name, sizeof(user.short_name)); strncpy(user.short_name, lite->short_name, sizeof(user.short_name));
user.short_name[sizeof(user.short_name) - 1] = '\0'; user.short_name[sizeof(user.short_name) - 1] = '\0';
sanitizeUtf8(user.short_name, sizeof(user.short_name)); sanitizeUtf8(user.short_name, sizeof(user.short_name));
user.hw_model = lite.hw_model; user.hw_model = lite->hw_model;
user.role = lite.role; user.role = lite->role;
user.is_licensed = lite.is_licensed; user.is_licensed = nodeInfoLiteIsLicensed(lite);
memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); memcpy(user.public_key.bytes, lite->public_key.bytes, sizeof(user.public_key.bytes));
memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); user.public_key.size = lite->public_key.size;
user.public_key.size = lite.public_key.size; user.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(lite);
user.has_is_unmessagable = lite.has_is_unmessagable; user.is_unmessagable = nodeInfoLiteIsUnmessagable(lite);
user.is_unmessagable = lite.is_unmessagable; // macaddr is gone from the slim header; zero-fill for old clients that read it.
memset(user.macaddr, 0, sizeof(user.macaddr));
return user; return user;
} }
+9 -2
View File
@@ -7,9 +7,16 @@
class TypeConversions class TypeConversions
{ {
public: public:
// Either pointer may be null; the corresponding has_* fields stay unset.
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite, const meshtastic_PositionLite *position,
const meshtastic_DeviceMetrics *deviceMetrics);
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite); static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite);
// Identity + link-state only; satellite payloads are replayed afterward.
static meshtastic_NodeInfo ConvertToNodeInfoThin(const meshtastic_NodeInfoLite *lite);
static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position); static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position);
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite); static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite);
static meshtastic_UserLite ConvertToUserLite(meshtastic_User user);
static meshtastic_User ConvertToUser(uint32_t nodeNum, meshtastic_UserLite lite); static void CopyUserToNodeInfoLite(meshtastic_NodeInfoLite *lite, const meshtastic_User &user);
static meshtastic_User ConvertToUser(const meshtastic_NodeInfoLite *lite);
}; };
@@ -18,6 +18,18 @@ PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO)
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2) PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2)
PB_BIND(meshtastic_NodePositionEntry, meshtastic_NodePositionEntry, AUTO)
PB_BIND(meshtastic_NodeTelemetryEntry, meshtastic_NodeTelemetryEntry, AUTO)
PB_BIND(meshtastic_NodeEnvironmentEntry, meshtastic_NodeEnvironmentEntry, AUTO)
PB_BIND(meshtastic_NodeStatusEntry, meshtastic_NodeStatusEntry, AUTO)
PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO) PB_BIND(meshtastic_NodeDatabase, meshtastic_NodeDatabase, AUTO)
+128 -43
View File
@@ -63,43 +63,35 @@ typedef struct _meshtastic_UserLite {
bool is_unmessagable; bool is_unmessagable;
} meshtastic_UserLite; } meshtastic_UserLite;
typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeInfoLite_public_key_t;
typedef struct _meshtastic_NodeInfoLite { typedef struct _meshtastic_NodeInfoLite {
/* The node number */ /* The node number */
uint32_t num; uint32_t num;
/* The user info for this node */
bool has_user;
meshtastic_UserLite user;
/* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
Position.time now indicates the last time we received a POSITION from that node. */
bool has_position;
meshtastic_PositionLite position;
/* Returns the Signal-to-noise ratio (SNR) of the last received message, /* Returns the Signal-to-noise ratio (SNR) of the last received message,
as measured by the receiver. Return SNR of the last received message in dB */ as measured by the receiver. Return SNR of the last received message in dB */
float snr; float snr;
/* Set to indicate the last time we received a packet from this node */ /* Set to indicate the last time we received a packet from this node */
uint32_t last_heard; uint32_t last_heard;
/* The latest device metrics for the node. */
bool has_device_metrics;
meshtastic_DeviceMetrics device_metrics;
/* local channel index we heard that node on. Only populated if its not the default channel. */ /* local channel index we heard that node on. Only populated if its not the default channel. */
uint8_t channel; uint8_t channel;
/* True if we witnessed the node over MQTT instead of LoRA transport */
bool via_mqtt;
/* Number of hops away from us this node is (0 if direct neighbor) */ /* Number of hops away from us this node is (0 if direct neighbor) */
bool has_hops_away; bool has_hops_away;
uint8_t hops_away; uint8_t hops_away;
/* True if node is in our favorites list
Persists between NodeDB internal clean ups */
bool is_favorite;
/* True if node is in our ignored list
Persists between NodeDB internal clean ups */
bool is_ignored;
/* Last byte of the node number of the node that should be used as the next hop to reach this node. */ /* Last byte of the node number of the node that should be used as the next hop to reach this node. */
uint8_t next_hop; uint8_t next_hop;
/* Bitfield for storing booleans. /* Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h. */
LSB 0 is_key_manually_verified
LSB 1 is_muted */
uint32_t bitfield; uint32_t bitfield;
/* A full name for this user, i.e. "Kevin Hester". */
char long_name[25];
/* A VERY short name, ideally two characters or an emoji.
Suitable for a tiny OLED screen. */
char short_name[5];
/* Hardware model the user's device is running. */
meshtastic_HardwareModel hw_model;
/* The user's role in the mesh. */
meshtastic_Config_DeviceConfig_Role role;
/* The public key of the user's device, for PKI-based encrypted DMs. */
meshtastic_NodeInfoLite_public_key_t public_key;
} meshtastic_NodeInfoLite; } meshtastic_NodeInfoLite;
/* This message is never sent over the wire, but it is used for serializing DB /* This message is never sent over the wire, but it is used for serializing DB
@@ -143,6 +135,30 @@ typedef struct _meshtastic_DeviceState {
meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12]; meshtastic_NodeRemoteHardwarePin node_remote_hardware_pins[12];
} meshtastic_DeviceState; } meshtastic_DeviceState;
typedef struct _meshtastic_NodePositionEntry {
uint32_t num;
bool has_position;
meshtastic_PositionLite position;
} meshtastic_NodePositionEntry;
typedef struct _meshtastic_NodeTelemetryEntry {
uint32_t num;
bool has_device_metrics;
meshtastic_DeviceMetrics device_metrics;
} meshtastic_NodeTelemetryEntry;
typedef struct _meshtastic_NodeEnvironmentEntry {
uint32_t num;
bool has_environment_metrics;
meshtastic_EnvironmentMetrics environment_metrics;
} meshtastic_NodeEnvironmentEntry;
typedef struct _meshtastic_NodeStatusEntry {
uint32_t num;
bool has_status;
meshtastic_StatusMessage status;
} meshtastic_NodeStatusEntry;
typedef struct _meshtastic_NodeDatabase { typedef struct _meshtastic_NodeDatabase {
/* A version integer used to invalidate old save files when we make /* A version integer used to invalidate old save files when we make
incompatible changes This integer is set at build time and is private to incompatible changes This integer is set at build time and is private to
@@ -150,6 +166,12 @@ typedef struct _meshtastic_NodeDatabase {
uint32_t version; uint32_t version;
/* New lite version of NodeDB to decrease memory footprint */ /* New lite version of NodeDB to decrease memory footprint */
std::vector<meshtastic_NodeInfoLite> nodes; std::vector<meshtastic_NodeInfoLite> nodes;
/* Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
these via MESHTASTIC_EXCLUDE_*DB build flags. */
std::vector<meshtastic_NodePositionEntry> positions;
std::vector<meshtastic_NodeTelemetryEntry> telemetry;
std::vector<meshtastic_NodeStatusEntry> status;
std::vector<meshtastic_NodeEnvironmentEntry> environment;
} meshtastic_NodeDatabase; } meshtastic_NodeDatabase;
/* The on-disk saved channels */ /* The on-disk saved channels */
@@ -191,16 +213,24 @@ extern "C" {
/* Initializer values for message structs */ /* Initializer values for message structs */
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0} #define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_NodeInfoLite_init_default {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_NodeDatabase_init_default {0, {0}} #define meshtastic_NodePositionEntry_init_default {0, false, meshtastic_PositionLite_init_default}
#define meshtastic_NodeTelemetryEntry_init_default {0, false, meshtastic_DeviceMetrics_init_default}
#define meshtastic_NodeEnvironmentEntry_init_default {0, false, meshtastic_EnvironmentMetrics_init_default}
#define meshtastic_NodeStatusEntry_init_default {0, false, meshtastic_StatusMessage_init_default}
#define meshtastic_NodeDatabase_init_default {0, {0}, {0}, {0}, {0}, {0}}
#define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0} #define meshtastic_ChannelFile_init_default {0, {meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default, meshtastic_Channel_init_default}, 0}
#define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default} #define meshtastic_BackupPreferences_init_default {0, 0, false, meshtastic_LocalConfig_init_default, false, meshtastic_LocalModuleConfig_init_default, false, meshtastic_ChannelFile_init_default, false, meshtastic_User_init_default}
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN} #define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0} #define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0} #define meshtastic_NodeInfoLite_init_zero {0, 0, 0, 0, false, 0, 0, 0, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}}
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_NodeDatabase_init_zero {0, {0}} #define meshtastic_NodePositionEntry_init_zero {0, false, meshtastic_PositionLite_init_zero}
#define meshtastic_NodeTelemetryEntry_init_zero {0, false, meshtastic_DeviceMetrics_init_zero}
#define meshtastic_NodeEnvironmentEntry_init_zero {0, false, meshtastic_EnvironmentMetrics_init_zero}
#define meshtastic_NodeStatusEntry_init_zero {0, false, meshtastic_StatusMessage_init_zero}
#define meshtastic_NodeDatabase_init_zero {0, {0}, {0}, {0}, {0}, {0}}
#define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0} #define meshtastic_ChannelFile_init_zero {0, {meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero, meshtastic_Channel_init_zero}, 0}
#define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero} #define meshtastic_BackupPreferences_init_zero {0, 0, false, meshtastic_LocalConfig_init_zero, false, meshtastic_LocalModuleConfig_init_zero, false, meshtastic_ChannelFile_init_zero, false, meshtastic_User_init_zero}
@@ -219,18 +249,17 @@ extern "C" {
#define meshtastic_UserLite_public_key_tag 7 #define meshtastic_UserLite_public_key_tag 7
#define meshtastic_UserLite_is_unmessagable_tag 9 #define meshtastic_UserLite_is_unmessagable_tag 9
#define meshtastic_NodeInfoLite_num_tag 1 #define meshtastic_NodeInfoLite_num_tag 1
#define meshtastic_NodeInfoLite_user_tag 2
#define meshtastic_NodeInfoLite_position_tag 3
#define meshtastic_NodeInfoLite_snr_tag 4 #define meshtastic_NodeInfoLite_snr_tag 4
#define meshtastic_NodeInfoLite_last_heard_tag 5 #define meshtastic_NodeInfoLite_last_heard_tag 5
#define meshtastic_NodeInfoLite_device_metrics_tag 6
#define meshtastic_NodeInfoLite_channel_tag 7 #define meshtastic_NodeInfoLite_channel_tag 7
#define meshtastic_NodeInfoLite_via_mqtt_tag 8
#define meshtastic_NodeInfoLite_hops_away_tag 9 #define meshtastic_NodeInfoLite_hops_away_tag 9
#define meshtastic_NodeInfoLite_is_favorite_tag 10
#define meshtastic_NodeInfoLite_is_ignored_tag 11
#define meshtastic_NodeInfoLite_next_hop_tag 12 #define meshtastic_NodeInfoLite_next_hop_tag 12
#define meshtastic_NodeInfoLite_bitfield_tag 13 #define meshtastic_NodeInfoLite_bitfield_tag 13
#define meshtastic_NodeInfoLite_long_name_tag 14
#define meshtastic_NodeInfoLite_short_name_tag 15
#define meshtastic_NodeInfoLite_hw_model_tag 16
#define meshtastic_NodeInfoLite_role_tag 17
#define meshtastic_NodeInfoLite_public_key_tag 18
#define meshtastic_DeviceState_my_node_tag 2 #define meshtastic_DeviceState_my_node_tag 2
#define meshtastic_DeviceState_owner_tag 3 #define meshtastic_DeviceState_owner_tag 3
#define meshtastic_DeviceState_receive_queue_tag 5 #define meshtastic_DeviceState_receive_queue_tag 5
@@ -240,8 +269,20 @@ extern "C" {
#define meshtastic_DeviceState_did_gps_reset_tag 11 #define meshtastic_DeviceState_did_gps_reset_tag 11
#define meshtastic_DeviceState_rx_waypoint_tag 12 #define meshtastic_DeviceState_rx_waypoint_tag 12
#define meshtastic_DeviceState_node_remote_hardware_pins_tag 13 #define meshtastic_DeviceState_node_remote_hardware_pins_tag 13
#define meshtastic_NodePositionEntry_num_tag 1
#define meshtastic_NodePositionEntry_position_tag 2
#define meshtastic_NodeTelemetryEntry_num_tag 1
#define meshtastic_NodeTelemetryEntry_device_metrics_tag 2
#define meshtastic_NodeEnvironmentEntry_num_tag 1
#define meshtastic_NodeEnvironmentEntry_environment_metrics_tag 2
#define meshtastic_NodeStatusEntry_num_tag 1
#define meshtastic_NodeStatusEntry_status_tag 2
#define meshtastic_NodeDatabase_version_tag 1 #define meshtastic_NodeDatabase_version_tag 1
#define meshtastic_NodeDatabase_nodes_tag 2 #define meshtastic_NodeDatabase_nodes_tag 2
#define meshtastic_NodeDatabase_positions_tag 3
#define meshtastic_NodeDatabase_telemetry_tag 4
#define meshtastic_NodeDatabase_status_tag 5
#define meshtastic_NodeDatabase_environment_tag 6
#define meshtastic_ChannelFile_channels_tag 1 #define meshtastic_ChannelFile_channels_tag 1
#define meshtastic_ChannelFile_version_tag 2 #define meshtastic_ChannelFile_version_tag 2
#define meshtastic_BackupPreferences_version_tag 1 #define meshtastic_BackupPreferences_version_tag 1
@@ -275,23 +316,19 @@ X(a, STATIC, OPTIONAL, BOOL, is_unmessagable, 9)
#define meshtastic_NodeInfoLite_FIELDLIST(X, a) \ #define meshtastic_NodeInfoLite_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \ X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \ X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \ X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \
X(a, STATIC, SINGULAR, UINT32, channel, 7) \ X(a, STATIC, SINGULAR, UINT32, channel, 7) \
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \ X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \ X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
X(a, STATIC, SINGULAR, UINT32, bitfield, 13) X(a, STATIC, SINGULAR, UINT32, bitfield, 13) \
X(a, STATIC, SINGULAR, STRING, long_name, 14) \
X(a, STATIC, SINGULAR, STRING, short_name, 15) \
X(a, STATIC, SINGULAR, UENUM, hw_model, 16) \
X(a, STATIC, SINGULAR, UENUM, role, 17) \
X(a, STATIC, SINGULAR, BYTES, public_key, 18)
#define meshtastic_NodeInfoLite_CALLBACK NULL #define meshtastic_NodeInfoLite_CALLBACK NULL
#define meshtastic_NodeInfoLite_DEFAULT NULL #define meshtastic_NodeInfoLite_DEFAULT NULL
#define meshtastic_NodeInfoLite_user_MSGTYPE meshtastic_UserLite
#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite
#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics
#define meshtastic_DeviceState_FIELDLIST(X, a) \ #define meshtastic_DeviceState_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \ X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \
@@ -312,13 +349,49 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 13)
#define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket #define meshtastic_DeviceState_rx_waypoint_MSGTYPE meshtastic_MeshPacket
#define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin #define meshtastic_DeviceState_node_remote_hardware_pins_MSGTYPE meshtastic_NodeRemoteHardwarePin
#define meshtastic_NodePositionEntry_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, position, 2)
#define meshtastic_NodePositionEntry_CALLBACK NULL
#define meshtastic_NodePositionEntry_DEFAULT NULL
#define meshtastic_NodePositionEntry_position_MSGTYPE meshtastic_PositionLite
#define meshtastic_NodeTelemetryEntry_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 2)
#define meshtastic_NodeTelemetryEntry_CALLBACK NULL
#define meshtastic_NodeTelemetryEntry_DEFAULT NULL
#define meshtastic_NodeTelemetryEntry_device_metrics_MSGTYPE meshtastic_DeviceMetrics
#define meshtastic_NodeEnvironmentEntry_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, environment_metrics, 2)
#define meshtastic_NodeEnvironmentEntry_CALLBACK NULL
#define meshtastic_NodeEnvironmentEntry_DEFAULT NULL
#define meshtastic_NodeEnvironmentEntry_environment_metrics_MSGTYPE meshtastic_EnvironmentMetrics
#define meshtastic_NodeStatusEntry_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, status, 2)
#define meshtastic_NodeStatusEntry_CALLBACK NULL
#define meshtastic_NodeStatusEntry_DEFAULT NULL
#define meshtastic_NodeStatusEntry_status_MSGTYPE meshtastic_StatusMessage
#define meshtastic_NodeDatabase_FIELDLIST(X, a) \ #define meshtastic_NodeDatabase_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, version, 1) \ X(a, STATIC, SINGULAR, UINT32, version, 1) \
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2) \
X(a, CALLBACK, REPEATED, MESSAGE, positions, 3) \
X(a, CALLBACK, REPEATED, MESSAGE, telemetry, 4) \
X(a, CALLBACK, REPEATED, MESSAGE, status, 5) \
X(a, CALLBACK, REPEATED, MESSAGE, environment, 6)
extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field); extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback #define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback
#define meshtastic_NodeDatabase_DEFAULT NULL #define meshtastic_NodeDatabase_DEFAULT NULL
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite #define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite
#define meshtastic_NodeDatabase_positions_MSGTYPE meshtastic_NodePositionEntry
#define meshtastic_NodeDatabase_telemetry_MSGTYPE meshtastic_NodeTelemetryEntry
#define meshtastic_NodeDatabase_status_MSGTYPE meshtastic_NodeStatusEntry
#define meshtastic_NodeDatabase_environment_MSGTYPE meshtastic_NodeEnvironmentEntry
#define meshtastic_ChannelFile_FIELDLIST(X, a) \ #define meshtastic_ChannelFile_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \ X(a, STATIC, REPEATED, MESSAGE, channels, 1) \
@@ -345,6 +418,10 @@ extern const pb_msgdesc_t meshtastic_PositionLite_msg;
extern const pb_msgdesc_t meshtastic_UserLite_msg; extern const pb_msgdesc_t meshtastic_UserLite_msg;
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg; extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
extern const pb_msgdesc_t meshtastic_DeviceState_msg; extern const pb_msgdesc_t meshtastic_DeviceState_msg;
extern const pb_msgdesc_t meshtastic_NodePositionEntry_msg;
extern const pb_msgdesc_t meshtastic_NodeTelemetryEntry_msg;
extern const pb_msgdesc_t meshtastic_NodeEnvironmentEntry_msg;
extern const pb_msgdesc_t meshtastic_NodeStatusEntry_msg;
extern const pb_msgdesc_t meshtastic_NodeDatabase_msg; extern const pb_msgdesc_t meshtastic_NodeDatabase_msg;
extern const pb_msgdesc_t meshtastic_ChannelFile_msg; extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
@@ -354,6 +431,10 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define meshtastic_UserLite_fields &meshtastic_UserLite_msg #define meshtastic_UserLite_fields &meshtastic_UserLite_msg
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg #define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg #define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg
#define meshtastic_NodePositionEntry_fields &meshtastic_NodePositionEntry_msg
#define meshtastic_NodeTelemetryEntry_fields &meshtastic_NodeTelemetryEntry_msg
#define meshtastic_NodeEnvironmentEntry_fields &meshtastic_NodeEnvironmentEntry_msg
#define meshtastic_NodeStatusEntry_fields &meshtastic_NodeStatusEntry_msg
#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg #define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg #define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
#define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg #define meshtastic_BackupPreferences_fields &meshtastic_BackupPreferences_msg
@@ -364,7 +445,11 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define meshtastic_BackupPreferences_size 2432 #define meshtastic_BackupPreferences_size 2432
#define meshtastic_ChannelFile_size 718 #define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737 #define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196 #define meshtastic_NodeEnvironmentEntry_size 170
#define meshtastic_NodeInfoLite_size 105
#define meshtastic_NodePositionEntry_size 36
#define meshtastic_NodeStatusEntry_size 89
#define meshtastic_NodeTelemetryEntry_size 35
#define meshtastic_PositionLite_size 28 #define meshtastic_PositionLite_size 28
#define meshtastic_UserLite_size 98 #define meshtastic_UserLite_size 98
@@ -0,0 +1,15 @@
/* Automatically generated nanopb constant definitions */
/* Generated by nanopb-0.4.9.1 */
#include "meshtastic/deviceonly_legacy.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
PB_BIND(meshtastic_NodeInfoLite_Legacy, meshtastic_NodeInfoLite_Legacy, AUTO)
PB_BIND(meshtastic_NodeDatabase_Legacy, meshtastic_NodeDatabase_Legacy, AUTO)
@@ -0,0 +1,124 @@
/* Automatically generated nanopb header */
/* Generated by nanopb-0.4.9.1 */
#ifndef PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED
#define PB_MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_INCLUDED
#include <pb.h>
#include <vector>
#include "meshtastic/deviceonly.pb.h"
#include "meshtastic/telemetry.pb.h"
#if PB_PROTO_HEADER_VERSION != 40
#error Regenerate this file with the current version of nanopb generator.
#endif
/* Struct definitions */
/* Legacy NodeInfoLite descriptor used only to decode pre-split
/prefs/nodes.proto saves during the v24 -> v25 migration boot.
This preserves the original NodeInfoLite-compatible field numbers needed
to parse old wire bytes cleanly, including user (2), position (3),
device_metrics (6), and the legacy-only compatibility fields via_mqtt (8),
is_favorite (10), and is_ignored (11). Steady-state code does not use
this struct; it is dropped after migration completes. This file should be
removed once DEVICESTATE_MIN_VER advances past 24. */
typedef struct _meshtastic_NodeInfoLite_Legacy {
uint32_t num;
bool has_user;
meshtastic_UserLite user;
bool has_position;
meshtastic_PositionLite position;
float snr;
uint32_t last_heard;
bool has_device_metrics;
meshtastic_DeviceMetrics device_metrics;
uint8_t channel;
bool via_mqtt;
bool has_hops_away;
uint8_t hops_away;
bool is_favorite;
bool is_ignored;
uint8_t next_hop;
uint32_t bitfield;
} meshtastic_NodeInfoLite_Legacy;
/* Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy
with no satellite position/telemetry arrays. */
typedef struct _meshtastic_NodeDatabase_Legacy {
uint32_t version;
std::vector<meshtastic_NodeInfoLite_Legacy> nodes;
} meshtastic_NodeDatabase_Legacy;
#ifdef __cplusplus
extern "C" {
#endif
/* Initializer values for message structs */
#define meshtastic_NodeInfoLite_Legacy_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_NodeDatabase_Legacy_init_default {0, {0}}
#define meshtastic_NodeInfoLite_Legacy_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_NodeDatabase_Legacy_init_zero {0, {0}}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_NodeInfoLite_Legacy_num_tag 1
#define meshtastic_NodeInfoLite_Legacy_user_tag 2
#define meshtastic_NodeInfoLite_Legacy_position_tag 3
#define meshtastic_NodeInfoLite_Legacy_snr_tag 4
#define meshtastic_NodeInfoLite_Legacy_last_heard_tag 5
#define meshtastic_NodeInfoLite_Legacy_device_metrics_tag 6
#define meshtastic_NodeInfoLite_Legacy_channel_tag 7
#define meshtastic_NodeInfoLite_Legacy_via_mqtt_tag 8
#define meshtastic_NodeInfoLite_Legacy_hops_away_tag 9
#define meshtastic_NodeInfoLite_Legacy_is_favorite_tag 10
#define meshtastic_NodeInfoLite_Legacy_is_ignored_tag 11
#define meshtastic_NodeInfoLite_Legacy_next_hop_tag 12
#define meshtastic_NodeInfoLite_Legacy_bitfield_tag 13
#define meshtastic_NodeDatabase_Legacy_version_tag 1
#define meshtastic_NodeDatabase_Legacy_nodes_tag 2
/* Struct field encoding specification for nanopb */
#define meshtastic_NodeInfoLite_Legacy_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, position, 3) \
X(a, STATIC, SINGULAR, FLOAT, snr, 4) \
X(a, STATIC, SINGULAR, FIXED32, last_heard, 5) \
X(a, STATIC, OPTIONAL, MESSAGE, device_metrics, 6) \
X(a, STATIC, SINGULAR, UINT32, channel, 7) \
X(a, STATIC, SINGULAR, BOOL, via_mqtt, 8) \
X(a, STATIC, OPTIONAL, UINT32, hops_away, 9) \
X(a, STATIC, SINGULAR, BOOL, is_favorite, 10) \
X(a, STATIC, SINGULAR, BOOL, is_ignored, 11) \
X(a, STATIC, SINGULAR, UINT32, next_hop, 12) \
X(a, STATIC, SINGULAR, UINT32, bitfield, 13)
#define meshtastic_NodeInfoLite_Legacy_CALLBACK NULL
#define meshtastic_NodeInfoLite_Legacy_DEFAULT NULL
#define meshtastic_NodeInfoLite_Legacy_user_MSGTYPE meshtastic_UserLite
#define meshtastic_NodeInfoLite_Legacy_position_MSGTYPE meshtastic_PositionLite
#define meshtastic_NodeInfoLite_Legacy_device_metrics_MSGTYPE meshtastic_DeviceMetrics
#define meshtastic_NodeDatabase_Legacy_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, version, 1) \
X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2)
extern bool meshtastic_NodeDatabase_Legacy_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
#define meshtastic_NodeDatabase_Legacy_CALLBACK meshtastic_NodeDatabase_Legacy_callback
#define meshtastic_NodeDatabase_Legacy_DEFAULT NULL
#define meshtastic_NodeDatabase_Legacy_nodes_MSGTYPE meshtastic_NodeInfoLite_Legacy
extern const pb_msgdesc_t meshtastic_NodeInfoLite_Legacy_msg;
extern const pb_msgdesc_t meshtastic_NodeDatabase_Legacy_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define meshtastic_NodeInfoLite_Legacy_fields &meshtastic_NodeInfoLite_Legacy_msg
#define meshtastic_NodeDatabase_Legacy_fields &meshtastic_NodeDatabase_Legacy_msg
/* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_Legacy_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_LEGACY_PB_H_MAX_SIZE meshtastic_NodeInfoLite_Legacy_size
#define meshtastic_NodeInfoLite_Legacy_size 196
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
+16 -16
View File
@@ -729,7 +729,7 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
uint32_t readIndex = 0; uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) { while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user) { if (nodeInfoLiteHasUser(tempNodeInfo)) {
JSONObject node; JSONObject node;
char id[16]; char id[16];
@@ -737,26 +737,26 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
node["id"] = new JSONValue(id); node["id"] = new JSONValue(id);
node["snr"] = new JSONValue(tempNodeInfo->snr); node["snr"] = new JSONValue(tempNodeInfo->snr);
node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt)); node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard); node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
node["position"] = new JSONValue(); node["position"] = new JSONValue();
if (nodeDB->hasValidPosition(tempNodeInfo)) { if (nodeDB->hasValidPosition(tempNodeInfo)) {
JSONObject position; meshtastic_PositionLite posLite;
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7); if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7); JSONObject position;
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude); position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
node["position"] = new JSONValue(position); position["longitude"] = new JSONValue((float)posLite.longitude_i * 1e-7);
position["altitude"] = new JSONValue((int)posLite.altitude);
node["position"] = new JSONValue(position);
}
} }
node["long_name"] = new JSONValue(tempNodeInfo->user.long_name); node["long_name"] = new JSONValue(tempNodeInfo->long_name);
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name); node["short_name"] = new JSONValue(tempNodeInfo->short_name);
char macStr[18]; // mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0], node["mac_address"] = new JSONValue("00:00:00:00:00:00");
tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3], node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
node["mac_address"] = new JSONValue(macStr);
node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
nodesArray.push_back(new JSONValue(node)); nodesArray.push_back(new JSONValue(node));
} }
@@ -930,4 +930,4 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
res->print(jsonString.c_str()); res->print(jsonString.c_str());
delete value; delete value;
} }
#endif #endif
+37 -3
View File
@@ -37,9 +37,43 @@
#define MAX_RX_NOTIFICATION_TOPHONE 2 #define MAX_RX_NOTIFICATION_TOPHONE 2
#endif #endif
/// Verify baseline assumption of node size. If it increases, we need to reevaluate /// Tighten this when the slim header shrinks; loosen only with deliberate
/// the impact of its memory footprint, notably on MAX_NUM_NODES. /// awareness of MAX_NUM_NODES impact per platform.
static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES."); static_assert(sizeof(meshtastic_NodeInfoLite) <= 130, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
// Compile satellite NodeDBs out on STM32WL (and the status DB also follows
// MESHTASTIC_EXCLUDE_STATUS).
#ifndef MESHTASTIC_EXCLUDE_POSITIONDB
#if defined(ARCH_STM32WL)
#define MESHTASTIC_EXCLUDE_POSITIONDB 1
#else
#define MESHTASTIC_EXCLUDE_POSITIONDB 0
#endif
#endif
#ifndef MESHTASTIC_EXCLUDE_TELEMETRYDB
#if defined(ARCH_STM32WL)
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 1
#else
#define MESHTASTIC_EXCLUDE_TELEMETRYDB 0
#endif
#endif
#ifndef MESHTASTIC_EXCLUDE_ENVIRONMENTDB
#if defined(ARCH_STM32WL)
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 1
#else
#define MESHTASTIC_EXCLUDE_ENVIRONMENTDB 0
#endif
#endif
#ifndef MESHTASTIC_EXCLUDE_STATUSDB
#if defined(ARCH_STM32WL) || defined(MESHTASTIC_EXCLUDE_STATUS)
#define MESHTASTIC_EXCLUDE_STATUSDB 1
#else
#define MESHTASTIC_EXCLUDE_STATUSDB 0
#endif
#endif
/// max number of nodes allowed in the nodeDB /// max number of nodes allowed in the nodeDB
#ifndef MAX_NUM_NODES #ifndef MAX_NUM_NODES
+15 -14
View File
@@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
// Automatically favorite the node that is using the admin key // Automatically favorite the node that is using the admin key
auto remoteNode = nodeDB->getMeshNode(mp.from); auto remoteNode = nodeDB->getMeshNode(mp.from);
if (remoteNode && !remoteNode->is_favorite) { if (remoteNode && !nodeInfoLiteIsFavorite(remoteNode)) {
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it // Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
// without the user doing so deliberately. // without the user doing so deliberately.
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from); LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
} else { } else {
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from); LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
remoteNode->is_favorite = true; nodeInfoLiteSetBit(remoteNode, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
} }
} }
} else { } else {
@@ -393,7 +393,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received set_favorite_node command"); LOG_INFO("Client received set_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
if (node != NULL) { if (node != NULL) {
node->is_favorite = true; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
if (screen) if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
@@ -404,7 +404,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received remove_favorite_node command"); LOG_INFO("Client received remove_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
if (node != NULL) { if (node != NULL) {
node->is_favorite = false; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
if (screen) if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
@@ -415,11 +415,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received set_ignored_node command"); LOG_INFO("Client received set_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
if (node != NULL) { if (node != NULL) {
node->is_ignored = true; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true);
node->has_device_metrics = false; nodeDB->eraseNodeSatellites(node->num);
node->has_position = false; node->public_key.size = 0;
node->user.public_key.size = 0; memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -428,7 +427,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received remove_ignored_node command"); LOG_INFO("Client received remove_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
if (node != NULL) { if (node != NULL) {
node->is_ignored = false; nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, false);
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -437,7 +436,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
LOG_INFO("Client received toggle_muted_node command"); LOG_INFO("Client received toggle_muted_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
if (node != NULL) { if (node != NULL) {
node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT); nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_IS_MUTED_MASK, !nodeInfoLiteIsMuted(node));
saveChanges(SEGMENT_NODEDATABASE, false); saveChanges(SEGMENT_NODEDATABASE, false);
} }
break; break;
@@ -445,9 +444,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
case meshtastic_AdminMessage_set_fixed_position_tag: { case meshtastic_AdminMessage_set_fixed_position_tag: {
LOG_INFO("Client received set_fixed_position command"); LOG_INFO("Client received set_fixed_position command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
node->has_position = true; // Route the fixed position through updatePosition so it lands in the
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position); // satellite map (or, on builds with PositionDB excluded, just sets
// localPosition for the local broadcast path).
nodeDB->updatePosition(node->num, r->set_fixed_position, RX_SRC_LOCAL);
nodeDB->setLocalPosition(r->set_fixed_position); nodeDB->setLocalPosition(r->set_fixed_position);
config.position.fixed_position = true; config.position.fixed_position = true;
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
+12 -12
View File
@@ -141,7 +141,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
static bool returnToCannedList = false; static bool returnToCannedList = false;
bool hasKeyForNode(const meshtastic_NodeInfoLite *node) bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
{ {
return node && node->has_user && node->user.public_key.size > 0; return nodeInfoLiteHasUser(node) && node->public_key.size > 0;
} }
/** /**
* @brief Items in array this->messages will be set to be pointing on the right * @brief Items in array this->messages will be set to be pointing on the right
@@ -262,10 +262,10 @@ void CannedMessageModule::updateDestinationSelectionList()
for (size_t i = 0; i < numMeshNodes; ++i) { for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) if (!node || node->num == myNodeNum || !nodeInfoLiteHasUser(node) || node->public_key.size != 32)
continue; continue;
const String &nodeName = node->user.long_name; const String &nodeName = node->long_name;
if (searchQuery.length() == 0) { if (searchQuery.length() == 0) {
this->filteredNodes.push_back({node, sinceLastSeen(node)}); this->filteredNodes.push_back({node, sinceLastSeen(node)});
@@ -1054,7 +1054,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
} }
NodeNum myNodeNum = nodeDB->getNodeNum(); NodeNum myNodeNum = nodeDB->getNodeNum();
if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { if (node && node->num != myNodeNum && nodeInfoLiteHasUser(node) && node->public_key.size == 32) {
p->pki_encrypted = true; p->pki_encrypted = true;
p->channel = 0; // force PKI p->channel = 0; // force PKI
} }
@@ -1415,8 +1415,8 @@ const char *CannedMessageModule::getNodeName(NodeNum node)
return "Broadcast"; return "Broadcast";
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user && strlen(info->user.long_name) > 0) { if (nodeInfoLiteHasUser(info) && strlen(info->long_name) > 0) {
return info->user.long_name; return info->long_name;
} }
static char fallback[12]; static char fallback[12];
@@ -1723,17 +1723,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
if (node) { if (node) {
if (display->getWidth() <= 64) { if (display->getWidth() <= 64) {
entryText = node->user.short_name; entryText = node->short_name;
} else if (node->user.long_name[0]) { } else if (node->long_name[0]) {
entryText = node->user.long_name; entryText = node->long_name;
} else { } else {
entryText = node->user.short_name; entryText = node->short_name;
} }
} }
int availWidth = display->getWidth() - int availWidth = display->getWidth() -
((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) -
((node && node->is_favorite) ? 10 : 0); ((nodeInfoLiteIsFavorite(node)) ? 10 : 0);
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
char truncatedEntry[96]; char truncatedEntry[96];
@@ -1742,7 +1742,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
entryText = truncatedEntry; entryText = truncatedEntry;
// Prepend "* " if this is a favorite // Prepend "* " if this is a favorite
if (node && node->is_favorite) { if (nodeInfoLiteIsFavorite(node)) {
entryText = "* " + entryText; entryText = "* " + entryText;
} }
graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry),
+1 -1
View File
@@ -372,7 +372,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP
// If we receive a direct message and the receipent is us, apply DM mute setting // If we receive a direct message and the receipent is us, apply DM mute setting
// Else we just handle it as not muted. // Else we just handle it as not muted.
const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp); const bool isDmToUs = !isBroadcast(mp.to) && isToUs(&mp);
bool is_muted = isDmToUs ? (sender && ((sender->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0)) bool is_muted = isDmToUs ? nodeInfoLiteIsMuted(sender)
: (ch.settings.has_module_settings && ch.settings.module_settings.is_muted); : (ch.settings.has_module_settings && ch.settings.module_settings.is_muted);
const bool buzzerModeIsDirectOnly = const bool buzzerModeIsDirectOnly =
+31 -17
View File
@@ -10,6 +10,18 @@
KeyVerificationModule *keyVerificationModule; KeyVerificationModule *keyVerificationModule;
namespace
{
void copyNodeLongNameOrUnknown(char *dest, size_t destSize, const meshtastic_NodeInfoLite *node)
{
if (!dest || destSize == 0)
return;
const char *name = (node && nodeInfoLiteHasUser(node) && node->long_name[0]) ? node->long_name : "Unknown";
strncpy(dest, name, destSize - 1);
dest[destSize - 1] = '\0';
}
} // namespace
KeyVerificationModule::KeyVerificationModule() KeyVerificationModule::KeyVerificationModule()
: ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg)
{ {
@@ -37,7 +49,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
request->key_verification.nonce == currentNonce) { request->key_verification.nonce == currentNonce) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; if (remoteNodePtr)
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
resetToIdle(); resetToIdle();
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
resetToIdle(); resetToIdle();
@@ -72,9 +85,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
sprintf(cn->message, "Enter Security Number for Key Verification"); sprintf(cn->message, "Enter Security Number for Key Verification");
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
cn->payload_variant.key_verification_number_request.nonce = currentNonce; cn->payload_variant.key_verification_number_request.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_request.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_request.remote_longname),
sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); nodeDB->getMeshNode(currentRemoteNode));
service->sendClientNotification(cn); service->sendClientNotification(cn);
LOG_INFO("Received hash2"); LOG_INFO("Received hash2");
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
@@ -95,7 +108,8 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
[=](int selected) { [=](int selected) {
if (selected == 1) { if (selected == 1) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; if (remoteNodePtr)
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
} }
}; };
screen->showOverlayBanner(options);) screen->showOverlayBanner(options);)
@@ -104,9 +118,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce; cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname),
sizeof(cn->payload_variant.key_verification_final.remote_longname)); nodeDB->getMeshNode(currentRemoteNode));
cn->payload_variant.key_verification_final.isSender = false; cn->payload_variant.key_verification_final.isSender = false;
service->sendClientNotification(cn); service->sendClientNotification(cn);
@@ -202,9 +216,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
currentSecurityNumber % 1000); currentSecurityNumber % 1000);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
cn->payload_variant.key_verification_number_inform.nonce = currentNonce; cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_number_inform.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_number_inform.remote_longname),
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); nodeDB->getMeshNode(currentRemoteNode));
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
service->sendClientNotification(cn); service->sendClientNotification(cn);
LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce);
@@ -219,7 +233,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
LOG_WARN("received security number: %u", incomingNumber); LOG_WARN("received security number: %u", incomingNumber);
meshtastic_NodeInfoLite *remoteNodePtr = nullptr; meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
currentState = KEY_VERIFICATION_IDLE; currentState = KEY_VERIFICATION_IDLE;
return; // should we throw an error here? return; // should we throw an error here?
} }
@@ -232,7 +246,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
hash.update(&currentRemoteNode, sizeof(currentRemoteNode)); hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(owner.public_key.bytes, owner.public_key.size); hash.update(owner.public_key.bytes, owner.public_key.size);
hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
hash.finalize(hash1, 32); hash.finalize(hash1, 32);
hash.reset(); hash.reset();
@@ -265,9 +279,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce; cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc copyNodeLongNameOrUnknown(cn->payload_variant.key_verification_final.remote_longname,
nodeDB->getMeshNode(currentRemoteNode)->user.long_name, sizeof(cn->payload_variant.key_verification_final.remote_longname),
sizeof(cn->payload_variant.key_verification_final.remote_longname)); nodeDB->getMeshNode(currentRemoteNode));
cn->payload_variant.key_verification_final.isSender = true; cn->payload_variant.key_verification_final.isSender = true;
service->sendClientNotification(cn); service->sendClientNotification(cn);
LOG_INFO(message); LOG_INFO(message);
@@ -310,4 +324,4 @@ void KeyVerificationModule::generateVerificationCode(char *readableCode)
readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary.
} }
} }
#endif #endif
+23 -13
View File
@@ -183,8 +183,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
return nullptr; return nullptr;
} }
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position const meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
assert(node->has_position);
// configuration of POSITION packet // configuration of POSITION packet
// consider making this a function argument? // consider making this a function argument?
@@ -194,7 +193,9 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
// if localPosition is totally empty, put our last saved position (lite) in there // if localPosition is totally empty, put our last saved position (lite) in there
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position)); meshtastic_PositionLite cachedSelf;
if (nodeDB->copyNodePosition(node->num, cachedSelf))
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(cachedSelf));
} }
localPosition.seq_number++; localPosition.seq_number++;
@@ -420,7 +421,7 @@ int32_t PositionModule::runOnce()
doDeepSleep(nightyNightMs, false, false); doDeepSleep(nightyNightMs, false, false);
} }
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node == nullptr) if (node == nullptr)
return RUNONCE_INTERVAL; return RUNONCE_INTERVAL;
@@ -445,8 +446,11 @@ int32_t PositionModule::runOnce()
} else if (nodeDB->hasValidPosition(node)) { } else if (nodeDB->hasValidPosition(node)) {
lastGpsSend = now; lastGpsSend = now;
lastGpsLatitude = node->position.latitude_i; meshtastic_PositionLite selfPos;
lastGpsLongitude = node->position.longitude_i; if (nodeDB->copyNodePosition(node->num, selfPos)) {
lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = selfPos.longitude_i;
}
if (transmitHistory) if (transmitHistory)
transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP);
@@ -461,7 +465,10 @@ int32_t PositionModule::runOnce()
if (nodeDB->hasValidPosition(node2)) { if (nodeDB->hasValidPosition(node2)) {
// The minimum time (in seconds) that would pass before we are able to send a new position packet. // The minimum time (in seconds) that would pass before we are able to send a new position packet.
auto smartPosition = getDistanceTraveledSinceLastSend(node->position); meshtastic_PositionLite selfPos;
if (!nodeDB->copyNodePosition(node->num, selfPos))
return RUNONCE_INTERVAL; // Defensive: hasValidPosition should imply this is non-null
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
msSinceLastSend = now - lastGpsSend; msSinceLastSend = now - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold && if (smartPosition.hasTraveledOverThreshold &&
@@ -479,8 +486,8 @@ int32_t PositionModule::runOnce()
msSinceLastSend, minimumTimeThreshold); msSinceLastSend, minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send // Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i; lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = node->position.longitude_i; lastGpsLongitude = selfPos.longitude_i;
} }
} }
} }
@@ -563,11 +570,14 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic
void PositionModule::handleNewPosition() void PositionModule::handleNewPosition()
{ {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
// We limit our GPS broadcasts to a max rate // We limit our GPS broadcasts to a max rate
if (nodeDB->hasValidPosition(node2)) { if (nodeDB->hasValidPosition(node2)) {
auto smartPosition = getDistanceTraveledSinceLastSend(node->position); meshtastic_PositionLite selfPos;
if (!nodeDB->copyNodePosition(node->num, selfPos))
return;
auto smartPosition = getDistanceTraveledSinceLastSend(selfPos);
uint32_t msSinceLastSend = millis() - lastGpsSend; uint32_t msSinceLastSend = millis() - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold && if (smartPosition.hasTraveledOverThreshold &&
Throttle::execute( Throttle::execute(
@@ -583,8 +593,8 @@ void PositionModule::handleNewPosition()
minimumTimeThreshold); minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send // Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i; lastGpsLatitude = selfPos.latitude_i;
lastGpsLongitude = node->position.longitude_i; lastGpsLongitude = selfPos.longitude_i;
} }
} }
} }
+21 -11
View File
@@ -268,26 +268,36 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
fileToAppend.printf("??:??:??,"); // Time fileToAppend.printf("??:??:??,"); // Time
} }
fileToAppend.printf("%d,", getFrom(&mp)); // From fileToAppend.printf("%d,", getFrom(&mp)); // From
fileToAppend.printf("%s,", n->user.long_name); // Long Name fileToAppend.printf("%s,", n ? n->long_name : ""); // Long Name
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat meshtastic_PositionLite senderPos;
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long const bool haveSenderPos = nodeDB->copyNodePosition(getFrom(&mp), senderPos);
if (haveSenderPos) {
fileToAppend.printf("%f,", senderPos.latitude_i * 1e-7); // Sender Lat
fileToAppend.printf("%f,", senderPos.longitude_i * 1e-7); // Sender Long
} else {
fileToAppend.printf("0.0,0.0,");
}
if (gpsStatus->getIsConnected() || config.position.fixed_position) { if (gpsStatus->getIsConnected() || config.position.fixed_position) {
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
} else { } else {
// When the phone API is in use, the node info will be updated with position // When the phone API is in use, the node info will be updated with position
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_PositionLite usPos;
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat if (nodeDB->copyNodePosition(nodeDB->getNodeNum(), usPos)) {
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long fileToAppend.printf("%f,", usPos.latitude_i * 1e-7); // RX Lat
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude fileToAppend.printf("%f,", usPos.longitude_i * 1e-7); // RX Long
fileToAppend.printf("%d,", usPos.altitude); // RX Altitude
} else {
fileToAppend.printf("0.0,0.0,0,");
}
} }
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) { if (haveSenderPos && senderPos.latitude_i && senderPos.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7, float distance = GeoCoord::latLongToMeter(senderPos.latitude_i * 1e-7, senderPos.longitude_i * 1e-7,
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7); gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
fileToAppend.printf("%f,", distance); // Distance in meters fileToAppend.printf("%f,", distance); // Distance in meters
} else { } else {
@@ -340,4 +350,4 @@ bool RangeTestModuleRadio::removeFile()
return 0; return 0;
#endif #endif
} }
+1 -2
View File
@@ -17,8 +17,7 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) { config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
if (!maybePKI) if (!maybePKI)
return false; return false;
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && if (!nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.from)) && !nodeInfoLiteHasUser(nodeDB->getMeshNode(mp.to)))
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
return false; return false;
} else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) || } else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) ||
(nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) { (nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) {
+15 -8
View File
@@ -250,9 +250,12 @@ int32_t SerialModule::runOnce()
uint32_t readIndex = 0; uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex); const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) { while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) { if (nodeInfoLiteHasUser(tempNodeInfo) && nodeDB->hasValidPosition(tempNodeInfo)) {
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true); meshtastic_PositionLite pos;
serialPrint->printf("%s", outbuf); if (nodeDB->copyNodePosition(tempNodeInfo->num, pos)) {
printWPL(outbuf, sizeof(outbuf), pos, tempNodeInfo->long_name, true);
serialPrint->printf("%s", outbuf);
}
} }
tempNodeInfo = nodeDB->readNextMeshNode(readIndex); tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
} }
@@ -401,7 +404,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
serialPrint->write(p.payload.bytes, p.payload.size); serialPrint->write(p.payload.bytes, p.payload.size);
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
const char *sender = (node && node->has_user) ? node->user.short_name : "???"; const char *sender = nodeInfoLiteHasUser(node) ? node->short_name : "???";
serialPrint->println(); serialPrint->println();
serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->printf("%s: %s", sender, p.payload.bytes);
serialPrint->println(); serialPrint->println();
@@ -417,9 +420,13 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
decoded = &scratch; decoded = &scratch;
} }
// send position packet as WPL to the serial port // send position packet as WPL to the serial port
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name, {
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO); meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp));
serialPrint->printf("%s", outbuf); const char *senderName = senderNode ? senderNode->long_name : "";
printWPL(outbuf, sizeof(outbuf), *decoded, senderName,
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
serialPrint->printf("%s", outbuf);
}
} }
} }
} }
@@ -714,4 +721,4 @@ void SerialModule::processWXSerial()
#endif #endif
return; return;
} }
#endif #endif
+3 -10
View File
@@ -2,6 +2,7 @@
#include "StatusMessageModule.h" #include "StatusMessageModule.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h"
#include "ProtobufModule.h" #include "ProtobufModule.h"
StatusMessageModule *statusMessageModule; StatusMessageModule *statusMessageModule;
@@ -36,16 +37,8 @@ ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
RecentStatus entry; if (nodeDB)
entry.fromNodeId = mp.from; nodeDB->setNodeStatus(mp.from, incomingMessage);
entry.statusText = incomingMessage.status;
recentReceived.push_back(std::move(entry));
// Keep only last MAX_RECENT_STATUSMESSAGES
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
recentReceived.erase(recentReceived.begin()); // drop oldest
}
} }
} }
return ProcessMessage::CONTINUE; return ProcessMessage::CONTINUE;
+3 -18
View File
@@ -2,8 +2,6 @@
#if !MESHTASTIC_EXCLUDE_STATUS #if !MESHTASTIC_EXCLUDE_STATUS
#include "SinglePortModule.h" #include "SinglePortModule.h"
#include "configuration.h" #include "configuration.h"
#include <string>
#include <vector>
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
{ {
@@ -20,29 +18,16 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
this->setInterval(1000 * 12 * 60 * 60); this->setInterval(1000 * 12 * 60 * 60);
} }
// TODO: If we have a string, set the initial delay (15 minutes maybe) // TODO: If we have a string, set the initial delay (15 minutes maybe)
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
} }
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
struct RecentStatus {
uint32_t fromNodeId; // mp.from
std::string statusText; // incomingMessage.status
};
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
protected: protected:
/** Called to handle a particular incoming message /** Called to handle a particular incoming message. The cached most-recent
* status for each node lives on NodeDB; use nodeDB->copyNodeStatus(num, ...).
*/ */
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
private:
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
std::vector<RecentStatus> recentReceived;
}; };
extern StatusMessageModule *statusMessageModule; extern StatusMessageModule *statusMessageModule;
#endif #endif
+2 -2
View File
@@ -80,9 +80,9 @@ void StoreForwardModule::populatePSRAM()
(this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct))); (this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)));
this->records = numberOfPackets; this->records = numberOfPackets;
#if defined(ARCH_ESP32) #if defined(ARCH_ESP32)
this->packetHistory = static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); this->packetHistory.reset(static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
this->packetHistory = static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct))); this->packetHistory.reset(static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
#endif #endif
+8 -1
View File
@@ -7,6 +7,7 @@
#include "configuration.h" #include "configuration.h"
#include <Arduino.h> #include <Arduino.h>
#include <functional> #include <functional>
#include <memory>
#include <unordered_map> #include <unordered_map>
struct PacketHistoryStruct { struct PacketHistoryStruct {
@@ -29,11 +30,17 @@ struct PacketHistoryStruct {
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward> class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
{ {
// packetHistory is allocated with ps_calloc / calloc, so it must be released with free(),
// not delete[].
struct CFreeDeleter {
void operator()(PacketHistoryStruct *p) const noexcept { free(p); }
};
bool busy = 0; bool busy = 0;
uint32_t busyTo = 0; uint32_t busyTo = 0;
char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0}; char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
PacketHistoryStruct *packetHistory = 0; std::unique_ptr<PacketHistoryStruct[], CFreeDeleter> packetHistory;
uint32_t packetHistoryTotalCount = 0; uint32_t packetHistoryTotalCount = 0;
uint32_t last_time = 0; uint32_t last_time = 0;
uint32_t requestCount = 0; uint32_t requestCount = 0;
@@ -515,6 +515,10 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac
packetPool.release(lastMeasurementPacket); packetPool.release(lastMeasurementPacket);
lastMeasurementPacket = packetPool.allocCopy(mp); lastMeasurementPacket = packetPool.allocCopy(mp);
// Cache the latest env metrics per node on NodeDB so the phone can
// pull last-known values across reboots and replays.
nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO);
} }
return false; // Let others look at this message also if they want return false; // Let others look at this message also if they want
+52 -15
View File
@@ -16,10 +16,28 @@ int32_t INA3221Sensor::runOnce()
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
} }
if (!status) { if (!status) {
ina3221.begin(nodeTelemetrySensorsMap[sensorType].second); // Re-initialise with the address and Wire bus from the telemetry sensors map.
ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors // (Rob Tillaart INA3221_RT takes address + TwoWire*, unlike sgtwilko which took Wire in begin().)
status = true; ina3221 = INA3221(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second);
status = ina3221.begin();
if (status) {
// Default all three channels to a 0.1 Ω shunt resistor.
// Override per-variant by defining INA3221_SHUNT_R_CH1/CH2/CH3 (in Ohms) in variant.h.
#ifndef INA3221_SHUNT_R_CH1
#define INA3221_SHUNT_R_CH1 0.1f
#endif
#ifndef INA3221_SHUNT_R_CH2
#define INA3221_SHUNT_R_CH2 0.1f
#endif
#ifndef INA3221_SHUNT_R_CH3
#define INA3221_SHUNT_R_CH3 0.1f
#endif
ina3221.setShuntR(0, INA3221_SHUNT_R_CH1);
ina3221.setShuntR(1, INA3221_SHUNT_R_CH2);
ina3221.setShuntR(2, INA3221_SHUNT_R_CH3);
}
} else { } else {
// Already initialised; status stays true and initI2CSensor() returns next poll interval.
status = true; status = true;
} }
return initI2CSensor(); return initI2CSensor();
@@ -27,12 +45,14 @@ int32_t INA3221Sensor::runOnce()
void INA3221Sensor::setup() {} void INA3221Sensor::setup() {}
struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch) struct _INA3221Measurement INA3221Sensor::getMeasurement(uint8_t ch)
{ {
struct _INA3221Measurement measurement; struct _INA3221Measurement measurement;
measurement.voltage = ina3221.getVoltage(ch); measurement.voltage = ina3221.getBusVoltage(ch); // Volts
measurement.current = ina3221.getCurrent(ch); // getCurrent_mA() is used instead of getCurrent() because Rob Tillaart's getCurrent()
// returns Amperes; the telemetry proto and VoltageSensor/CurrentSensor interfaces expect mA.
measurement.current = ina3221.getCurrent_mA(ch); // milliAmps
return measurement; return measurement;
} }
@@ -43,7 +63,7 @@ struct _INA3221Measurements INA3221Sensor::getMeasurements()
// INA3221 has 3 channels starting from 0 // INA3221 has 3 channels starting from 0
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
measurements.measurements[i] = getMeasurement((ina3221_ch_t)i); measurements.measurements[i] = getMeasurement((uint8_t)i);
} }
return measurements; return measurements;
@@ -87,24 +107,41 @@ bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
measurement->variant.power_metrics.has_ch3_voltage = true; measurement->variant.power_metrics.has_ch3_voltage = true;
measurement->variant.power_metrics.has_ch3_current = true; measurement->variant.power_metrics.has_ch3_current = true;
measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage; // INA3221 channel indices are zero-based (0=CH1, 1=CH2, 2=CH3).
measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current; measurement->variant.power_metrics.ch1_voltage = m.measurements[0].voltage;
measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage; measurement->variant.power_metrics.ch1_current = m.measurements[0].current;
measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current; measurement->variant.power_metrics.ch2_voltage = m.measurements[1].voltage;
measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage; measurement->variant.power_metrics.ch2_current = m.measurements[1].current;
measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current; measurement->variant.power_metrics.ch3_voltage = m.measurements[2].voltage;
measurement->variant.power_metrics.ch3_current = m.measurements[2].current;
return true; return true;
} }
uint16_t INA3221Sensor::getBusVoltageMv() uint16_t INA3221Sensor::getBusVoltageMv()
{ {
return lround(ina3221.getVoltage(BAT_CH) * 1000); return lround(ina3221.getBusVoltage_mV(BAT_CH));
} }
int16_t INA3221Sensor::getCurrentMa() int16_t INA3221Sensor::getCurrentMa()
{ {
return lround(ina3221.getCurrent(BAT_CH)); return lround(ina3221.getCurrent_mA(BAT_CH));
}
// Bus voltage register (0x02 + ch*2): bits [15:3] unsigned, 1 LSB = 8 mV (datasheet p.6).
// Voltage raw units: 1 count = 8 mV, so V_mV = raw * 8.
int16_t INA3221Sensor::getRawBusVoltage(uint8_t ch)
{
return (int16_t)(ina3221.getRegister(0x02 + ch * 2) >> 3);
}
// Shunt voltage register (0x01 + ch*2): bits [15:3] signed two's complement, 1 LSB = 40 µV (datasheet p.6).
// Current raw units are shunt-voltage counts: 1 count = 40 uV, signed.
// I_mA = (raw * 40 uV) / R_mOhm, because uV / mOhm = mA.
// Example for 100 mOhm shunt: I_mA = raw * 40 / 100 = raw * 0.4.
int16_t INA3221Sensor::getRawShuntCurrent(uint8_t ch)
{
return (int16_t)(ina3221.getRegister(0x01 + ch * 2) >> 3);
} }
#endif #endif
+19 -6
View File
@@ -1,3 +1,9 @@
// INA3221 channel aliases (zero-based: 0 = CH1, 1 = CH2, 2 = CH3).
// Defined before configuration.h so variant.h can use them in INA3221_ENV_CH / INA3221_BAT_CH.
#define INA3221_CH1 0
#define INA3221_CH2 1
#define INA3221_CH3 2
#include "configuration.h" #include "configuration.h"
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<INA3221.h>) #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<INA3221.h>)
@@ -9,26 +15,29 @@
#include <INA3221.h> #include <INA3221.h>
#ifndef INA3221_ENV_CH #ifndef INA3221_ENV_CH
#define INA3221_ENV_CH INA3221_CH1 #define INA3221_ENV_CH INA3221_CH1 // channel to report in environment metrics (default: CH1)
#endif #endif
#ifndef INA3221_BAT_CH #ifndef INA3221_BAT_CH
#define INA3221_BAT_CH INA3221_CH1 #define INA3221_BAT_CH INA3221_CH1 // channel for device_battery_ina_address (default: CH1)
#endif #endif
class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
{ {
private: private:
INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); // Placeholder constructor; re-initialised with correct address and Wire in runOnce().
INA3221 ina3221 = INA3221(INA3221_ADDR);
// channel to report voltage/current for environment metrics // channel to report voltage/current for environment metrics
static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; static const uint8_t ENV_CH = INA3221_ENV_CH;
static_assert(INA3221_ENV_CH >= 0 && INA3221_ENV_CH <= 2, "INA3221_ENV_CH must be 0, 1, or 2");
// channel to report battery voltage for device_battery_ina_address // channel to report battery voltage for device_battery_ina_address
static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; static const uint8_t BAT_CH = INA3221_BAT_CH;
static_assert(INA3221_BAT_CH >= 0 && INA3221_BAT_CH <= 2, "INA3221_BAT_CH must be 0, 1, or 2");
// get a single measurement for a channel // get a single measurement for a channel
struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); struct _INA3221Measurement getMeasurement(uint8_t ch);
// get all measurements for all channels // get all measurements for all channels
struct _INA3221Measurements getMeasurements(); struct _INA3221Measurements getMeasurements();
@@ -45,6 +54,10 @@ class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
bool getMetrics(meshtastic_Telemetry *measurement) override; bool getMetrics(meshtastic_Telemetry *measurement) override;
virtual uint16_t getBusVoltageMv() override; virtual uint16_t getBusVoltageMv() override;
virtual int16_t getCurrentMa() override; virtual int16_t getCurrentMa() override;
// Raw register reads (bits [15:3] right-shifted), no conversion applied.
int16_t getRawBusVoltage(uint8_t ch);
int16_t getRawShuntCurrent(uint8_t ch);
}; };
struct _INA3221Measurement { struct _INA3221Measurement {
+5 -5
View File
@@ -492,12 +492,12 @@ TraceRouteModule::TraceRouteModule()
const char *TraceRouteModule::getNodeName(NodeNum node) const char *TraceRouteModule::getNodeName(NodeNum node)
{ {
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user) { if (nodeInfoLiteHasUser(info)) {
if (strlen(info->user.short_name) > 0) { if (strlen(info->short_name) > 0) {
return info->user.short_name; return info->short_name;
} }
if (strlen(info->user.long_name) > 0) { if (strlen(info->long_name) > 0) {
return info->user.long_name; return info->long_name;
} }
} }
+2 -2
View File
@@ -1226,9 +1226,9 @@ bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacke
// Fallback only when PSRAM cache is unavailable on this target. // Fallback only when PSRAM cache is unavailable on this target.
// In this mode we use the node-wide table maintained by NodeInfoModule. // In this mode we use the node-wide table maintained by NodeInfoModule.
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
if (!node || !node->has_user) if (!nodeInfoLiteHasUser(node))
return false; return false;
cachedUser = TypeConversions::ConvertToUser(node->num, node->user); cachedUser = TypeConversions::ConvertToUser(node);
} }
if (!sendResponse) if (!sendResponse)
+5 -3
View File
@@ -100,7 +100,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
char distStr[20] = ""; char distStr[20] = "";
// Get our node, to use our own position // Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); const meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// Match compass sizing/placement to favorite node screen logic. // Match compass sizing/placement to favorite node screen logic.
const int w = display->getWidth(); const int w = display->getWidth();
@@ -147,8 +147,10 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
const char *statusLine2 = nullptr; const char *statusLine2 = nullptr;
// Distance only needs our own position fix; compass/bearing additionally needs heading. // Distance only needs our own position fix; compass/bearing additionally needs heading.
if (hasOwnPositionFix) { meshtastic_PositionLite ownPos;
const meshtastic_PositionLite &op = ourNode->position; const bool haveOwnPos = ourNode && nodeDB->copyNodePosition(ourNode->num, ownPos);
if (hasOwnPositionFix && haveOwnPos) {
const meshtastic_PositionLite &op = ownPos;
const float d = const float d =
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
+4 -1
View File
@@ -141,7 +141,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to); const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to);
// Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's // Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's
// likely they discovered each other via a channel we have downlink enabled for // likely they discovered each other via a channel we have downlink enabled for
if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user)) if (isToUs(p.get()) || (nodeInfoLiteHasUser(tx) && nodeInfoLiteHasUser(rx)))
router->enqueueReceivedMessage(p.release()); router->enqueueReceivedMessage(p.release());
} else if (router && } else if (router &&
perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key
@@ -409,6 +409,9 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length)
void mqttInit() void mqttInit()
{ {
if (!moduleConfig.mqtt.enabled)
return;
new MQTT(); new MQTT();
} }
+4 -4
View File
@@ -18,6 +18,7 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <map> #include <map>
#include <memory>
#include <set> #include <set>
#include <stdexcept> #include <stdexcept>
#include <sys/ioctl.h> #include <sys/ioctl.h>
@@ -301,10 +302,9 @@ void portduinoSetup()
// Try CH341 // Try CH341
try { try {
std::cout << "autoconf: Looking for CH341 device..." << std::endl; std::cout << "autoconf: Looking for CH341 device..." << std::endl;
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, auto probe = std::unique_ptr<Ch341Hal>(new Ch341Hal(0, portduino_config.lora_usb_serial_num,
portduino_config.lora_usb_pid); portduino_config.lora_usb_vid, portduino_config.lora_usb_pid));
ch341Hal->getProductString(autoconf_product, 95); probe->getProductString(autoconf_product, 95);
delete ch341Hal;
std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl;
found_ch341 = true; found_ch341 = true;
+9 -5
View File
@@ -319,10 +319,14 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
// Lambda function for adding a long name to the route // Lambda function for adding a long name to the route
auto addToRoute = [](JSONArray *route, NodeNum num) { auto addToRoute = [](JSONArray *route, NodeNum num) {
char long_name[40] = "Unknown"; char long_name[40] = "Unknown";
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
bool name_known = node ? node->has_user : false; bool name_known = nodeInfoLiteHasUser(node);
if (name_known) if (name_known) {
memcpy(long_name, node->user.long_name, sizeof(long_name)); const size_t copy_len =
(sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1;
memcpy(long_name, node->long_name, copy_len);
long_name[copy_len] = '\0';
}
route->push_back(new JSONValue(long_name)); route->push_back(new JSONValue(long_name));
}; };
addToRoute(&route, mp->to); // Started at the original transmitter (destination of response) addToRoute(&route, mp->to); // Started at the original transmitter (destination of response)
@@ -473,4 +477,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
delete value; delete value;
return jsonStr; return jsonStr;
} }
#endif #endif
@@ -292,9 +292,13 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
auto addToRoute = [](JsonArray *route, NodeNum num) { auto addToRoute = [](JsonArray *route, NodeNum num) {
char long_name[40] = "Unknown"; char long_name[40] = "Unknown";
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(num);
bool name_known = node ? node->has_user : false; bool name_known = nodeInfoLiteHasUser(node);
if (name_known) if (name_known) {
memcpy(long_name, node->user.long_name, sizeof(long_name)); const size_t copy_len =
(sizeof(node->long_name) < sizeof(long_name)) ? sizeof(node->long_name) : sizeof(long_name) - 1;
memcpy(long_name, node->long_name, copy_len);
long_name[copy_len] = '\0';
}
route->add(long_name); route->add(long_name);
}; };
@@ -418,4 +422,4 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa
return jsonStr; return jsonStr;
} }
#endif #endif
+1 -1
View File
@@ -113,7 +113,7 @@ void test_DH25519(void)
void test_PKC(void) void test_PKC(void)
{ {
uint8_t private_key[32]; uint8_t private_key[32];
meshtastic_UserLite_public_key_t public_key; meshtastic_NodeInfoLite_public_key_t public_key;
uint8_t expected_shared[32]; uint8_t expected_shared[32];
uint8_t expected_decrypted[32]; uint8_t expected_decrypted[32];
uint8_t radioBytes[128] __attribute__((__aligned__)); uint8_t radioBytes[128] __attribute__((__aligned__));
+12
View File
@@ -800,6 +800,17 @@ void test_disabled(void)
TEST_ASSERT_FALSE(mqtt->isEnabled()); TEST_ASSERT_FALSE(mqtt->isEnabled());
} }
void test_mqttInitSkipsAllocationWhenDisabled(void)
{
delete unitTest;
mqtt = unitTest = NULL;
moduleConfig.mqtt.enabled = false;
mqttInit();
TEST_ASSERT_NULL(mqtt);
}
// Subscriptions contain the moduleConfig.mqtt.root prefix. // Subscriptions contain the moduleConfig.mqtt.root prefix.
void test_customMqttRoot(void) void test_customMqttRoot(void)
{ {
@@ -912,6 +923,7 @@ void setup()
RUN_TEST(test_usingCustomServer); RUN_TEST(test_usingCustomServer);
RUN_TEST(test_enabled); RUN_TEST(test_enabled);
RUN_TEST(test_disabled); RUN_TEST(test_disabled);
RUN_TEST(test_mqttInitSkipsAllocationWhenDisabled);
RUN_TEST(test_customMqttRoot); RUN_TEST(test_customMqttRoot);
RUN_TEST(test_configEmptyIsValid); RUN_TEST(test_configEmptyIsValid);
RUN_TEST(test_configEnabledEmptyIsValid); RUN_TEST(test_configEnabledEmptyIsValid);
+6 -26
View File
@@ -641,30 +641,12 @@ void test_sender_zero_substituted(void)
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false)); TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
} }
void test_uninitialized_wasSeenRecently(void) // NOTE: Tests for the !initOk() short-circuit in wasSeenRecently / wasRelayer
{ // were removed: the only way to drive that branch from a test is to destruct
// Simulate uninitialized state — create a PacketHistory that looks uninitialized // the object and then call methods on it, which is UB and trips ASAN under
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one // `pio test -e coverage`. In practice the guard can only fire if `new[]` throws
PacketHistory h(4); // (in which case the constructor doesn't return), so the lost coverage is
TEST_ASSERT_TRUE(h.initOk()); // sanity check // purely defensive.
h.~PacketHistory();
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(h.wasSeenRecently(&p));
// Reconstruct in place to allow proper destruction
new (&h) PacketHistory(4);
}
void test_uninitialized_wasRelayer(void)
{
PacketHistory h(4);
h.~PacketHistory();
TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111));
new (&h) PacketHistory(4);
}
void test_multiple_instances_independent(void) void test_multiple_instances_independent(void)
{ {
@@ -819,8 +801,6 @@ void setup()
// Group 10 — Edge Cases // Group 10 — Edge Cases
RUN_TEST(test_packet_id_zero_not_stored); RUN_TEST(test_packet_id_zero_not_stored);
RUN_TEST(test_sender_zero_substituted); RUN_TEST(test_sender_zero_substituted);
RUN_TEST(test_uninitialized_wasSeenRecently);
RUN_TEST(test_uninitialized_wasRelayer);
RUN_TEST(test_multiple_instances_independent); RUN_TEST(test_multiple_instances_independent);
// Group 11 — Hash Table Stress // Group 11 — Hash Table Stress
+4 -4
View File
@@ -50,7 +50,7 @@ class MockNodeDB : public NodeDB
hasCachedNode = true; hasCachedNode = true;
cachedNodeNum = n; cachedNodeNum = n;
cachedNode.num = n; cachedNode.num = n;
cachedNode.has_user = true; cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
} }
private: private:
@@ -494,9 +494,9 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result)); TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
TEST_ASSERT_NOT_NULL(requestor); TEST_ASSERT_NOT_NULL(requestor);
TEST_ASSERT_TRUE(requestor->has_user); TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name);
TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name);
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
} }
+406
View File
@@ -0,0 +1,406 @@
// Tests for src/mesh/TypeConversions.cpp covering:
// - bitfield bit collapse on store + extraction round-trip
// - long_name / short_name truncation at the new max_size:25 / 5 boundaries
// - public_key / hw_model / role pass-through
// - thin vs bundled NodeInfo emission
//
// All exercised via the explicit-args overload of ConvertToNodeInfo so we don't
// touch the global nodeDB pointer (which isn't initialized in this test env).
#include "NodeDB.h"
#include "TestUtil.h"
#include "TypeConversions.h"
#include <cstdio>
#include <cstring>
#include <unity.h>
void setUp(void) {}
void tearDown(void) {}
// ---------- helpers -----------------------------------------------------------
static meshtastic_User makeUser(const char *longName, const char *shortName)
{
meshtastic_User u = meshtastic_User_init_zero;
if (longName)
strncpy(u.long_name, longName, sizeof(u.long_name) - 1);
if (shortName)
strncpy(u.short_name, shortName, sizeof(u.short_name) - 1);
u.hw_model = meshtastic_HardwareModel_TBEAM;
u.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
return u;
}
// ---------- has_user / id / macaddr ------------------------------------------
void test_copy_user_sets_has_user_bit(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Kevin Hester", "kh");
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
}
void test_convert_to_user_id_derived_from_nodenum(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0x12345678;
meshtastic_User u = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_STRING("!12345678", u.id);
}
void test_convert_to_user_zero_fills_macaddr(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xCAFEBABE;
meshtastic_User u = TypeConversions::ConvertToUser(&lite);
uint8_t zeros[sizeof(u.macaddr)] = {0};
TEST_ASSERT_EQUAL_MEMORY(zeros, u.macaddr, sizeof(u.macaddr));
}
// ---------- long_name truncation ---------------------------------------------
void test_long_name_short_passes_through(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Kevin Hester", "kh"); // 12 chars, fits
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING("Kevin Hester", lite.long_name);
}
void test_long_name_exact_24_fits(void)
{
// 24 chars -> stored as 24 chars + NUL inside char[25].
const char *exact24 = "abcdefghijklmnopqrstuvwx";
TEST_ASSERT_EQUAL_INT(24, (int)strlen(exact24));
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(exact24, "ex");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING(exact24, lite.long_name);
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
}
void test_long_name_truncates_when_too_long(void)
{
// 33 chars in, must fit in 24 + NUL.
const char *tooLong = "North-County Search & Rescue Base";
TEST_ASSERT_EQUAL_INT(33, (int)strlen(tooLong));
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(tooLong, "nc");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
TEST_ASSERT_EQUAL_STRING_LEN(tooLong, lite.long_name, 24);
TEST_ASSERT_EQUAL_INT('\0', lite.long_name[24]);
}
void test_long_name_round_trip_to_wire(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("Mountain Repeater Site E", "mr"); // exactly 24
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_STRING(in.long_name, out.long_name);
}
void test_long_name_truncated_utf8_boundary_sanitized(void)
{
// Suffix the 24th byte with the start of a 4-byte emoji; truncation should
// leave the dangling bytes for sanitizeUtf8 to replace with '?'.
char input[40] = {0};
memset(input, 'a', 22); // 22 ASCII
input[22] = static_cast<char>(0xF0); // emoji lead byte at position 22
input[23] = static_cast<char>(0x9F); // continuation
input[24] = static_cast<char>(0xA4); // continuation - past the cap
input[25] = static_cast<char>(0x96); // continuation - past the cap
input[26] = '\0';
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(input, "u8");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
// 24 bytes survive plus NUL. sanitizeUtf8 should have turned the dangling
// multi-byte head into '?' since its continuation bytes were chopped.
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
for (int i = 0; i < 22; ++i)
TEST_ASSERT_EQUAL_INT('a', lite.long_name[i]);
// The 4-byte sequence got truncated mid-codepoint; sanitizeUtf8 replaces
// any invalid lead/continuation byte with '?'.
TEST_ASSERT_EQUAL_INT('?', lite.long_name[22]);
TEST_ASSERT_EQUAL_INT('?', lite.long_name[23]);
}
// ---------- short_name truncation --------------------------------------------
void test_short_name_passes_through(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Test", "abcd"); // 4 chars, fits short_name[5]
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING("abcd", lite.short_name);
}
void test_short_name_truncates_when_too_long(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Test", "abcdefgh");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_INT(4, (int)strlen(lite.short_name));
TEST_ASSERT_EQUAL_INT('\0', lite.short_name[4]);
}
// ---------- bitfield collapse + round-trip per bool --------------------------
void test_bitfield_is_licensed_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.is_licensed = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.is_licensed);
in.is_licensed = false;
meshtastic_NodeInfoLite lite2 = meshtastic_NodeInfoLite_init_default;
TypeConversions::CopyUserToNodeInfoLite(&lite2, in);
TEST_ASSERT_FALSE((lite2.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0);
meshtastic_User out2 = TypeConversions::ConvertToUser(&lite2);
TEST_ASSERT_FALSE(out2.is_licensed);
}
void test_bitfield_unmessagable_present_and_true(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = true;
in.is_unmessagable = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.has_is_unmessagable);
TEST_ASSERT_TRUE(out.is_unmessagable);
}
void test_bitfield_unmessagable_present_but_false(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = true;
in.is_unmessagable = false;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.has_is_unmessagable);
TEST_ASSERT_FALSE(out.is_unmessagable);
}
void test_bitfield_unmessagable_absent(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = false;
in.is_unmessagable = true; // explicitly true to make sure absence still wins
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_FALSE(out.has_is_unmessagable);
TEST_ASSERT_FALSE(out.is_unmessagable);
}
void test_copy_user_preserves_unrelated_bits(void)
{
// Pre-set is_muted and is_key_manually_verified - CopyUserToNodeInfoLite
// must not stomp them.
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.bitfield = NODEINFO_BITFIELD_IS_MUTED_MASK | NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK |
NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK;
meshtastic_User in = makeUser("a", "a");
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK) != 0);
}
void test_bitfield_bits_are_independent(void)
{
// Set just is_licensed, verify only that bit (and HAS_USER) is set.
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.is_licensed = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
const uint32_t expected = NODEINFO_BITFIELD_HAS_USER_MASK | NODEINFO_BITFIELD_IS_LICENSED_MASK;
TEST_ASSERT_EQUAL_HEX32(expected, lite.bitfield);
}
// ---------- public_key / hw_model / role pass-through ------------------------
void test_public_key_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.public_key.size = 32;
for (int i = 0; i < 32; ++i)
in.public_key.bytes[i] = (uint8_t)(i ^ 0xA5);
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_EQUAL_INT(32, lite.public_key.size);
TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, lite.public_key.bytes, 32);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_INT(32, out.public_key.size);
TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, out.public_key.bytes, 32);
}
void test_hw_model_and_role_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.hw_model = meshtastic_HardwareModel_HELTEC_V3;
in.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, lite.hw_model);
TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, lite.role);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, out.hw_model);
TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, out.role);
}
// ---------- ConvertToNodeInfo (3-arg) ----------------------------------------
void test_convert_to_node_info_thin_omits_position_and_metrics(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfoThin(&lite);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
}
void test_convert_to_node_info_3arg_with_position(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_PositionLite pos = meshtastic_PositionLite_init_default;
pos.latitude_i = 374200000;
pos.longitude_i = -1221000000;
pos.altitude = 30;
pos.time = 12345;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, &pos, nullptr);
TEST_ASSERT_TRUE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
TEST_ASSERT_EQUAL_INT32(374200000, info.position.latitude_i);
TEST_ASSERT_EQUAL_INT32(-1221000000, info.position.longitude_i);
TEST_ASSERT_EQUAL_INT32(30, info.position.altitude);
TEST_ASSERT_EQUAL_UINT32(12345, info.position.time);
}
void test_convert_to_node_info_3arg_with_metrics(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_DeviceMetrics dm = meshtastic_DeviceMetrics_init_default;
dm.battery_level = 88;
dm.has_battery_level = true;
dm.voltage = 3.71f;
dm.has_voltage = true;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, &dm);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_TRUE(info.has_device_metrics);
TEST_ASSERT_EQUAL_INT(88, info.device_metrics.battery_level);
TEST_ASSERT_TRUE(info.device_metrics.has_battery_level);
TEST_ASSERT_EQUAL_FLOAT(3.71f, info.device_metrics.voltage);
}
void test_convert_to_node_info_3arg_null_inputs(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
}
void test_convert_to_node_info_extracts_bitfield_bools(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xBB;
lite.bitfield = NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK | NODEINFO_BITFIELD_IS_MUTED_MASK;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_TRUE(info.via_mqtt);
TEST_ASSERT_TRUE(info.is_favorite);
TEST_ASSERT_TRUE(info.is_ignored);
TEST_ASSERT_TRUE(info.is_key_manually_verified);
TEST_ASSERT_TRUE(info.is_muted);
}
void test_convert_to_node_info_extracts_bitfield_bools_none_set(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xBB;
lite.bitfield = 0;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.via_mqtt);
TEST_ASSERT_FALSE(info.is_favorite);
TEST_ASSERT_FALSE(info.is_ignored);
TEST_ASSERT_FALSE(info.is_key_manually_verified);
TEST_ASSERT_FALSE(info.is_muted);
}
void test_convert_to_node_info_user_only_when_has_user_bit_set(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0x01;
strcpy(lite.long_name, "Alpha");
strcpy(lite.short_name, "A");
// No HAS_USER bit -> user fields ignored on emit.
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.has_user);
lite.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
meshtastic_NodeInfo info2 = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_TRUE(info2.has_user);
TEST_ASSERT_EQUAL_STRING("Alpha", info2.user.long_name);
TEST_ASSERT_EQUAL_STRING("A", info2.user.short_name);
TEST_ASSERT_EQUAL_STRING("!00000001", info2.user.id);
}
// ---------- entry point -------------------------------------------------------
void setup()
{
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_copy_user_sets_has_user_bit);
RUN_TEST(test_convert_to_user_id_derived_from_nodenum);
RUN_TEST(test_convert_to_user_zero_fills_macaddr);
RUN_TEST(test_long_name_short_passes_through);
RUN_TEST(test_long_name_exact_24_fits);
RUN_TEST(test_long_name_truncates_when_too_long);
RUN_TEST(test_long_name_round_trip_to_wire);
RUN_TEST(test_long_name_truncated_utf8_boundary_sanitized);
RUN_TEST(test_short_name_passes_through);
RUN_TEST(test_short_name_truncates_when_too_long);
RUN_TEST(test_bitfield_is_licensed_round_trip);
RUN_TEST(test_bitfield_unmessagable_present_and_true);
RUN_TEST(test_bitfield_unmessagable_present_but_false);
RUN_TEST(test_bitfield_unmessagable_absent);
RUN_TEST(test_copy_user_preserves_unrelated_bits);
RUN_TEST(test_bitfield_bits_are_independent);
RUN_TEST(test_public_key_round_trip);
RUN_TEST(test_hw_model_and_role_round_trip);
RUN_TEST(test_convert_to_node_info_thin_omits_position_and_metrics);
RUN_TEST(test_convert_to_node_info_3arg_with_position);
RUN_TEST(test_convert_to_node_info_3arg_with_metrics);
RUN_TEST(test_convert_to_node_info_3arg_null_inputs);
RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools);
RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools_none_set);
RUN_TEST(test_convert_to_node_info_user_only_when_has_user_bit_set);
exit(UNITY_END());
}
void loop() {}
+1 -1
View File
@@ -12,4 +12,4 @@ build_flags =
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -71,7 +71,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@1.4.3 h2zero/NimBLE-Arduino@1.4.3
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip https://github.com/dbinfrago/libpax/archive/df424747f9acb86ab07c5a206ded1e8e3650726a.zip
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
lewisxhe/XPowersLib@0.3.3 lewisxhe/XPowersLib@0.3.3
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
+1 -1
View File
@@ -35,4 +35,4 @@ lib_ignore =
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -11,7 +11,7 @@ build_flags =
lib_deps = lib_deps =
${esp32_base.lib_deps} ${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
# renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander
sparkfun/SX1509 IO Expander@3.0.6 sparkfun/SX1509 IO Expander@3.0.6
# renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102
+1 -1
View File
@@ -133,6 +133,6 @@ build_flags =
lib_deps = ${heltec_v4_base.lib_deps} lib_deps = ${heltec_v4_base.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
+1 -1
View File
@@ -140,6 +140,6 @@ build_flags =
lib_deps = ${heltec_v4_r8_base.lib_deps} lib_deps = ${heltec_v4_r8_base.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/3b2b6cebf3177b3e2c33d06e07909b0b10159516.zip https://github.com/Quency-D/chsc6x/archive/3b2b6cebf3177b3e2c33d06e07909b0b10159516.zip
@@ -24,4 +24,4 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
@@ -22,4 +22,4 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
@@ -21,4 +21,4 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -55,7 +55,7 @@ lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
${device-ui_base.lib_deps} ${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
[mesh_tab_xpt2046] [mesh_tab_xpt2046]
extends = mesh_tab_base extends = mesh_tab_base
@@ -25,7 +25,7 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
build_src_filter = build_src_filter =
${esp32s3_base.build_src_filter} ${esp32s3_base.build_src_filter}
@@ -37,7 +37,7 @@ build_flags =
lib_deps = lib_deps =
${esp32s3_base.lib_deps} ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19 lovyan03/LovyanGFX@1.2.21
[env:rak_wismesh_tap_v2-tft] [env:rak_wismesh_tap_v2-tft]
extends = env:rak_wismesh_tap_v2 extends = env:rak_wismesh_tap_v2

Some files were not shown because too many files have changed in this diff Show More