Compare commits

...

59 Commits

Author SHA1 Message Date
Ben Meadors
e85cddef94 Flat NodeDB (NodeDetail) refactor hello world 2025-11-25 04:56:16 -06:00
simon-muzi
1bfa9ed4c4 Tweak OCV_ARRAY 100% voltage to take into account charger hysteresis and voltage sag after charge (#8720)
Measured voltage of fully charged battery after a few minutes of rest
2025-11-22 16:35:10 -06:00
Jonathan Bennett
b18794e98d Log error if startReceive fails in LR11x0Interface (#8718) 2025-11-22 13:54:24 -06:00
simon-muzi
f4e260e0f1 R1 Neo - Added OCV_ARRAY from measured discharge curve testing + update ADC multiplier (#8716)
* Added OCV_ARRAY from measured discharge curve testing and update ADC multiplier

The ADC resistor divider ratio is 0.6 -> multiplier should be 1/0.6 ~=1.667

We data logged a full discharge curve at constant 100mA draw over 15hours to get a realistic voltage curve for battery SoC measurements.

* Remove power.h in favor of variant.h

---------

Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2025-11-22 13:54:10 -06:00
Avi0n
14463043bd Add WisMesh Tag OCV array (#8646)
* Add WisMesh Tag OCV array

* Update 10% to 3650

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2025-11-22 12:03:47 -06:00
renovate[bot]
376dc7ef3a Update actions/checkout action to v6 (#8695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 08:27:00 -06:00
renovate[bot]
c051c56544 Update Kongduino-Adafruit_nRFCrypto digest to 8cde718 (#8708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 08:26:39 -06:00
Ben Meadors
d3976e7461 Merge pull request #8713 from meshtastic/develop
Develop to master
2025-11-22 08:23:46 -06:00
github-actions[bot]
a4c92d9fd5 Update protobufs (#8707)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
2025-11-21 20:31:12 +01:00
Ben Meadors
186cbe61bb Merge pull request #8705 from meshtastic/thinknode-m6
Add Thinknode M6
2025-11-21 05:48:16 -06:00
github-actions[bot]
0e3e8b7607 Update protobufs (#8707)
Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com>
2025-11-21 11:02:13 +01:00
Thomas Göttgens
451e52b541 fix some minor compiler warnings. Note: The 'delete' is actually safe, so we suppress the warning. 2025-11-21 10:42:15 +01:00
Thomas Göttgens
d743ba8e75 Add Thinknode M6 2025-11-21 10:14:06 +01:00
Ben Meadors
626dce8323 Merge pull request #8701 from jasonbcox/fix-exclude-pki-menuhandler
Fix MenuHandler when MESHTASTIC_EXCLUDE_PKI is defined
2025-11-20 17:07:21 -06:00
Jason B. Cox
0100eeea67 Fix MenuHandler when MESHTASTIC_EXCLUDE_PKI is defined 2025-11-20 14:20:18 -08:00
Ben Meadors
5640179ce2 Merge pull request #8698 from jasonbcox/fix-exclude-pki
Fix build when MESHTASTIC_EXCLUDE_PKI is defined
2025-11-20 15:15:01 -06:00
Jason B. Cox
066da492d5 Fix build when MESHTASTIC_EXCLUDE_PKI is defined 2025-11-20 12:26:07 -08:00
Ben Meadors
2b8806486d Merge pull request #8670 from SebKuzminsky/nrf52-watchdog-take-2
nrf52 watchdog (attempt #2)
2025-11-20 10:14:30 -06:00
Ben Meadors
9ae545918c Merge pull request #8694 from meshtastic/develop
Missed one
2025-11-20 09:57:44 -06:00
Ben Meadors
5374291c3c Merge pull request #8663 from meshtastic/master
Master --> develop
2025-11-20 09:57:01 -06:00
Ben Meadors
38b0c1588a Merge pull request #8689 from meshtastic/develop-to-master
Develop to master
2025-11-20 08:50:03 -06:00
Quency-D
f329de04c4 Add a reset pulse signal to the OLED. (#8691)
* Add a reset pulse signal to the OLED.

* The modification time is the same as that of the Adafruit_SSD1306 library.
2025-11-20 08:47:41 -06:00
Jason P
b09fa31492 Update src/graphics/draw/MenuHandler.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-20 08:07:11 -06:00
Ben Meadors
a2a0150ee8 Trunk fmt 2025-11-20 06:14:29 -06:00
Ben Meadors
9ae92724a9 Merge branch 'master' into develop-to-master 2025-11-20 05:58:57 -06:00
Jonathan Bennett
9cf369c5d0 actually respect wake_on_motion setting (#8690) 2025-11-20 05:41:32 -06:00
Ben Meadors
441a7c5b20 Merge branch 'master' into develop-to-master 2025-11-19 16:07:58 -06:00
renovate[bot]
2ca03fbf4b chore(deps): update meshtastic-esp8266-oled-ssd1306 digest to 2887bf4 (#8688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 16:02:33 -06:00
Austin
ef298814f2 CI: Submit Bump Version PR against master (#8668) 2025-11-19 16:00:13 -06:00
Jason B. Cox
8d31fc5ec6 Unify uptime formatting (#8677)
* Unify uptime formatting

* Fix small label alignment item

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
2025-11-19 15:59:45 -06:00
Sebastian Kuzminsky
7232dddd69 nrf52 wdt: pause wdt in Sleep and Halt, set timeout to 90 s
The 90 seconds wdt timeout matches the esp32 wdt timeout.
2025-11-18 11:52:08 -07:00
Sebastian Kuzminsky
10de230dba nrf52: add watchdog (#8485)
* nrf52: add watchdog

Main thread only for now.

* bump framework-arduinoadafruitnrf52 to pick up new wdt support

* clang-format the new parts of main-nrf52.cpp

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
(cherry picked from commit 83954293d8)
2025-11-18 11:52:08 -07:00
github-actions[bot]
567b8ea1c2 Automated version bumps (#8626)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-11-18 08:25:17 -06:00
omgbebebe
d39d1917ad mqtt: do not try to send packets when it disconnected (#8658) 2025-11-18 06:33:15 -06:00
Jason P
b202559d37 Add code for preserving favorites, also move to Home screen before reseting (#8647) 2025-11-18 06:33:02 -06:00
Tom
85ea22ac38 Update to Pro-micro variants (#8600)
* Update to Pro-micro variants

Schematic updated
Xtal variant removed
Extra module added to list
Extra explanation added to readme.

* Fix markdown formatting in readme.md

* Fix formatting in readme.md for RF switch section

---------

Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com>
2025-11-18 06:31:41 -06:00
Quency-D
15257b017c Add the Heltec v4 expansion box. (#8539)
* Add the Heltec v4 expansion box.

* Change heltec-v4-oled to heltec-v4.

* Add touchscreen to I2C scanning.

* Add reset and busy pins to the ST7789.

* Ignore the touch interrupt pin and extend the sleep time to 1 hour.

* Remove the default sleep function.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-18 06:31:06 -06:00
Jonathan Bennett
59864dd09d Add API types, state, and log message in Debug screen. Added persistent "Connected" icon (#8576)
* Add API types, state, and log message in Debug screen

* un-goober the API state tracking

* Set the SerialConsole api_type

* Add api_type for Ethernet

* Remove API state debugging code

* Update wording for client connection states

* Improve string width for smaller screen devices

* Reserve space on navigation bar to fit link indicator

* Add persistent Connected icon to screen

* Connect System frame to ensure text doesn't overflow

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2025-11-18 06:26:34 -06:00
Jason B. Cox
edcdb2dcb2 Cleanup unnecessary global dereferencing in CryptoEngine (#8611)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-18 06:25:30 -06:00
Ben Meadors
ef4cb2abfb If we're not client proxying and we are not connected, don't publish 2025-11-17 20:05:42 -06:00
omgbebebe
c34f94abda mqtt: do not try to send packets when it disconnected (#8658) 2025-11-17 20:01:50 -06:00
Ben Meadors
a8d1a90e16 Fix ble rssi crash (#8661)
* Fix BLE crash occuring when trying to get RSSI from Android with a bad connection handle

* Cleanup
2025-11-17 19:58:12 -06:00
Austin
501c296e75 Linux: Fix silly EPEL9 mistake (#8660) 2025-11-17 17:39:52 -06:00
Austin
ec5e79585b Don't trust the AI! (#8659)
Read the docs instead
2025-11-17 16:40:19 -06:00
Austin
438e170b03 Packaging: Add libbsd where needed (#8533) 2025-11-17 06:41:37 -06:00
renovate[bot]
43e0c35466 chore(deps): update dorny/test-reporter action to v2.2.0 (#8637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-16 20:00:05 -06:00
Jonathan Bennett
6e3be132f2 Reset the calibration data back to 0 when doing a compass calibration 2025-11-16 16:20:30 -06:00
Jonathan Bennett
0aa11d810c Clean up GPS toggle logging
Removed redundant log warnings for GPS toggle events.
2025-11-13 12:04:39 -06:00
github-actions[bot]
4df6627ab1 Upgrade trunk (#8606)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-11-12 05:33:31 -06:00
Ben Meadors
e9590003f4 Only call stopNow if we're nagging (#8601) 2025-11-10 11:58:39 -06:00
github-actions[bot]
1c0c6b2736 Automated version bumps (#8527)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-11-08 18:41:04 -06:00
Quency-D
602945f66b Add the Heltec v4 expansion box. (#8539)
* Add the Heltec v4 expansion box.

* Change heltec-v4-oled to heltec-v4.

* Add touchscreen to I2C scanning.

* Add reset and busy pins to the ST7789.

* Ignore the touch interrupt pin and extend the sleep time to 1 hour.

* Remove the default sleep function.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-08 06:50:20 -06:00
Benjamin Faershtein
b86827967e Drop PKI acks if there is no downlink on MQTTClientProxy (#8580)
* Discard everything if downlink isn't on

* Drop PKI packets when downlink not on
2025-11-08 06:00:38 -06:00
github-actions[bot]
b707001873 Upgrade trunk (#8552)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2025-11-07 05:33:54 -06:00
renovate[bot]
85afd706fd chore(deps): update meshtastic/device-ui digest to 28167c6 (#8583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 05:33:36 -06:00
Ben Meadors
e76013fb60 Try-fix traceroute panic (#8568) 2025-11-07 05:16:00 -06:00
Benjamin Faershtein
b25797e1b3 Discard everything if downlink isn't on (#8578) 2025-11-06 21:02:46 -06:00
Ford Jones
bdb3fb1477 Persist favourites on NodeDB reset (#8292)
* Conditionally delete favourited nodes on reset

* trunk fmt

* Fix equality check, use existing macro for role validation

* Extend favourite persistence setting to devices of all roles

* Refactor: Decoupled role/config check and set role defaults appropriately

* Use American-English spelling

* Use existing reference

* Convert reset to bool, regen protos

* Add optional arg to nodedb_reset in favor of additional device setting

* Use correct proto commit ID

* Regen protos

* Log preservation status

* Pull latest from master
2025-11-06 21:02:36 -06:00
Jonathan Bennett
7eca061f01 Bugfix: Don't toggle BLE when choosing active state (#8579) 2025-11-06 21:01:30 -06:00
107 changed files with 1854 additions and 768 deletions

View File

@@ -5,7 +5,7 @@ runs:
using: composite
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
path: meshtasticd

View File

@@ -22,7 +22,7 @@ jobs:
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}

View File

@@ -26,7 +26,7 @@ jobs:
setup:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -44,7 +44,7 @@ jobs:
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -108,7 +108,7 @@ jobs:
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}

View File

@@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -66,7 +66,7 @@ jobs:
if: ${{ inputs.target != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -114,7 +114,7 @@ jobs:
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}

View File

@@ -47,7 +47,7 @@ jobs:
runs-on: ${{ inputs.runs-on }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}

View File

@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{ github.ref }}

View File

@@ -35,7 +35,7 @@ jobs:
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -59,7 +59,7 @@ jobs:
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -81,7 +81,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build base
id: base
uses: ./.github/actions/setup-base
@@ -163,7 +163,7 @@ jobs:
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -242,7 +242,7 @@ jobs:
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
@@ -311,7 +311,7 @@ jobs:
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
@@ -366,7 +366,7 @@ jobs:
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6

View File

@@ -17,7 +17,7 @@ jobs:
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -40,7 +40,7 @@ jobs:
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build base
id: base
uses: ./.github/actions/setup-base
@@ -142,7 +142,7 @@ jobs:
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -221,7 +221,7 @@ jobs:
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
@@ -290,7 +290,7 @@ jobs:
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
@@ -345,7 +345,7 @@ jobs:
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Trunk Check
uses: trunk-io/trunk-action@v1
@@ -31,7 +31,7 @@ jobs:
pull-requests: write # For trunk to create PRs
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Trunk Upgrade
uses: trunk-io/trunk-action/upgrade@v1

View File

@@ -34,7 +34,7 @@ jobs:
needs: build-debian-src
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
path: meshtasticd

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}

View File

@@ -32,7 +32,7 @@ jobs:
needs: build-debian-src
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: recursive
path: meshtasticd

View File

@@ -40,7 +40,7 @@ jobs:
checks: write
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive

View File

@@ -60,7 +60,10 @@ jobs:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
# Always use master branch for version bumps
ref: master
- name: Setup Python
uses: actions/setup-python@v6

View File

@@ -21,7 +21,7 @@ jobs:
steps:
# step 1
- name: clone application source code
uses: actions/checkout@v5
uses: actions/checkout@v6
# step 2
- name: full scan

View File

@@ -13,7 +13,7 @@ jobs:
steps:
# step 1
- name: clone application source code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -14,7 +14,7 @@ jobs:
name: Native Simulator Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -70,7 +70,7 @@ jobs:
name: Native PlatformIO Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -127,7 +127,7 @@ jobs:
- platformio-tests
if: always()
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -143,7 +143,7 @@ jobs:
merge-multiple: true
- name: Test Report
uses: dorny/test-reporter@v2.1.1
uses: dorny/test-reporter@v2.2.0
with:
name: PlatformIO Tests
path: testreport.xml

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: test-runner
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
# - uses: actions/setup-python@v5
# with:

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Trunk Check
uses: trunk-io/trunk-action@v1

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Trunk Check
uses: trunk-io/trunk-action@v1

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}

View File

@@ -11,7 +11,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
submodules: true

View File

@@ -8,15 +8,15 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.489
- renovate@41.169.1
- checkov@3.2.492
- renovate@42.5.4
- prettier@3.6.2
- trufflehog@3.90.12
- trufflehog@3.90.13
- yamllint@1.37.1
- bandit@1.8.6
- trivy@0.67.2
- taplo@0.10.0
- ruff@0.14.3
- ruff@0.14.4
- isort@7.0.0
- markdownlint@0.45.0
- oxipng@9.1.5
@@ -26,9 +26,9 @@ lint:
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@25.9.0
- black@25.11.0
- git-diff-check
- gitleaks@8.28.0
- gitleaks@8.29.0
- clang-format@16.0.3
ignore:
- linters: [ALL]

View File

@@ -37,4 +37,3 @@ Join our community and help improve Meshtastic! 🚀
## Stats
![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image")

View File

@@ -7,7 +7,7 @@ extends = arduino_base
platform_packages =
; our custom Git version until they merge our PR
# TODO renovate
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#e13f5820002a4fb2a5e6754b42ace185277e5adf
platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#c770c8a16a351b55b86e347a3d9d7b74ad0bbf39
; Don't renovate toolchain-gccarmnoneeabi
platformio/toolchain-gccarmnoneeabi@~1.90301.0

View File

@@ -8,7 +8,7 @@ lib_deps =
${environmental_base.lib_deps}
${environmental_extra.lib_deps}
# renovate: datasource=git-refs depName=Kongduino-Adafruit_nRFCrypto packageName=https://github.com/Kongduino/Adafruit_nRFCrypto gitBranch=master
https://github.com/Kongduino/Adafruit_nRFCrypto/archive/5f838d2709461a2c981f642917aa50254a25c14c.zip
https://github.com/Kongduino/Adafruit_nRFCrypto/archive/8cde7189b5ead9dcd49f72601b43b969c0bbc06e.zip
; Common NRF52 debugging settings follow. See the Meshtastic developer docs for how to connect SWD debugging probes to your board.

53
boards/ThinkNode-M6.json Normal file
View File

@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M6 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m6",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M6",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "ELECROW ThinkNode M6",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.elecrow.com/thinknode-m6-outdoor-solar-power-for-lora-powered-by-nrf52840-supports-gps.html",
"vendor": "ELECROW"
}

1
debian/changelog vendored
View File

@@ -4,6 +4,7 @@ meshtasticd (2.7.16.0) unstable; urgency=medium
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 19 Nov 2025 16:12:32 +0000
meshtasticd (2.7.15.0) unstable; urgency=medium
* Version 2.7.15

View File

@@ -50,6 +50,13 @@ BuildRequires: pkgconfig(x11)
BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(xkbcommon-x11)
# libbsd is needed on older Fedora/RHEL to provide 'strlcpy'
%if 0%{?fedora} >= 39 || 0%{?rhel} >= 10
BuildRequires: glibc-devel >= 2.38
%else
BuildRequires: pkgconfig(libbsd-overlay)
%endif
Requires: systemd-udev
%description

View File

@@ -62,7 +62,7 @@ monitor_speed = 115200
monitor_filters = direct
lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2887bf4a19f64d92c984dcc8fd5ca7429e425e4a.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -121,7 +121,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/19b7855e9a1d9deff37391659ca7194e4ef57c43.zip
https://github.com/meshtastic/device-ui/archive/28167c67dfd13015a0b5eef1828f95fe8e3ab7c3.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]

View File

@@ -53,8 +53,8 @@ class GPSStatus : public Status
int32_t getLatitude() const
{
if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node->position.latitude_i;
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node ? node->latitude_i : 0;
} else {
return p.latitude_i;
}
@@ -63,8 +63,8 @@ class GPSStatus : public Status
int32_t getLongitude() const
{
if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node->position.longitude_i;
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node ? node->longitude_i : 0;
} else {
return p.longitude_i;
}
@@ -73,8 +73,8 @@ class GPSStatus : public Status
int32_t getAltitude() const
{
if (config.position.fixed_position) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node->position.altitude;
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
return node ? node->altitude : 0;
} else {
return p.altitude;
}

View File

@@ -1106,8 +1106,8 @@ void Screen::setFrames(FrameFocus focus)
std::vector<FrameCallback> favoriteFrames;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
const meshtastic_NodeDetail *nodeDetail = nodeDB->getMeshNodeByIndex(i);
if (nodeDetail && nodeDetail->num != nodeDB->getNodeNum() && detailIsFavorite(*nodeDetail)) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
}
}
@@ -1460,10 +1460,10 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
forceDisplay(); // Forces screen redraw
}
// === Prepare banner content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
const char *longName = (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER)) ? node->long_name : nullptr;
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);

View File

@@ -427,33 +427,35 @@ static LGFX *tft = nullptr;
#include "lgfx/v1/Touch.hpp"
namespace lgfx
{
inline namespace v1
{
inline namespace v1
{
class TOUCH_CHSC6X : public ITouch
{
public:
public:
TOUCH_CHSC6X(void)
{
_cfg.i2c_addr = TOUCH_SLAVE_ADDRESS;
_cfg.x_min = 0;
_cfg.x_max = 240;
_cfg.y_min = 0;
_cfg.y_max = 320;
_cfg.i2c_addr = TOUCH_SLAVE_ADDRESS;
_cfg.x_min = 0;
_cfg.x_max = 240;
_cfg.y_min = 0;
_cfg.y_max = 320;
};
bool init(void) override {
if(chsc6xTouch==nullptr) {
chsc6xTouch=new chsc6x(&Wire1,TOUCH_SDA_PIN,TOUCH_SCL_PIN,TOUCH_INT_PIN,TOUCH_RST_PIN);
bool init(void) override
{
if (chsc6xTouch == nullptr) {
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
}
chsc6xTouch->chsc6x_init();
return true;
};
uint_fast8_t getTouchRaw(touch_point_t* tp, uint_fast8_t count) override {
uint16_t raw_x,raw_y;
if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y)==0) {
tp[0].x = 320-1-raw_y;
tp[0].y = 240-1-raw_x ;
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override
{
uint16_t raw_x, raw_y;
if (chsc6xTouch->chsc6x_read_touch_info(&raw_x, &raw_y) == 0) {
tp[0].x = 320 - 1 - raw_y;
tp[0].y = 240 - 1 - raw_x;
tp[0].size = 1;
tp[0].id = 1;
return 1;
@@ -462,13 +464,14 @@ public:
return 0;
};
void wakeup(void) override {};
void sleep(void) override {};
void wakeup(void) override{};
void sleep(void) override{};
private:
chsc6x *chsc6xTouch=nullptr;
};
}
}
chsc6x *chsc6xTouch = nullptr;
};
} // namespace v1
} // namespace lgfx
#endif
class LGFX : public lgfx::LGFX_Device
{
@@ -513,9 +516,9 @@ class LGFX : public lgfx::LGFX_Device
{ // Set the display panel control.
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable)
cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable)
cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = ST7789_RESET; // Pin number where RST is connected (-1 = disable)
cfg.pin_busy = ST7789_BUSY; // Pin number where BUSY is connected (-1 = disable)
// The following setting values are general initial values for each panel, so please comment out any
// unknown items and try them.

View File

@@ -101,3 +101,23 @@ void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength)
else
snprintf(timeStr, maxLength, "unknown age");
}
void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs)
{
uint32_t days = uptimeMillis / 86400000;
uint32_t hours = (uptimeMillis % 86400000) / 3600000;
uint32_t mins = (uptimeMillis % 3600000) / 60000;
uint32_t secs = (uptimeMillis % 60000) / 1000;
if (days) {
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
} else if (hours) {
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
} else if (!includeSecs) {
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
} else if (mins) {
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
} else {
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
}
}

View File

@@ -24,3 +24,10 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int
* @param maxLength Maximum length of the resulting string buffer
*/
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength);
/**
* Get a compact human-readable string that only shows the largest non-zero time components.
* For example, 0 days 1 hour 2 minutes will display as "1h 2m" but 1 day 2 hours 3 minutes
* will display as "1d 2h".
*/
void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs = false);

View File

@@ -11,6 +11,7 @@
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
#include "main.h"
#include "mesh/Channels.h"
@@ -650,17 +651,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it
char uptimeStr[32] = "";
uint32_t uptime = millis() / 1000;
uint32_t days = uptime / 86400;
uint32_t hours = (uptime % 86400) / 3600;
uint32_t mins = (uptime % 3600) / 60;
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
if (days)
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours);
else if (hours)
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins);
else
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins);
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
textWidth = display->getStringWidth(uptimeStr);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], uptimeStr);
@@ -729,4 +720,4 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
} // namespace DebugRenderer
} // namespace graphics
#endif
#endif

View File

@@ -119,6 +119,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
auto changes = SEGMENT_CONFIG;
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
if (!owner.is_licensed) {
bool keygenSuccess = false;
if (config.security.private_key.size == 32) {
@@ -139,6 +140,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
}
}
#endif
config.lora.tx_enabled = true;
initRegion();
if (myRegion->dutyCycle < 100) {
@@ -935,7 +937,9 @@ void menuHandler::BluetoothToggleMenu()
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1 || selected == 2) {
if (selected == 0)
return;
else if (selected != (config.bluetooth.enabled ? 1 : 2)) {
InputEvent event = {.inputEvent = (input_broker_event)170, .kbchar = 170, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
@@ -1187,8 +1191,8 @@ void menuHandler::removeFavoriteMenu()
BannerOverlayOptions bannerOptions;
std::string message = "Unfavorite This Node?\n";
auto node = nodeDB->getMeshNode(graphics::UIRenderer::currentFavoriteNodeNum);
if (node && node->has_user) {
message += sanitizeString(node->user.long_name).substr(0, 15);
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER)) {
message += sanitizeString(node->long_name).substr(0, 15);
}
bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
@@ -1351,7 +1355,7 @@ void menuHandler::screenOptionsMenu()
optionsEnumArray[options++] = ScreenColor;
#endif
optionsArray[options] = "Frame Visiblity Toggle";
optionsArray[options] = "Frame Visibility Toggle";
optionsEnumArray[options++] = FrameToggles;
optionsArray[options] = "Display Units";
@@ -1455,8 +1459,10 @@ void menuHandler::keyVerificationFinalPrompt()
options.notificationType = graphics::notificationTypeEnum::selection_picker;
options.bannerCallback = [=](int selected) {
if (selected == 1) {
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
meshtastic_NodeDetail *remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
if (remoteNodePtr) {
detailSetFlag(*remoteNodePtr, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED, true);
}
}
};
screen->showOverlayBanner(options);
@@ -1748,4 +1754,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif

View File

@@ -218,18 +218,18 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
}
// === Header Construction ===
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(getFrom(&mp));
char headerStr[80];
const char *sender = "???";
#if defined(M5STACK_UNITC6L)
if (node && node->has_user)
sender = node->user.short_name;
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER))
sender = node->short_name;
#else
if (node && node->has_user) {
if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
sender = node->user.long_name;
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER)) {
if (SCREEN_WIDTH >= 200 && node->long_name[0] != '\0') {
sender = node->long_name;
} else {
sender = node->user.short_name;
sender = node->short_name;
}
}
#endif

View File

@@ -53,23 +53,29 @@ static int scrollIndex = 0;
// Utility Functions
// =============================
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
const char *getSafeNodeName(OLEDDisplay *display, const meshtastic_NodeDetail *node)
{
const char *name = NULL;
static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) {
if (node->has_user && strlen(node->user.long_name) > 0) {
name = node->user.long_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
if (!node) {
strncpy(nodeName, "?", sizeof(nodeName));
return nodeName;
}
const bool hasUser = detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER);
const char *name = nullptr;
if (config.display.use_long_node_name) {
if (hasUser && node->long_name[0] != '\0') {
name = node->long_name;
}
} else {
if (node->has_user && strlen(node->user.short_name) > 0) {
name = node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
if (hasUser && node->short_name[0] != '\0') {
name = node->short_name;
}
}
if (!name) {
snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node->num & 0xFFFF));
name = nodeName;
}
// Use sanitizeString() function and copy directly into nodeName
std::string sanitized_name = sanitizeString(name ? name : "");
@@ -164,7 +170,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
// Entry Renderers
// =============================
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
@@ -189,7 +195,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
if (node->is_favorite) {
if (node && detailIsFavorite(*node)) {
if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
@@ -204,7 +210,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawString(rightEdge - textWidth, y, timeStr);
}
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
@@ -220,7 +226,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (node && detailIsFavorite(*node)) {
if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
@@ -243,7 +249,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
// Draw hop count
char hopStr[6] = "";
if (node->has_hops_away && node->hops_away > 0)
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY) && node->hops_away > 0)
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
if (hopStr[0] != '\0') {
@@ -253,7 +259,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
}
}
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
@@ -261,12 +267,12 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
const char *nodeName = getSafeNodeName(display, node);
char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
double lat1 = ourNode->position.latitude_i * 1e-7;
double lon1 = ourNode->position.longitude_i * 1e-7;
double lat2 = node->position.latitude_i * 1e-7;
double lon2 = node->position.longitude_i * 1e-7;
double lat1 = ourNode->latitude_i * 1e-7;
double lon1 = ourNode->longitude_i * 1e-7;
double lat2 = node->latitude_i * 1e-7;
double lon2 = node->longitude_i * 1e-7;
double earthRadiusKm = 6371.0;
double dLat = (lat2 - lat1) * DEG_TO_RAD;
@@ -312,7 +318,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (node && detailIsFavorite(*node)) {
if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
@@ -329,7 +335,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
}
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth)
{
switch (currentMode) {
case MODE_LAST_HEARD:
@@ -346,7 +352,7 @@ void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
}
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth)
{
bool isLeftCol = (x < SCREEN_WIDTH / 2);
@@ -358,7 +364,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) {
if (node && detailIsFavorite(*node)) {
if (isHighResolution) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
} else {
@@ -367,7 +373,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
}
}
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth, float myHeading,
double userLat, double userLon)
{
if (!nodeDB->hasValidPosition(node))
@@ -379,8 +385,8 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
int centerX = x + columnWidth - arrowXOffset;
int centerY = y + FONT_HEIGHT_SMALL / 2;
double nodeLat = node->position.latitude_i * 1e-7;
double nodeLon = node->position.longitude_i * 1e-7;
double nodeLat = node->latitude_i * 1e-7;
double nodeLon = node->longitude_i * 1e-7;
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
float bearingToNode = RAD_TO_DEG * bearing;
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
@@ -466,16 +472,17 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int rowCount = 0;
for (int i = startIndex; i < endIndex; ++i) {
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
meshtastic_NodeDetail *candidate = nodeDB->getMeshNodeByIndex(i);
if (locationScreen && candidate && !detailHasFlag(*candidate, NODEDETAIL_FLAG_HAS_POSITION)) {
numskipped++;
continue;
}
int xPos = x + (col * columnWidth);
int yPos = y + yOffset;
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
renderer(display, candidate, xPos, yPos, columnWidth);
if (extras) {
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
extras(display, candidate, xPos, yPos, columnWidth, heading, lat, lon);
}
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
@@ -578,9 +585,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
{
float heading = 0;
bool validHeading = false;
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
double lat = DegD(ourNode->position.latitude_i);
double lon = DegD(ourNode->position.longitude_i);
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
return;
}
double lat = DegD(ourNode->latitude_i);
double lon = DegD(ourNode->longitude_i);
#if defined(M5STACK_UNITC6L)
display->clear();

View File

@@ -20,8 +20,8 @@ class Screen;
namespace NodeListRenderer
{
// Entry renderer function types
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeDetail *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeDetail *, int16_t, int16_t, int, float, double, double);
// Node list mode enumeration
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
@@ -32,14 +32,14 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
double lon = 0);
// Entry renderers
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth);
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth);
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth);
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeDetail *node, int16_t x, int16_t y, int columnWidth, float myHeading,
double userLat, double userLon);
// Screen frame functions
@@ -51,7 +51,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
// Utility functions
const char *getCurrentModeTitle(int screenWidth);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
const char *getSafeNodeName(OLEDDisplay *display, const meshtastic_NodeDetail *node);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Bitmap drawing function

View File

@@ -300,14 +300,17 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
int scratchLineNum = 0;
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char temp_name[16] = {0};
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
meshtastic_NodeDetail *candidate = nodeDB->getMeshNodeByIndex(i + 1);
if (candidate && detailHasFlag(*candidate, NODEDETAIL_FLAG_HAS_USER) && candidate->long_name[0] != '\0') {
std::string sanitized = sanitizeString(candidate->long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else if (candidate) {
snprintf(temp_name, sizeof(temp_name), "(%04X)", static_cast<uint16_t>(candidate->num & 0xFFFF));
} else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
strncpy(temp_name, "(?)", sizeof(temp_name) - 1);
}
if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (candidate && i == curSelected) {
selectedNodenum = candidate->num;
if (isHighResolution) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);

View File

@@ -11,6 +11,7 @@
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
#include "main.h"
#include "target_specific.h"
@@ -26,22 +27,22 @@ static uint32_t lastSwitchTime = 0;
namespace graphics
{
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
std::vector<meshtastic_NodeDetail *> graphics::UIRenderer::favoritedNodes;
void graphics::UIRenderer::rebuildFavoritedNodes()
{
favoritedNodes.clear();
size_t total = nodeDB->getNumMeshNodes();
for (size_t i = 0; i < total; i++) {
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
meshtastic_NodeDetail *n = nodeDB->getMeshNodeByIndex(i);
if (!n || n->num == nodeDB->getNodeNum())
continue;
if (n->is_favorite)
if (detailIsFavorite(*n))
favoritedNodes.push_back(n);
}
std::sort(favoritedNodes.begin(), favoritedNodes.end(),
[](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; });
[](const meshtastic_NodeDetail *a, const meshtastic_NodeDetail *b) { return a->num < b->num; });
}
#if !MESHTASTIC_EXCLUDE_GPS
@@ -288,8 +289,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size())
return;
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
meshtastic_NodeDetail *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !detailIsFavorite(*node))
return;
display->clear();
#if defined(M5STACK_UNITC6L)
@@ -302,7 +303,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
#endif
currentFavoriteNodeNum = node->num;
// === Create the shortName and title string ===
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
const bool hasUser = detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER);
const char *shortNameCandidate = (hasUser && node->short_name[0] != '\0') ? node->short_name : nullptr;
const char *shortName = (shortNameCandidate && haveGlyphs(shortNameCandidate)) ? shortNameCandidate : "Node";
char titlestr[32] = {0};
snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName);
@@ -320,9 +323,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
std::string usernameStr;
// === 1. Long Name (always try to show first) ===
#if defined(M5STACK_UNITC6L)
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
const char *username = (hasUser && node->long_name[0]) ? node->short_name : nullptr;
#else
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
const char *username = (hasUser && node->long_name[0]) ? node->long_name : nullptr;
#endif
if (username) {
@@ -348,7 +351,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
haveSignal = true;
}
// If hops is valid (>0), show right after signal
if (node->hops_away > 0) {
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY) && node->hops_away > 0) {
size_t len = strlen(signalHopsStr);
// Decide between "1 Hop" and "N Hops"
if (haveSignal) {
@@ -382,33 +385,23 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
#if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = "";
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
uint32_t uptime = node->device_metrics.uptime_seconds;
uint32_t days = uptime / 86400;
uint32_t hours = (uptime % 86400) / 3600;
uint32_t mins = (uptime % 3600) / 60;
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
if (days)
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours);
else if (hours)
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins);
else
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins);
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_UPTIME)) {
getUptimeStr(node->uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr));
}
if (uptimeStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], uptimeStr);
}
// === 5. Distance (only if both nodes have GPS position) ===
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
char distStr[24] = ""; // Make buffer big enough for any string
bool haveDistance = false;
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
double lat1 = ourNode->position.latitude_i * 1e-7;
double lon1 = ourNode->position.longitude_i * 1e-7;
double lat2 = node->position.latitude_i * 1e-7;
double lon2 = node->position.longitude_i * 1e-7;
double lat1 = ourNode->latitude_i * 1e-7;
double lon1 = ourNode->longitude_i * 1e-7;
double lat2 = node->latitude_i * 1e-7;
double lon2 = node->longitude_i * 1e-7;
double earthRadiusKm = 6371.0;
double dLat = (lat2 - lat1) * DEG_TO_RAD;
double dLon = (lon2 - lon1) * DEG_TO_RAD;
@@ -466,6 +459,10 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
showCompass = true;
}
if (showCompass) {
const double ourLatDeg = DegD(ourNode->latitude_i);
const double ourLonDeg = DegD(ourNode->longitude_i);
const double nodeLatDeg = DegD(node->latitude_i);
const double nodeLonDeg = DegD(node->longitude_i);
const int16_t topY = getTextPositions(display)[1];
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
const int16_t usableHeight = bottomY - topY - 5;
@@ -476,16 +473,10 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
const auto &op = ourNode->position;
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
float myHeading =
screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(ourLatDeg, ourLonDeg);
const auto &p = node->position;
/* unused
float d =
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
float bearing = GeoCoord::bearing(ourLatDeg, ourLonDeg, nodeLatDeg, nodeLonDeg);
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = 0;
} else {
@@ -529,20 +520,21 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
int compassX = x + SCREEN_WIDTH / 2;
int compassY = yBelowContent + availableHeight / 2;
const auto &op = ourNode->position;
const double ourLatDeg = DegD(ourNode->latitude_i);
const double ourLonDeg = DegD(ourNode->longitude_i);
float myHeading = 0;
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
myHeading =
screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(ourLatDeg, ourLonDeg);
}
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
const auto &p = node->position;
const double nodeLatDeg = DegD(node->latitude_i);
const double nodeLonDeg = DegD(node->longitude_i);
/* unused
float d =
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
float d = GeoCoord::latLongToMeter(nodeLatDeg, nodeLonDeg, ourLatDeg, ourLonDeg);
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
float bearing = GeoCoord::bearing(ourLatDeg, ourLonDeg, nodeLatDeg, nodeLonDeg);
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
bearing -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
@@ -564,7 +556,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// === Header ===
#if defined(M5STACK_UNITC6L)
@@ -592,18 +584,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
#endif
char uptimeStr[32] = "";
uint32_t uptime = millis() / 1000;
uint32_t days = uptime / 86400;
uint32_t hours = (uptime % 86400) / 3600;
uint32_t mins = (uptime % 3600) / 60;
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
#if !defined(M5STACK_UNITC6L)
if (days)
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours);
else if (hours)
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins);
else
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins);
getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr));
#endif
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
@@ -742,8 +724,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int yOffset = (isHighResolution) ? 0 : 5;
std::string longNameStr;
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
longNameStr = sanitizeString(ourNode->user.long_name);
if (ourNode && detailHasFlag(*ourNode, NODEDETAIL_FLAG_HAS_USER) && ourNode->long_name[0] != '\0') {
longNameStr = sanitizeString(ourNode->long_name);
}
char shortnameble[35];
snprintf(shortnameble, sizeof(shortnameble), "%s",
@@ -1000,8 +982,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
if (config.position.fixed_position) {
displayLine = "Fixed GPS";
@@ -1048,36 +1028,17 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
// === Second Row: Last GPS Fix ===
if (gpsStatus->getLastFixMillis() > 0) {
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
uint32_t days = delta / 86400;
uint32_t hours = (delta % 86400) / 3600;
uint32_t mins = (delta % 3600) / 60;
uint32_t secs = delta % 60;
char buf[32];
uint32_t delta = millis() - gpsStatus->getLastFixMillis();
char uptimeStr[32];
#if defined(USE_EINK)
// E-Ink: skip seconds, show only days/hours/mins
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else {
snprintf(buf, sizeof(buf), "Last: %um", mins);
}
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false);
#else
// Non E-Ink: include seconds where useful
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else if (mins > 0) {
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
} else {
snprintf(buf, sizeof(buf), "Last: %us", secs);
}
getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true);
#endif
display->drawString(0, getTextPositions(display)[line++], buf);
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
} else {
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
}
@@ -1422,4 +1383,4 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi
} // namespace graphics
#endif // HAS_SCREEN
#endif // HAS_SCREEN

View File

@@ -63,7 +63,7 @@ class UIRenderer
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static NodeNum currentFavoriteNodeNum;
static std::vector<meshtastic_NodeInfoLite *> favoritedNodes;
static std::vector<meshtastic_NodeDetail *> favoritedNodes;
static void rebuildFavoritedNodes();
// OEM screens

View File

@@ -5,6 +5,7 @@
#include "main.h"
#include "RTC.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
@@ -333,13 +334,13 @@ std::string InkHUD::Applet::parse(std::string text)
// Get the best version of a node's short name available to us
// Parses any non-ascii chars
// Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji)
std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node)
std::string InkHUD::Applet::parseShortName(meshtastic_NodeDetail *node)
{
assert(node);
// Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.)
if (node->has_user) {
std::string parsed = parse(node->user.short_name);
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER) && node->short_name[0] != '\0') {
std::string parsed = parse(node->short_name);
if (isPrintable(parsed))
return parsed;
}
@@ -640,7 +641,9 @@ uint16_t InkHUD::Applet::getActiveNodeCount()
// For each node in db
for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
const meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
if (!node)
continue;
// Check if heard recently, and not our own node
if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum())

View File

@@ -16,6 +16,7 @@
#include <GFX.h> // GFXRoot drawing lib
#include "mesh/MeshTypes.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "./AppletFont.h"
#include "./Applets/System/Notification/Notification.h" // The notification object, not the applet
@@ -133,15 +134,15 @@ class Applet : public GFX
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height,
Color color = BLACK); // Draw the Meshtastic logo
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
std::string parse(std::string text); // Handle text which might contain special chars
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
bool isPrintable(std::string); // Check for characters which the font can't print
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
std::string getTimeString(uint32_t epochSeconds); // Human readable
std::string getTimeString(); // Current time, human readable
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
std::string parse(std::string text); // Handle text which might contain special chars
std::string parseShortName(meshtastic_NodeDetail *node); // Get the shortname, or a substitute if has unprintable chars
bool isPrintable(std::string); // Check for characters which the font can't print
// Convenient references

View File

@@ -2,6 +2,8 @@
#include "./MapApplet.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
void InkHUD::MapApplet::onRender()
@@ -136,9 +138,9 @@ void InkHUD::MapApplet::onRender()
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
// Draw our node LAST with full white fill + outline
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
Marker self = calculateMarker(ourNode->latitude_i * 1e-7f, ourNode->longitude_i * 1e-7f, false, 0);
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
@@ -166,10 +168,10 @@ void InkHUD::MapApplet::onRender()
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
{
// If we have a valid position for our own node, use that as the anchor
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
*lat = ourNode->position.latitude_i * 1e-7;
*lng = ourNode->position.longitude_i * 1e-7;
*lat = ourNode->latitude_i * 1e-7f;
*lng = ourNode->longitude_i * 1e-7f;
} else {
// Find mean lat long coords
// ============================
@@ -189,7 +191,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
// For each node in db
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
@@ -200,8 +202,8 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
continue;
// Latitude and Longitude of node, in radians
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
float latRad = node->latitude_i * (1e-7f) * DEG_TO_RAD;
float lngRad = node->longitude_i * (1e-7f) * DEG_TO_RAD;
// Convert to cartesian points, with center of earth at 0, 0, 0
// Exact distance from center is irrelevant, as we're only interested in the vector
@@ -288,7 +290,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
float westernmost = lngCenter;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
@@ -299,12 +301,12 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
continue;
// Check for a new top or bottom latitude
float latNode = node->position.latitude_i * 1e-7;
float latNode = node->latitude_i * 1e-7f;
northernmost = max(northernmost, latNode);
southernmost = min(southernmost, latNode);
// Longitude is trickier
float lngNode = node->position.longitude_i * 1e-7;
float lngNode = node->longitude_i * 1e-7f;
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
if (degEastward < degWestward)
@@ -366,14 +368,14 @@ InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float ln
return m;
}
// Draw a marker on the map for a node, with a shortname label, and backing box
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeDetail *node)
{
// Find x and y position based on node's position in nodeDB
assert(nodeDB->hasValidPosition(node));
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
Marker m = calculateMarker(node->latitude_i * 1e-7f, // Lat, converted from Meshtastic's internal int32 style
node->longitude_i * 1e-7f, // Long, converted from Meshtastic's internal int32 style
detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY), // Is the hopsAway number valid
node->hops_away // Hops away
);
// Convert to pixel coords
@@ -396,9 +398,9 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
uint16_t labelH;
uint8_t markerSize;
bool tooManyHops = node->hops_away > config.lora.hop_limit;
bool tooManyHops = detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY) && node->hops_away > config.lora.hop_limit;
bool isOurNode = node->num == nodeDB->getNodeNum();
bool unknownHops = !node->has_hops_away && !isOurNode;
bool unknownHops = !detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY) && !isOurNode;
// Parse any non-ascii chars in the short name,
// and use last 4 instead if unknown / can't render
@@ -476,7 +478,7 @@ bool InkHUD::MapApplet::enoughMarkers()
{
size_t count = 0;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
// Count nodes
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
@@ -499,7 +501,7 @@ void InkHUD::MapApplet::calculateAllMarkers()
// For each node in db
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
// Skip if no position
if (!nodeDB->hasValidPosition(node))
@@ -515,12 +517,11 @@ void InkHUD::MapApplet::calculateAllMarkers()
continue;
// Calculate marker and store it
markers.push_back(
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
node->has_hops_away, // Is the hopsAway number valid
node->hops_away // Hops away
));
markers.push_back(calculateMarker(node->latitude_i * 1e-7f, // Lat, converted from Meshtastic's internal int32 style
node->longitude_i * 1e-7f, // Long, converted from Meshtastic's internal int32 style
detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY), // Is the hopsAway number valid
node->hops_away // Hops away
));
}
}

View File

@@ -30,12 +30,12 @@ class MapApplet : public Applet
void onRender() override;
protected:
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
virtual bool shouldDrawNode(meshtastic_NodeDetail *node) { return true; } // Allow derived applets to filter the nodes
virtual void getMapCenter(float *lat, float *lng);
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
bool enoughMarkers(); // Anything to draw?
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
bool enoughMarkers(); // Anything to draw?
void drawLabeledMarker(meshtastic_NodeDetail *node); // Highlight a specific marker
private:
// Position and size of a marker to be drawn

View File

@@ -3,7 +3,7 @@
#include "RTC.h"
#include "GeoCoord.h"
#include "NodeDB.h"
#include "mesh/NodeDB.h"
#include "./NodeListApplet.h"
@@ -50,19 +50,19 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
// Assemble info: from nodeDB (needed to detect changes)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *node = nodeDB->getMeshNode(c.nodeNum);
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node) {
if (node->has_hops_away)
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY))
c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float
// Meshtastic stores these as integers internally
float ourLat = ourNode->position.latitude_i * 1e-7;
float ourLong = ourNode->position.longitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7;
float theirLong = node->position.longitude_i * 1e-7;
float ourLat = ourNode->latitude_i * 1e-7f;
float ourLong = ourNode->longitude_i * 1e-7f;
float theirLat = node->latitude_i * 1e-7f;
float theirLong = node->longitude_i * 1e-7f;
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
}
@@ -144,7 +144,7 @@ void InkHUD::NodeListApplet::onRender()
std::string distance; // handled below;
uint8_t &hopsAway = card->hopsAway;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeNum);
// Skip deleted nodes
if (!node) {
@@ -162,8 +162,8 @@ void InkHUD::NodeListApplet::onRender()
// -- Longname --
// Parse special chars in long name
// Use node id if unknown
if (node && node->has_user)
longName = parse(node->user.long_name); // Found in nodeDB
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER))
longName = parse(node->long_name); // Found in nodeDB
else {
// Not found in nodeDB, show a hex nodeid instead
longName = hexifyNodeNum(nodeNum);

View File

@@ -14,10 +14,10 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
// During onboarding, show the default short name as well as the version string
// This behavior assists manufacturers during mass production, and should not be modified without good reason
if (!settings->tips.safeShutdownSeen) {
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
fontTitle = fontMedium;
textLeft = xstr(APP_VERSION_SHORT);
textRight = parseShortName(ourNode);
textRight = ourNode ? parseShortName(ourNode) : "";
textTitle = "Meshtastic";
} else {
fontTitle = fontSmall;
@@ -146,10 +146,10 @@ void InkHUD::LogoApplet::onShutdown()
// Prepare for the powered-off screen now
// We can change these values because the initial "shutting down" screen has already rendered at this point
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
textLeft = "";
textRight = "";
textTitle = parseShortName(ourNode);
textTitle = ourNode ? parseShortName(ourNode) : "";
fontTitle = fontMedium;
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete

View File

@@ -8,6 +8,7 @@
#include "Router.h"
#include "airtime.h"
#include "main.h"
#include "mesh/NodeDB.h"
#include "power.h"
#if !MESHTASTIC_EXCLUDE_GPS
@@ -616,7 +617,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Count favorites
for (uint32_t i = 0; i < nodeCount; i++) {
if (nodeDB->getMeshNodeByIndex(i)->is_favorite)
const meshtastic_NodeDetail *detail = nodeDB->getMeshNodeByIndex(i);
if (detail && detailIsFavorite(*detail))
favoriteCount++;
}
@@ -624,10 +626,12 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Don't want some monstrous list that takes 100 clicks to reach exit
if (favoriteCount < 20) {
for (uint32_t i = 0; i < nodeCount; i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
const meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
if (!node)
continue;
// Skip node if not a favorite
if (!node->is_favorite)
if (!detailIsFavorite(*node))
continue;
CannedMessages::RecipientItem r;
@@ -637,8 +641,8 @@ void InkHUD::MenuApplet::populateRecipientPage()
// Set a label for the menu item
r.label = "DM: ";
if (node->has_user)
r.label += parse(node->user.long_name);
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER) && node->long_name[0] != '\0')
r.label += parse(node->long_name);
else
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?

View File

@@ -5,6 +5,7 @@
#include "./Notification.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "mesh/NodeDB.h"
#include "meshUtils.h"
#include "modules/TextMessageModule.h"
@@ -206,13 +207,13 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(message->sender);
// Leading tag (channel vs. DM)
text += isBroadcast ? "From:" : "DM: ";
// Sender id
if (node && node->has_user)
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER))
text += parseShortName(node);
else
text += hexifyNodeNum(message->sender);
@@ -226,7 +227,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += isBroadcast ? "Msg from " : "DM from ";
// Sender id
if (node && node->has_user)
if (node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER))
text += parseShortName(node);
else
text += hexifyNodeNum(message->sender);

View File

@@ -69,11 +69,11 @@ void InkHUD::AllMessageApplet::onRender()
// Sender's id
// - short name and long name, if available, or
// - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
if (sender && sender->has_user) {
meshtastic_NodeDetail *sender = nodeDB->getMeshNode(message->sender);
if (sender && detailHasFlag(*sender, NODEDETAIL_FLAG_HAS_USER)) {
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " (";
header += parse(sender->user.long_name);
header += parse(sender->long_name);
header += ")";
} else
header += hexifyNodeNum(message->sender);

View File

@@ -65,11 +65,11 @@ void InkHUD::DMApplet::onRender()
// Sender's id
// - shortname and long name, if available, or
// - node id
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
if (sender && sender->has_user) {
meshtastic_NodeDetail *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
if (sender && detailHasFlag(*sender, NODEDETAIL_FLAG_HAS_USER)) {
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
header += " (";
header += parse(sender->user.long_name);
header += parse(sender->long_name);
header += ")";
} else
header += hexifyNodeNum(latestMessage->dm.sender);

View File

@@ -6,6 +6,8 @@
#include "./HeardApplet.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
void InkHUD::HeardApplet::onActivate()
@@ -61,7 +63,7 @@ void InkHUD::HeardApplet::handleParsed(CardInfo c)
void InkHUD::HeardApplet::populateFromNodeDB()
{
// Fill a collection with pointers to each node in db
std::vector<meshtastic_NodeInfoLite *> ordered;
std::vector<meshtastic_NodeDetail *> ordered;
for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) {
// Only copy if valid, and not our own node
if (mn->num != 0 && mn->num != nodeDB->getNodeNum())
@@ -69,7 +71,7 @@ void InkHUD::HeardApplet::populateFromNodeDB()
}
// Sort the collection by age
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
std::sort(ordered.begin(), ordered.end(), [](const meshtastic_NodeDetail *top, const meshtastic_NodeDetail *bottom) -> bool {
return (top->last_heard > bottom->last_heard);
});
@@ -79,21 +81,21 @@ void InkHUD::HeardApplet::populateFromNodeDB()
ordered.resize(maxCards());
// Create card info for these (stale) node observations
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
for (meshtastic_NodeInfoLite *node : ordered) {
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
for (meshtastic_NodeDetail *node : ordered) {
CardInfo c;
c.nodeNum = node->num;
if (node->has_hops_away)
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_HOPS_AWAY))
c.hopsAway = node->hops_away;
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
// Get lat and long as float
// Meshtastic stores these as integers internally
float ourLat = ourNode->position.latitude_i * 1e-7;
float ourLong = ourNode->position.longitude_i * 1e-7;
float theirLat = node->position.latitude_i * 1e-7;
float theirLong = node->position.longitude_i * 1e-7;
float ourLat = ourNode->latitude_i * 1e-7f;
float ourLong = ourNode->longitude_i * 1e-7f;
float theirLat = node->latitude_i * 1e-7f;
float theirLong = node->longitude_i * 1e-7f;
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
}

View File

@@ -14,9 +14,9 @@ void InkHUD::PositionsApplet::onRender()
// We might be rendering because we got a position packet from them
// We might be rendering because our own position updated
// Either way, we still highlight which node most recently sent us a position packet
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(lastFrom);
if (node && nodeDB->hasValidPosition(node) && enoughMarkers())
drawLabeledMarker(node);
drawLabeledMarker(const_cast<meshtastic_NodeDetail *>(node));
}
// Determine if we need to redraw the map, when we receive a new position packet

View File

@@ -108,8 +108,8 @@ void InkHUD::ThreadedMessageApplet::onRender()
info += "Me";
else {
// Check if sender is node db
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
if (sender)
meshtastic_NodeDetail *sender = nodeDB->getMeshNode(m.sender);
if (sender && detailHasFlag(*sender, NODEDETAIL_FLAG_HAS_USER))
info += parseShortName(sender); // Handle any unprintable chars in short name
else
info += hexifyNodeNum(m.sender); // No node info at all. Print the node num

View File

@@ -5,7 +5,6 @@ A pattern / collection of resources for creating custom UIs, to target small gro
For an example, see the `heltec-vision-master-e290-inkhud` platformio env.
- platformio.ini
- suppress default Meshtastic components (Screen, ButtonThread, etc)
- define `MESHTASTIC_INCLUDE_NICHE_GRAPHICS`
- (possibly) Edit `build_src_filter` to include our new nicheGraphics.h file

View File

@@ -52,7 +52,7 @@ int InputBroker::handleInputEvent(const InputEvent *event)
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule &&
moduleConfig.external_notification.enabled) {
moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) {
externalNotificationModule->stopNow();
}

View File

@@ -477,6 +477,10 @@ void setup()
#ifdef RESET_OLED
pinMode(RESET_OLED, OUTPUT);
digitalWrite(RESET_OLED, 1);
delay(2);
digitalWrite(RESET_OLED, 0);
delay(10);
digitalWrite(RESET_OLED, 1);
#endif
#ifdef SENSOR_POWER_CTRL_PIN
@@ -959,6 +963,7 @@ void setup()
i2cScanner.reset();
#endif
#if !defined(MESHTASTIC_EXCLUDE_PKI)
// warn the user about a low entropy key
if (nodeDB->keyIsLowEntropy && !nodeDB->hasWarned) {
LOG_WARN(LOW_ENTROPY_WARNING);
@@ -969,6 +974,7 @@ void setup()
service->sendClientNotification(cn);
nodeDB->hasWarned = true;
}
#endif
// buttons are now inputBroker, so have to come after setupModules
#if HAS_BUTTON

View File

@@ -244,6 +244,8 @@ template <typename T> void LR11x0Interface<T>::startReceive()
// We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly.
int err =
lora.startReceive(RADIOLIB_LR11X0_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();
@@ -304,4 +306,4 @@ template <typename T> bool LR11x0Interface<T>::sleep()
return true;
}
#endif
#endif

View File

@@ -86,12 +86,15 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio
bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
meshtastic_NodeDetail *fromNode = nodeDB->getMeshNode(mp->from);
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");
// ignore our request for its NodeInfo
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user &&
nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) {
} else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && fromNode &&
!detailHasFlag(*fromNode, NODEDETAIL_FLAG_HAS_USER) && nodeInfoModule && !isPreferredRebroadcaster &&
!nodeDB->isFull()) {
if (airTime->isTxAllowedChannelUtil(true)) {
// Hops used by the request. If somebody in between running modified firmware modified it, ignore it
auto hopStart = mp->hop_start;
@@ -269,7 +272,7 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
{
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
assert(node);
@@ -376,24 +379,25 @@ void MeshService::sendClientNotification(meshtastic_ClientNotification *n)
fromNum++;
}
meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
meshtastic_NodeDetail *MeshService::refreshLocalMeshNode()
{
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
assert(node);
// We might not have a position yet for our local node, in that case, at least try to send the time
if (!node->has_position) {
memset(&node->position, 0, sizeof(node->position));
node->has_position = true;
// Ensure we have a position container so time fields stay fresh even without GPS fixes
if (!detailHasFlag(*node, NODEDETAIL_FLAG_HAS_POSITION)) {
detailSetFlag(*node, NODEDETAIL_FLAG_HAS_POSITION, true);
node->latitude_i = 0;
node->longitude_i = 0;
node->altitude = 0;
node->position_source = _meshtastic_Position_LocSource_MIN;
}
meshtastic_PositionLite &position = node->position;
// Update our local node info with our time (even if we don't decide to update anyone else)
node->last_heard =
getValidTime(RTCQualityFromNet); // This nodedb timestamp might be stale, so update it if our clock is kinda valid
position.time = getValidTime(RTCQualityFromNet);
node->position_time = getValidTime(RTCQualityFromNet);
if (powerStatus->getHasBattery() == 1) {
updateBatteryLevel(powerStatus->getBatteryChargePercent());
@@ -406,7 +410,7 @@ meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode()
int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
{
// Update our local node info with our position (even if we don't decide to update anyone else)
const meshtastic_NodeInfoLite *node = refreshLocalMeshNode();
const meshtastic_NodeDetail *node = refreshLocalMeshNode();
meshtastic_Position pos = meshtastic_Position_init_default;
if (newStatus->getHasLock()) {
@@ -421,7 +425,7 @@ int MeshService::onGPSChanged(const meshtastic::GPSStatus *newStatus)
// Used fixed position if configured regardless of GPS lock
if (config.position.fixed_position) {
LOG_WARN("Use fixed position");
pos = TypeConversions::ConvertToPosition(node->position);
pos = TypeConversions::ConvertToPosition(detailToPositionLite(*node));
}
// Add a fresh timestamp

View File

@@ -170,7 +170,7 @@ class MeshService
bool cancelSending(PacketId id);
/// Pull the latest power and time info into my nodeinfo
meshtastic_NodeInfoLite *refreshLocalMeshNode();
meshtastic_NodeDetail *refreshLocalMeshNode();
/// Send a packet to the phone
void sendToPhone(meshtastic_MeshPacket *p);

View File

@@ -94,7 +94,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
// is not 0 (means implicit ACK) and original packet was also relayed by this node, or we sent it directly to the
// destination
if (p->from != 0) {
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
meshtastic_NodeDetail *origTx = nodeDB->getMeshNode(p->from);
if (origTx) {
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
// directly from the destination
@@ -176,7 +176,7 @@ uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node)
if (isBroadcast(to))
return NO_NEXT_HOP_PREFERENCE;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(to);
if (node && node->next_hop) {
// We are careful not to return the relay node as the next hop
if (node->next_hop != relay_node) {
@@ -296,7 +296,7 @@ int32_t NextHopRouter::doRetransmissions()
// Last retransmission, reset next_hop (fallback to FloodingRouter)
p.packet->next_hop = NO_NEXT_HOP_PREFERENCE;
// Also reset it in the nodeDB
meshtastic_NodeInfoLite *sentTo = nodeDB->getMeshNode(p.packet->to);
meshtastic_NodeDetail *sentTo = nodeDB->getMeshNode(p.packet->to);
if (sentTo) {
LOG_INFO("Resetting next hop for packet with dest 0x%x\n", p.packet->to);
sentTo->next_hop = NO_NEXT_HOP_PREFERENCE;

View File

@@ -24,6 +24,7 @@
#include "modules/NeighborInfoModule.h"
#include <ErriezCRC32.h>
#include <algorithm>
#include <cmath>
#include <pb_decode.h>
#include <pb_encode.h>
#include <vector>
@@ -152,18 +153,19 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field)
{
if (ostream) {
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
for (auto item : *vec) {
const auto *vec = reinterpret_cast<const std::vector<meshtastic_NodeDetail> *>(field->pData);
for (const auto &item : *vec) {
if (!pb_encode_tag_for_field(ostream, field))
return false;
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item);
if (!pb_encode_submessage(ostream, meshtastic_NodeDetail_fields, &item))
return false;
}
}
if (istream) {
meshtastic_NodeInfoLite node; // this gets good data
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
meshtastic_NodeDetail node = meshtastic_NodeDetail_init_default; // this gets good data
auto *vec = reinterpret_cast<std::vector<meshtastic_NodeDetail> *>(field->pData);
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeDetail_fields, &node))
vec->push_back(node);
}
return true;
@@ -295,9 +297,8 @@ NodeDB::NodeDB()
}
#endif
// Include our owner in the node db under our nodenum
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
info->user = TypeConversions::ConvertToUserLite(owner);
info->has_user = true;
meshtastic_NodeDetail *info = getOrCreateMeshNode(getNodeNum());
applyUserToDetail(*info, owner);
// If node database has not been saved for the first time, save it now
#ifdef FSCom
@@ -520,7 +521,7 @@ void NodeDB::installDefaultNodeDatabase()
{
LOG_DEBUG("Install default NodeDatabase");
nodeDatabase.version = DEVICESTATE_CUR_VER;
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
nodeDatabase.nodes = std::vector<meshtastic_NodeDetail>(MAX_NUM_NODES, makeDefaultDetail());
numMeshNodes = 0;
meshNodes = &nodeDatabase.nodes;
}
@@ -653,7 +654,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL)||defined(HELTEC_V4_TFT)) && \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
@@ -992,16 +993,16 @@ void NodeDB::resetNodes(bool keepFavorites)
if (keepFavorites) {
LOG_INFO("Clearing node database - preserving favorites");
for (size_t i = 0; i < meshNodes->size(); i++) {
meshtastic_NodeInfoLite &node = meshNodes->at(i);
if (i > 0 && !node.is_favorite) {
node = meshtastic_NodeInfoLite();
meshtastic_NodeDetail &detail = meshNodes->at(i);
if (i > 0 && !detailIsFavorite(detail)) {
detail = makeDefaultDetail();
} else {
numMeshNodes += 1;
}
};
} else {
LOG_INFO("Clearing node database - removing favorites");
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), makeDefaultDetail());
}
devicestate.has_rx_text_message = false;
devicestate.has_rx_waypoint = false;
@@ -1021,19 +1022,17 @@ void NodeDB::removeNodeByNum(NodeNum nodeNum)
removed++;
}
numMeshNodes -= removed;
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
meshtastic_NodeInfoLite());
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1, makeDefaultDetail());
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
saveNodeDatabaseToDisk();
}
void NodeDB::clearLocalPosition()
{
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum());
node->position.latitude_i = 0;
node->position.longitude_i = 0;
node->position.altitude = 0;
node->position.time = 0;
meshtastic_NodeDetail *detail = getMeshNode(nodeDB->getNodeNum());
if (detail) {
clearPositionFromDetail(*detail);
}
setLocalPosition(meshtastic_Position_init_default);
}
@@ -1041,10 +1040,10 @@ void NodeDB::cleanupMeshDB()
{
int newPos = 0, removed = 0;
for (int i = 0; i < numMeshNodes; i++) {
if (meshNodes->at(i).has_user) {
if (meshNodes->at(i).user.public_key.size > 0) {
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) {
meshNodes->at(i).user.public_key.size = 0;
if (detailHasFlag(meshNodes->at(i), NODEDETAIL_FLAG_HAS_USER)) {
if (meshNodes->at(i).public_key.size > 0) {
if (memfll(meshNodes->at(i).public_key.bytes, 0, meshNodes->at(i).public_key.size)) {
meshNodes->at(i).public_key.size = 0;
}
}
if (newPos != i)
@@ -1057,7 +1056,7 @@ void NodeDB::cleanupMeshDB()
}
numMeshNodes -= removed;
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed,
meshtastic_NodeInfoLite());
makeDefaultDetail());
LOG_DEBUG("cleanupMeshDB purged %d entries", removed);
}
@@ -1109,14 +1108,14 @@ void NodeDB::pickNewNodeNum()
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
}
meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) ||
meshtastic_NodeDetail *found;
while (((found = getMeshNode(nodeNum)) && memcmp(found->macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so "
"trying for 0x%x",
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
nodeNum, found->macaddr[4], found->macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
nodeNum = candidate;
}
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
@@ -1415,7 +1414,7 @@ bool NodeDB::saveDeviceStateToDisk()
FSCom.mkdir("/prefs");
spiLock->unlock();
#endif
// Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB
// Note: if MAX_NUM_NODES=100 and meshtastic_NodeDetail_size=182, node storage alone is roughly 18KB of data
// Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this
return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true);
}
@@ -1508,7 +1507,7 @@ bool NodeDB::saveToDisk(int saveWhat)
return success;
}
const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex)
const meshtastic_NodeDetail *NodeDB::readNextMeshNode(uint32_t &readIndex)
{
if (readIndex < numMeshNodes)
return &meshNodes->at(readIndex++);
@@ -1517,7 +1516,7 @@ const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex)
}
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n)
uint32_t sinceLastSeen(const meshtastic_NodeDetail *n)
{
uint32_t now = getTime();
@@ -1547,9 +1546,10 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
// FIXME this implementation is kinda expensive
for (int i = 0; i < numMeshNodes; i++) {
if (localOnly && meshNodes->at(i).via_mqtt)
const auto &detail = meshNodes->at(i);
if (localOnly && detailViaMqtt(detail))
continue;
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
if (sinceLastSeen(&detail) < NUM_ONLINE_SECS)
numseen++;
}
@@ -1563,8 +1563,8 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
*/
void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
if (!info) {
meshtastic_NodeDetail *detail = getOrCreateMeshNode(nodeId);
if (!detail) {
return;
}
@@ -1574,12 +1574,13 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
p.altitude);
setLocalPosition(p);
info->position = TypeConversions::ConvertToPositionLite(p);
applyPositionToDetail(*detail, 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;
detailSetFlag(*detail, NODEDETAIL_FLAG_HAS_POSITION, true);
detail->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
@@ -1589,17 +1590,16 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
LOG_INFO("updatePosition REMOTE node=0x%x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
// First, back up fields that we want to protect from overwrite
uint32_t tmp_time = info->position.time;
uint32_t tmp_time = detail->position_time;
// Next, update atomically
info->position = TypeConversions::ConvertToPositionLite(p);
applyPositionToDetail(*detail, p);
// Last, restore any fields that may have been overwritten
if (!info->position.time)
info->position.time = tmp_time;
if (!detail->position_time)
detail->position_time = tmp_time;
}
info->has_position = true;
updateGUIforNode = info;
updateGUIforNode = detail;
notifyObservers(true); // Force an update whether or not our node counts have changed
}
@@ -1608,9 +1608,9 @@ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSou
*/
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
meshtastic_NodeDetail *detail = getOrCreateMeshNode(nodeId);
// Environment metrics should never go to NodeDb but we'll safegaurd anyway
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
if (!detail || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
return;
}
@@ -1620,9 +1620,8 @@ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxS
} else {
LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId);
}
info->device_metrics = t.variant.device_metrics;
info->has_device_metrics = true;
updateGUIforNode = info;
applyMetricsToDetail(*detail, t.variant.device_metrics);
updateGUIforNode = detail;
notifyObservers(true); // Force an update whether or not our node counts have changed
}
@@ -1631,32 +1630,32 @@ void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxS
*/
void NodeDB::addFromContact(meshtastic_SharedContact contact)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num);
if (!info || !contact.has_user) {
meshtastic_NodeDetail *detail = getOrCreateMeshNode(contact.node_num);
if (!detail || !contact.has_user) {
return;
}
// If the local node has this node marked as manually verified
// and the client does not, do not allow the client to update the
// saved public key.
if ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) {
if (contact.user.public_key.size != info->user.public_key.size ||
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) {
if (detailHasFlag(*detail, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED) && !contact.manually_verified) {
if (contact.user.public_key.size != detail->public_key.size ||
memcmp(contact.user.public_key.bytes, detail->public_key.bytes, detail->public_key.size) != 0) {
return;
}
}
info->num = contact.node_num;
info->has_user = true;
info->user = TypeConversions::ConvertToUserLite(contact.user);
detail->num = contact.node_num;
applyUserToDetail(*detail, contact.user);
if (contact.should_ignore) {
// If should_ignore is set,
// we need to clear the public key and other cruft, in addition to setting the node as ignored
info->is_ignored = true;
info->is_favorite = false;
info->has_device_metrics = false;
info->has_position = false;
info->user.public_key.size = 0;
info->user.public_key.bytes[0] = 0;
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_IGNORED, true);
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_FAVORITE, false);
clearMetricsFromDetail(*detail);
clearPositionFromDetail(*detail);
detail->public_key.size = 0;
memset(detail->public_key.bytes, 0, sizeof(detail->public_key.bytes));
} else {
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_IGNORED, false);
/* 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!
*/
@@ -1670,19 +1669,19 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
// without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add
// contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set
// last_heard to now, so that the add_contact node doesn't immediately get evicted.
info->last_heard = getTime();
detail->last_heard = getTime();
} else {
// Normal case: set is_favorite to prevent expiration.
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
info->is_favorite = true;
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_FAVORITE, true);
}
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
if (contact.manually_verified) {
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED, true);
}
// Mark the node's key as manually verified to indicate trustworthiness.
updateGUIforNode = info;
updateGUIforNode = detail;
sortMeshDB();
notifyObservers(true); // Force an update whether or not our node counts have changed
}
@@ -1693,8 +1692,8 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
*/
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex)
{
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
if (!info) {
meshtastic_NodeDetail *detail = getOrCreateMeshNode(nodeId);
if (!detail) {
return false;
}
@@ -1719,9 +1718,9 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
return false;
}
}
if (info->user.public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
if (detail->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
// if the key doesn't match, don't update nodeDB at all.
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->user.public_key.bytes, 32) != 0)) {
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, detail->public_key.bytes, 32) != 0)) {
LOG_WARN("Public Key mismatch, dropping NodeInfo");
return false;
}
@@ -1734,22 +1733,38 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
// Always ensure user.id is derived from nodeId, regardless of what was received
snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
// Both of info->user and p start as filled with zero so I think this is okay
auto lite = TypeConversions::ConvertToUserLite(p);
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex);
meshtastic_NodeDetail before = *detail;
info->user = lite;
if (info->user.public_key.size == 32) {
printBytes("Saved Pubkey: ", info->user.public_key.bytes, 32);
applyUserToDetail(*detail, p);
if (detail->public_key.size == 32) {
printBytes("Saved Pubkey: ", detail->public_key.bytes, 32);
}
if (nodeId != getNodeNum())
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->user.long_name, info->user.short_name, nodeId,
info->channel);
info->has_user = true;
if (nodeId != getNodeNum()) {
detail->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
}
bool userChanged = strncmp(before.long_name, detail->long_name, sizeof(detail->long_name)) != 0 ||
strncmp(before.short_name, detail->short_name, sizeof(detail->short_name)) != 0 ||
memcmp(before.macaddr, detail->macaddr, sizeof(detail->macaddr)) != 0 ||
before.hw_model != detail->hw_model || before.role != detail->role ||
before.public_key.size != detail->public_key.size ||
memcmp(before.public_key.bytes, detail->public_key.bytes, detail->public_key.size) != 0;
uint32_t flagDelta = before.flags ^ detail->flags;
bool flagChange =
(flagDelta & (NODEDETAIL_FLAG_IS_LICENSED | NODEDETAIL_FLAG_HAS_UNMESSAGABLE | NODEDETAIL_FLAG_IS_UNMESSAGABLE)) != 0;
bool channelChanged = (nodeId != getNodeNum()) && (before.channel != detail->channel);
bool changed = userChanged || flagChange || channelChanged;
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, detail->long_name, detail->short_name, nodeId,
detail->channel);
if (changed) {
updateGUIforNode = info;
updateGUIforNode = detail;
notifyObservers(true); // Force an update whether or not our node counts have changed
// We just changed something about a User,
@@ -1777,23 +1792,24 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time);
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getFrom(&mp));
if (!info) {
meshtastic_NodeDetail *detail = getOrCreateMeshNode(getFrom(&mp));
if (!detail) {
return;
}
if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard
info->last_heard = mp.rx_time;
detail->last_heard = mp.rx_time;
if (mp.rx_snr)
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
detail->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
detailSetFlag(*detail, NODEDETAIL_FLAG_VIA_MQTT, 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 (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start) {
info->has_hops_away = true;
info->hops_away = mp.hop_start - mp.hop_limit;
detailSetFlag(*detail, NODEDETAIL_FLAG_HAS_HOPS_AWAY, true);
uint32_t hopDelta = mp.hop_start - mp.hop_limit;
detail->hops_away = static_cast<uint8_t>(std::min<uint32_t>(hopDelta, UINT8_MAX));
}
sortMeshDB();
}
@@ -1801,9 +1817,9 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
{
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
if (lite && lite->is_favorite != is_favorite) {
lite->is_favorite = is_favorite;
meshtastic_NodeDetail *detail = getMeshNode(nodeId);
if (detail && detailIsFavorite(*detail) != is_favorite) {
detailSetFlag(*detail, NODEDETAIL_FLAG_IS_FAVORITE, is_favorite);
sortMeshDB();
saveNodeDatabaseToDisk();
}
@@ -1817,10 +1833,10 @@ bool NodeDB::isFavorite(uint32_t nodeId)
if (nodeId == NODENUM_BROADCAST)
return false;
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
meshtastic_NodeDetail *detail = getMeshNode(nodeId);
if (lite) {
return lite->is_favorite;
if (detail) {
return detailIsFavorite(*detail);
}
return false;
}
@@ -1836,23 +1852,21 @@ bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
if (p.to == NODENUM_BROADCAST)
return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from
meshtastic_NodeInfoLite *lite = NULL;
bool seenFrom = false;
bool seenTo = false;
for (int i = 0; i < numMeshNodes; i++) {
lite = &meshNodes->at(i);
auto *detail = &meshNodes->at(i);
if (lite->num == p.from) {
if (lite->is_favorite)
if (detail->num == p.from) {
if (detailIsFavorite(*detail))
return true;
seenFrom = true;
}
if (lite->num == p.to) {
if (lite->is_favorite)
if (detail->num == p.to) {
if (detailIsFavorite(*detail))
return true;
seenTo = true;
@@ -1888,10 +1902,10 @@ void NodeDB::sortMeshDB()
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true;
} else if (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) {
} else if (detailIsFavorite(meshNodes->at(i)) && !detailIsFavorite(meshNodes->at(i - 1))) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
changed = true;
} else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) {
} else if (!detailIsFavorite(meshNodes->at(i)) && detailIsFavorite(meshNodes->at(i - 1))) {
// noop
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
@@ -1905,11 +1919,11 @@ void NodeDB::sortMeshDB()
uint8_t NodeDB::getMeshNodeChannel(NodeNum n)
{
const meshtastic_NodeInfoLite *info = getMeshNode(n);
if (!info) {
const meshtastic_NodeDetail *detail = getMeshNode(n);
if (!detail) {
return 0; // defaults to PRIMARY
}
return info->channel;
return detail->channel;
}
std::string NodeDB::getNodeId() const
@@ -1921,7 +1935,7 @@ std::string NodeDB::getNodeId() const
/// Find a node in our DB, return null for missing
/// NOTE: This function might be called from an ISR
meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n)
meshtastic_NodeDetail *NodeDB::getMeshNode(NodeNum n)
{
for (int i = 0; i < numMeshNodes; i++)
if (meshNodes->at(i).num == n)
@@ -1937,11 +1951,11 @@ bool NodeDB::isFull()
}
/// Find a node in our DB, create an empty NodeInfo if missing
meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
meshtastic_NodeDetail *NodeDB::getOrCreateMeshNode(NodeNum n)
{
meshtastic_NodeInfoLite *lite = getMeshNode(n);
meshtastic_NodeDetail *detail = getMeshNode(n);
if (!lite) {
if (!detail) {
if (isFull()) {
LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes,
memGet.getFreeHeap());
@@ -1952,14 +1966,14 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
int oldestBoringIndex = -1;
for (int i = 1; i < numMeshNodes; i++) {
// Simply the oldest non-favorite, non-ignored, non-verified node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored &&
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) &&
meshNodes->at(i).last_heard < oldest) {
const auto &candidate = meshNodes->at(i);
if (!detailIsFavorite(candidate) && !detailIsIgnored(candidate) &&
!detailHasFlag(candidate, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED) && meshNodes->at(i).last_heard < oldest) {
oldest = meshNodes->at(i).last_heard;
oldestIndex = i;
}
// The oldest "boring" node
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 &&
if (!detailIsFavorite(candidate) && !detailIsIgnored(candidate) && candidate.public_key.size == 0 &&
meshNodes->at(i).last_heard < oldestBoring) {
oldestBoring = meshNodes->at(i).last_heard;
oldestBoringIndex = i;
@@ -1979,35 +1993,36 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
}
}
// add the node at the end
lite = &meshNodes->at((numMeshNodes)++);
detail = &meshNodes->at((numMeshNodes)++);
// everything is missing except the nodenum
memset(lite, 0, sizeof(*lite));
lite->num = n;
*detail = makeDefaultDetail();
detail->num = n;
LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap());
}
return lite;
return detail;
}
/// Sometimes we will have Position objects that only have a time, so check for
/// valid lat/lon
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
bool NodeDB::hasValidPosition(const meshtastic_NodeDetail *n)
{
return n->has_position && (n->position.latitude_i != 0 || n->position.longitude_i != 0);
return detailHasFlag(*n, NODEDETAIL_FLAG_HAS_POSITION) && (n->latitude_i != 0 || n->longitude_i != 0);
}
/// If we have a node / user and they report is_licensed = true
/// we consider them licensed
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
{
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
if (!info || !info->has_user) {
meshtastic_NodeDetail *detail = getMeshNode(nodeNum);
if (!detail || !detailHasFlag(*detail, NODEDETAIL_FLAG_HAS_USER)) {
return UserLicenseStatus::NotKnown;
}
return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
return detailHasFlag(*detail, NODEDETAIL_FLAG_IS_LICENSED) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
}
#if !defined(MESHTASTIC_EXCLUDE_PKI)
bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest)
{
if (keyToTest.size == 32) {
@@ -2022,6 +2037,7 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub
}
return false;
}
#endif
bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location)
{

View File

@@ -4,6 +4,8 @@
#include <Arduino.h>
#include <algorithm>
#include <assert.h>
#include <cmath>
#include <cstring>
#include <pb_encode.h>
#include <string>
#include <vector>
@@ -104,8 +106,13 @@ static constexpr const char *moduleConfigFileName = "/prefs/module.proto";
static constexpr const char *channelFileName = "/prefs/channels.proto";
static constexpr const char *backupFileName = "/backups/backup.proto";
template <typename T> inline T clampValue(T value, T low, T high)
{
return std::max(low, std::min(value, high));
}
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n);
uint32_t sinceLastSeen(const meshtastic_NodeDetail *n);
/// Given a packet, return how many seconds in the past (vs now) it was received
uint32_t sinceReceived(const meshtastic_MeshPacket *p);
@@ -135,9 +142,9 @@ class NodeDB
// Note: these two references just point into our static array we serialize to/from disk
public:
std::vector<meshtastic_NodeInfoLite> *meshNodes;
std::vector<meshtastic_NodeDetail> *meshNodes;
bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled
meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI
meshtastic_NodeDetail *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI
Observable<const meshtastic::NodeStatus *> newStatus;
pb_size_t numMeshNodes;
@@ -241,15 +248,15 @@ class NodeDB
void installRoleDefaults(meshtastic_Config_DeviceConfig_Role role);
const meshtastic_NodeInfoLite *readNextMeshNode(uint32_t &readIndex);
const meshtastic_NodeDetail *readNextMeshNode(uint32_t &readIndex);
meshtastic_NodeInfoLite *getMeshNodeByIndex(size_t x)
meshtastic_NodeDetail *getMeshNodeByIndex(size_t x)
{
assert(x < numMeshNodes);
return &meshNodes->at(x);
}
virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
virtual meshtastic_NodeDetail *getMeshNode(NodeNum n);
size_t getNumMeshNodes() { return numMeshNodes; }
UserLicenseStatus getLicenseStatus(uint32_t nodeNum);
@@ -260,7 +267,7 @@ class NodeDB
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
size_t nodeDatabaseSize;
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeInfoLite_size);
return nodeDatabaseSize + (MAX_NUM_NODES * meshtastic_NodeDetail_size);
}
// returns true if the maximum number of nodes is reached or we are running low on memory
@@ -281,9 +288,11 @@ class NodeDB
localPosition = position;
}
bool hasValidPosition(const meshtastic_NodeInfoLite *n);
bool hasValidPosition(const meshtastic_NodeDetail *n);
#if !defined(MESHTASTIC_EXCLUDE_PKI)
bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest);
#endif
bool backupPreferences(meshtastic_AdminMessage_BackupLocation location);
bool restorePreferences(meshtastic_AdminMessage_BackupLocation location,
@@ -302,8 +311,8 @@ class NodeDB
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
uint32_t lastSort = 0; // When last sorted the nodeDB
/// Find a node in our DB, create an empty NodeInfoLite if missing
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
/// Find a node in our DB, create an empty NodeDetail if missing
meshtastic_NodeDetail *getOrCreateMeshNode(NodeNum n);
/*
* Internal boolean to track sorting paused
@@ -368,9 +377,265 @@ extern uint32_t error_address;
#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)
enum NodeDetailFlag : uint32_t {
NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED = 1u << 0,
NODEDETAIL_FLAG_HAS_USER = 1u << 1,
NODEDETAIL_FLAG_HAS_POSITION = 1u << 2,
NODEDETAIL_FLAG_HAS_DEVICE_METRICS = 1u << 3,
NODEDETAIL_FLAG_VIA_MQTT = 1u << 4,
NODEDETAIL_FLAG_IS_FAVORITE = 1u << 5,
NODEDETAIL_FLAG_IS_IGNORED = 1u << 6,
NODEDETAIL_FLAG_HAS_HOPS_AWAY = 1u << 7,
NODEDETAIL_FLAG_IS_LICENSED = 1u << 8,
NODEDETAIL_FLAG_HAS_UNMESSAGABLE = 1u << 9,
NODEDETAIL_FLAG_IS_UNMESSAGABLE = 1u << 10,
NODEDETAIL_FLAG_HAS_BATTERY_LEVEL = 1u << 11,
NODEDETAIL_FLAG_HAS_VOLTAGE = 1u << 12,
NODEDETAIL_FLAG_HAS_CHANNEL_UTIL = 1u << 13,
NODEDETAIL_FLAG_HAS_AIR_UTIL_TX = 1u << 14,
NODEDETAIL_FLAG_HAS_UPTIME = 1u << 15
};
inline bool detailHasFlag(const meshtastic_NodeDetail &detail, NodeDetailFlag flag)
{
return (detail.flags & static_cast<uint32_t>(flag)) != 0;
}
inline void detailSetFlag(meshtastic_NodeDetail &detail, NodeDetailFlag flag, bool value = true)
{
if (value) {
detail.flags |= static_cast<uint32_t>(flag);
} else {
detail.flags &= ~static_cast<uint32_t>(flag);
}
}
inline meshtastic_NodeDetail makeDefaultDetail()
{
meshtastic_NodeDetail detail = meshtastic_NodeDetail_init_default;
return detail;
}
inline void clearUserFromDetail(meshtastic_NodeDetail &detail)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_USER, false);
detailSetFlag(detail, NODEDETAIL_FLAG_IS_LICENSED, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UNMESSAGABLE, false);
detailSetFlag(detail, NODEDETAIL_FLAG_IS_UNMESSAGABLE, false);
detail.long_name[0] = '\0';
detail.short_name[0] = '\0';
memset(detail.macaddr, 0, sizeof(detail.macaddr));
detail.hw_model = _meshtastic_HardwareModel_MIN;
detail.role = _meshtastic_Config_DeviceConfig_Role_MIN;
detail.public_key.size = 0;
memset(detail.public_key.bytes, 0, sizeof(detail.public_key.bytes));
}
inline void applyUserLiteToDetail(meshtastic_NodeDetail &detail, const meshtastic_UserLite &userLite)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_USER, true);
strncpy(detail.long_name, userLite.long_name, sizeof(detail.long_name));
detail.long_name[sizeof(detail.long_name) - 1] = '\0';
strncpy(detail.short_name, userLite.short_name, sizeof(detail.short_name));
detail.short_name[sizeof(detail.short_name) - 1] = '\0';
memcpy(detail.macaddr, userLite.macaddr, sizeof(detail.macaddr));
detail.hw_model = userLite.hw_model;
detail.role = userLite.role;
const pb_size_t keySize = std::min(userLite.public_key.size, static_cast<pb_size_t>(sizeof(detail.public_key.bytes)));
memcpy(detail.public_key.bytes, userLite.public_key.bytes, keySize);
detail.public_key.size = keySize;
detailSetFlag(detail, NODEDETAIL_FLAG_IS_LICENSED, userLite.is_licensed);
if (userLite.has_is_unmessagable) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UNMESSAGABLE, true);
detailSetFlag(detail, NODEDETAIL_FLAG_IS_UNMESSAGABLE, userLite.is_unmessagable);
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UNMESSAGABLE, false);
detailSetFlag(detail, NODEDETAIL_FLAG_IS_UNMESSAGABLE, false);
}
}
inline void applyUserToDetail(meshtastic_NodeDetail &detail, const meshtastic_User &user)
{
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
lite.hw_model = user.hw_model;
lite.role = user.role;
lite.is_licensed = user.is_licensed;
memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr));
const pb_size_t keySize = std::min(user.public_key.size, static_cast<pb_size_t>(sizeof(lite.public_key.bytes)));
memcpy(lite.public_key.bytes, user.public_key.bytes, keySize);
lite.public_key.size = keySize;
lite.has_is_unmessagable = user.has_is_unmessagable;
lite.is_unmessagable = user.is_unmessagable;
applyUserLiteToDetail(detail, lite);
}
inline void clearPositionFromDetail(meshtastic_NodeDetail &detail)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_POSITION, false);
detail.latitude_i = 0;
detail.longitude_i = 0;
detail.altitude = 0;
detail.position_time = 0;
detail.position_source = _meshtastic_Position_LocSource_MIN;
}
inline void applyPositionLiteToDetail(meshtastic_NodeDetail &detail, const meshtastic_PositionLite &positionLite)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_POSITION, true);
detail.latitude_i = positionLite.latitude_i;
detail.longitude_i = positionLite.longitude_i;
detail.altitude = positionLite.altitude;
detail.position_time = positionLite.time;
detail.position_source = positionLite.location_source;
}
inline void applyPositionToDetail(meshtastic_NodeDetail &detail, const meshtastic_Position &position)
{
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
lite.latitude_i = position.latitude_i;
lite.longitude_i = position.longitude_i;
lite.altitude = position.altitude;
lite.location_source = position.location_source;
lite.time = position.time;
applyPositionLiteToDetail(detail, lite);
}
inline void clearMetricsFromDetail(meshtastic_NodeDetail &detail)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_DEVICE_METRICS, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_BATTERY_LEVEL, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_VOLTAGE, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_CHANNEL_UTIL, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_AIR_UTIL_TX, false);
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UPTIME, false);
detail.battery_level = 0;
detail.voltage_millivolts = 0;
detail.channel_utilization_permille = 0;
detail.air_util_tx_permille = 0;
detail.uptime_seconds = 0;
}
inline void applyMetricsToDetail(meshtastic_NodeDetail &detail, const meshtastic_DeviceMetrics &metrics)
{
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_DEVICE_METRICS, true);
if (metrics.has_battery_level) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_BATTERY_LEVEL, true);
uint32_t battery = metrics.battery_level;
if (battery > 255u) {
battery = 255u;
}
detail.battery_level = static_cast<uint8_t>(battery);
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_BATTERY_LEVEL, false);
}
if (metrics.has_voltage) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_VOLTAGE, true);
double limitedVoltage = clampValue(static_cast<double>(metrics.voltage), 0.0, 65.535);
int millivolts = static_cast<int>(std::lround(limitedVoltage * 1000.0));
millivolts = clampValue<int>(millivolts, 0, 0xFFFF);
detail.voltage_millivolts = static_cast<uint16_t>(millivolts);
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_VOLTAGE, false);
detail.voltage_millivolts = 0;
}
if (metrics.has_channel_utilization) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_CHANNEL_UTIL, true);
double limitedUtil = clampValue(static_cast<double>(metrics.channel_utilization), 0.0, 100.0);
int permille = static_cast<int>(std::lround(limitedUtil * 10.0));
permille = clampValue<int>(permille, 0, 1000);
detail.channel_utilization_permille = static_cast<uint16_t>(permille);
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_CHANNEL_UTIL, false);
detail.channel_utilization_permille = 0;
}
if (metrics.has_air_util_tx) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_AIR_UTIL_TX, true);
double limitedAirUtil = clampValue(static_cast<double>(metrics.air_util_tx), 0.0, 100.0);
int permille = static_cast<int>(std::lround(limitedAirUtil * 10.0));
permille = clampValue<int>(permille, 0, 1000);
detail.air_util_tx_permille = static_cast<uint16_t>(permille);
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_AIR_UTIL_TX, false);
detail.air_util_tx_permille = 0;
}
if (metrics.has_uptime_seconds) {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UPTIME, true);
detail.uptime_seconds = metrics.uptime_seconds;
} else {
detailSetFlag(detail, NODEDETAIL_FLAG_HAS_UPTIME, false);
detail.uptime_seconds = 0;
}
}
inline bool detailIsFavorite(const meshtastic_NodeDetail &detail)
{
return detailHasFlag(detail, NODEDETAIL_FLAG_IS_FAVORITE);
}
inline bool detailIsIgnored(const meshtastic_NodeDetail &detail)
{
return detailHasFlag(detail, NODEDETAIL_FLAG_IS_IGNORED);
}
inline bool detailViaMqtt(const meshtastic_NodeDetail &detail)
{
return detailHasFlag(detail, NODEDETAIL_FLAG_VIA_MQTT);
}
inline meshtastic_PositionLite detailToPositionLite(const meshtastic_NodeDetail &detail)
{
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;
if (!detailHasFlag(detail, NODEDETAIL_FLAG_HAS_POSITION)) {
return lite;
}
lite.latitude_i = detail.latitude_i;
lite.longitude_i = detail.longitude_i;
lite.altitude = detail.altitude;
lite.time = detail.position_time;
lite.location_source = detail.position_source;
return lite;
}
inline meshtastic_UserLite detailToUserLite(const meshtastic_NodeDetail &detail)
{
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
if (!detailHasFlag(detail, NODEDETAIL_FLAG_HAS_USER)) {
return lite;
}
strncpy(lite.long_name, detail.long_name, sizeof(lite.long_name));
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
strncpy(lite.short_name, detail.short_name, sizeof(lite.short_name));
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
lite.hw_model = detail.hw_model;
lite.role = detail.role;
lite.is_licensed = detailHasFlag(detail, NODEDETAIL_FLAG_IS_LICENSED);
memcpy(lite.macaddr, detail.macaddr, sizeof(lite.macaddr));
lite.public_key.size = std::min(static_cast<pb_size_t>(sizeof(lite.public_key.bytes)), detail.public_key.size);
memcpy(lite.public_key.bytes, detail.public_key.bytes, lite.public_key.size);
if (detailHasFlag(detail, NODEDETAIL_FLAG_HAS_UNMESSAGABLE)) {
lite.has_is_unmessagable = true;
lite.is_unmessagable = detailHasFlag(detail, NODEDETAIL_FLAG_IS_UNMESSAGABLE);
}
return lite;
}
#define Module_Config_size \
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \
ModuleConfig_TelemetryConfig_size + ModuleConfig_size)
// Please do not remove this comment, it makes trunk and compiler happy at the same time.
// Please do not remove this comment, it makes trunk and compiler happy at the same time.

View File

@@ -267,9 +267,9 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
case STATE_SEND_OWN_NODEINFO: {
LOG_DEBUG("Send My NodeInfo");
auto us = nodeDB->readNextMeshNode(readIndex);
const meshtastic_NodeDetail *us = nodeDB->readNextMeshNode(readIndex);
if (us) {
auto info = TypeConversions::ConvertToNodeInfo(us);
auto info = TypeConversions::ConvertToNodeInfo(*us);
info.has_hops_away = false;
info.is_favorite = true;
{
@@ -633,11 +633,11 @@ void PhoneAPI::prefetchNodeInfos()
{
concurrency::LockGuard guard(&nodeInfoMutex);
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
auto nextNode = nodeDB->readNextMeshNode(readIndex);
const meshtastic_NodeDetail *nextNode = nodeDB->readNextMeshNode(readIndex);
if (!nextNode)
break;
auto info = TypeConversions::ConvertToNodeInfo(nextNode);
auto info = TypeConversions::ConvertToNodeInfo(*nextNode);
bool isUs = info.num == nodeDB->getNodeNum();
info.hops_away = isUs ? 0 : info.hops_away;
info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard;

View File

@@ -58,7 +58,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
const char *getSenderShortName(const meshtastic_MeshPacket &mp)
{
auto node = nodeDB->getMeshNode(getFrom(&mp));
const char *sender = (node) ? node->user.short_name : "???";
const char *sender = (node) ? node->short_name : "???";
return sender;
}

View File

@@ -119,11 +119,16 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
// stop the immediate relayer's retransmissions.
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
}
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 &&
(nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) {
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit));
} else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0) {
const meshtastic_NodeDetail *fromDetail = nodeDB->getMeshNode(p->from);
if (!fromDetail || fromDetail->public_key.size == 0) {
LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY");
sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(),
routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit));
} else {
sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(),
routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit));
}
} else {
// Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded
sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(),

View File

@@ -100,21 +100,20 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
// Optimized search for favorite routers with matching last byte
// Check ordering optimized for IoT devices (cheapest checks first)
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
const meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
if (!node)
continue;
// Check 1: is_favorite (cheapest - single bool)
if (!node->is_favorite)
if (!detailIsFavorite(*node))
continue;
// Check 2: has_user (cheap - single bool)
if (!node->has_user)
if (!detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER))
continue;
// Check 3: role check (moderate cost - multiple comparisons)
if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER,
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
if (!IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
continue;
}
@@ -261,7 +260,7 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
// don't override if a channel was requested and no need to set it when PKI is enforced
if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) {
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to);
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(p->to);
if (node) {
p->channel = node->channel;
LOG_DEBUG("localSend to channel %d", p->channel);
@@ -409,8 +408,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
{
concurrency::LockGuard g(cryptLock);
const meshtastic_NodeDetail *fromDetail = nodeDB->getMeshNode(p->from);
const meshtastic_NodeDetail *toDetail = nodeDB->getMeshNode(p->to);
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&
(nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) {
(!fromDetail || !detailHasFlag(*fromDetail, NODEDETAIL_FLAG_HAS_USER))) {
LOG_DEBUG("Node 0x%x not in nodeDB-> Rebroadcast mode KNOWN_ONLY will ignore packet", p->from);
return DecodeState::DECODE_FAILURE;
}
@@ -427,33 +429,38 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
ChannelIndex chIndex = 0;
#if !(MESHTASTIC_EXCLUDE_PKI)
// Attempt PKI decryption first
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 &&
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && fromDetail != nullptr && toDetail != nullptr &&
rawSize > MESHTASTIC_PKC_OVERHEAD) {
LOG_DEBUG("Attempt PKI decryption");
meshtastic_UserLite fromLite = meshtastic_UserLite_init_default;
meshtastic_UserLite toLite = meshtastic_UserLite_init_default;
fromLite = detailToUserLite(*fromDetail);
toLite = detailToUserLite(*toDetail);
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->user.public_key, p->id, rawSize, p->encrypted.bytes,
bytes)) {
LOG_INFO("PKI Decryption worked!");
if (fromLite.public_key.size == 32 && toLite.public_key.size == 32) {
LOG_DEBUG("Attempt PKI decryption");
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
rawSize -= MESHTASTIC_PKC_OVERHEAD;
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
decrypted = true;
LOG_INFO("Packet decrypted using PKI!");
p->pki_encrypted = true;
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32);
p->public_key.size = 32;
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
if (crypto->decryptCurve25519(p->from, fromLite.public_key, p->id, rawSize, p->encrypted.bytes, bytes)) {
LOG_INFO("PKI Decryption worked!");
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
rawSize -= MESHTASTIC_PKC_OVERHEAD;
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) &&
decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) {
decrypted = true;
LOG_INFO("Packet decrypted using PKI!");
p->pki_encrypted = true;
memcpy(&p->public_key.bytes, fromLite.public_key.bytes, 32);
p->public_key.size = 32;
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
} else {
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
return DecodeState::DECODE_FAILURE;
}
} else {
LOG_ERROR("PKC Decrypted, but pb_decode failed!");
return DecodeState::DECODE_FAILURE;
LOG_WARN("PKC decrypt attempted but failed!");
}
} else {
LOG_WARN("PKC decrypt attempted but failed!");
}
}
#endif
@@ -596,7 +603,11 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
#if !(MESHTASTIC_EXCLUDE_PKI)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(p->to);
meshtastic_UserLite destLite = meshtastic_UserLite_init_default;
if (node) {
destLite = detailToUserLite(*node);
}
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
// is not in the local nodedb
// First, only PKC encrypt packets we are originating
@@ -613,7 +624,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Check for valid keys and single node destination
config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr &&
// Check for a known public key for the destination
(node->user.public_key.size == 32) &&
(destLite.public_key.size == 32) &&
// Some portnums either make no sense to send with PKC
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
@@ -621,12 +632,12 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE;
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, destLite.public_key.bytes, 32) != 0) {
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
*node->user.public_key.bytes);
*destLite.public_key.bytes);
return meshtastic_Routing_Error_PKI_FAILED;
}
crypto->encryptCurve25519(p->to, getFrom(p), node->user.public_key, p->id, numbytes, bytes, p->encrypted.bytes);
crypto->encryptCurve25519(p->to, getFrom(p), destLite.public_key, p->id, numbytes, bytes, p->encrypted.bytes);
numbytes += MESHTASTIC_PKC_OVERHEAD;
p->channel = 0;
p->pki_encrypted = true;
@@ -776,8 +787,8 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
return;
}
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
if (node != NULL && node->is_ignored) {
const meshtastic_NodeDetail *node = nodeDB->getMeshNode(p->from);
if (node != NULL && detailIsIgnored(*node)) {
LOG_DEBUG("Ignore msg, 0x%x is ignored", p->from);
packetPool.release(p);
return;

View File

@@ -1,6 +1,9 @@
#include "TypeConversions.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <algorithm>
#include <cmath>
#include <cstring>
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
{
@@ -45,6 +48,196 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
return info;
}
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeDetail &detail)
{
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;
info.num = detail.num;
info.snr = detail.snr;
info.last_heard = detail.last_heard;
info.channel = detail.channel;
info.via_mqtt = detail.flags & NODEDETAIL_FLAG_VIA_MQTT;
info.is_favorite = detail.flags & NODEDETAIL_FLAG_IS_FAVORITE;
info.is_ignored = detail.flags & NODEDETAIL_FLAG_IS_IGNORED;
info.is_key_manually_verified = detail.flags & NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED;
if (detail.flags & NODEDETAIL_FLAG_HAS_HOPS_AWAY) {
info.has_hops_away = true;
info.hops_away = detail.hops_away;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_POSITION) {
info.has_position = true;
info.position = meshtastic_Position_init_default;
if (detail.latitude_i != 0) {
info.position.has_latitude_i = true;
}
info.position.latitude_i = detail.latitude_i;
if (detail.longitude_i != 0) {
info.position.has_longitude_i = true;
}
info.position.longitude_i = detail.longitude_i;
if (detail.altitude != 0) {
info.position.has_altitude = true;
}
info.position.altitude = detail.altitude;
info.position.location_source = detail.position_source;
info.position.time = detail.position_time;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_USER) {
info.has_user = true;
meshtastic_User user = meshtastic_User_init_default;
snprintf(user.id, sizeof(user.id), "!%08x", detail.num);
strncpy(user.long_name, detail.long_name, sizeof(user.long_name));
user.long_name[sizeof(user.long_name) - 1] = '\0';
strncpy(user.short_name, detail.short_name, sizeof(user.short_name));
user.short_name[sizeof(user.short_name) - 1] = '\0';
user.hw_model = detail.hw_model;
user.role = detail.role;
user.is_licensed = detail.flags & NODEDETAIL_FLAG_IS_LICENSED;
memcpy(user.macaddr, detail.macaddr, sizeof(user.macaddr));
const pb_size_t keySize = std::min(detail.public_key.size, static_cast<pb_size_t>(sizeof(user.public_key.bytes)));
memcpy(user.public_key.bytes, detail.public_key.bytes, keySize);
user.public_key.size = keySize;
if (detail.flags & NODEDETAIL_FLAG_HAS_UNMESSAGABLE) {
user.has_is_unmessagable = true;
user.is_unmessagable = detail.flags & NODEDETAIL_FLAG_IS_UNMESSAGABLE;
}
info.user = user;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_DEVICE_METRICS) {
info.has_device_metrics = true;
meshtastic_DeviceMetrics metrics = meshtastic_DeviceMetrics_init_default;
if (detail.flags & NODEDETAIL_FLAG_HAS_BATTERY_LEVEL) {
metrics.has_battery_level = true;
metrics.battery_level = detail.battery_level;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_VOLTAGE) {
metrics.has_voltage = true;
metrics.voltage = static_cast<float>(detail.voltage_millivolts) / 1000.0f;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_CHANNEL_UTIL) {
metrics.has_channel_utilization = true;
metrics.channel_utilization = static_cast<float>(detail.channel_utilization_permille) / 10.0f;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_AIR_UTIL_TX) {
metrics.has_air_util_tx = true;
metrics.air_util_tx = static_cast<float>(detail.air_util_tx_permille) / 10.0f;
}
if (detail.flags & NODEDETAIL_FLAG_HAS_UPTIME) {
metrics.has_uptime_seconds = true;
metrics.uptime_seconds = detail.uptime_seconds;
}
info.device_metrics = metrics;
}
return info;
}
meshtastic_NodeDetail TypeConversions::ConvertToNodeDetail(const meshtastic_NodeInfoLite &lite)
{
meshtastic_NodeDetail detail = meshtastic_NodeDetail_init_default;
detail.num = lite.num;
detail.snr = lite.snr;
detail.last_heard = lite.last_heard;
detail.channel = lite.channel;
detail.next_hop = lite.next_hop;
if (lite.has_hops_away) {
detail.flags |= NODEDETAIL_FLAG_HAS_HOPS_AWAY;
detail.hops_away = lite.hops_away;
}
if (lite.via_mqtt) {
detail.flags |= NODEDETAIL_FLAG_VIA_MQTT;
}
if (lite.is_favorite) {
detail.flags |= NODEDETAIL_FLAG_IS_FAVORITE;
}
if (lite.is_ignored) {
detail.flags |= NODEDETAIL_FLAG_IS_IGNORED;
}
if (lite.bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) {
detail.flags |= NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED;
}
if (lite.has_user) {
detail.flags |= NODEDETAIL_FLAG_HAS_USER;
strncpy(detail.long_name, lite.user.long_name, sizeof(detail.long_name));
detail.long_name[sizeof(detail.long_name) - 1] = '\0';
strncpy(detail.short_name, lite.user.short_name, sizeof(detail.short_name));
detail.short_name[sizeof(detail.short_name) - 1] = '\0';
detail.hw_model = lite.user.hw_model;
detail.role = lite.user.role;
memcpy(detail.macaddr, lite.user.macaddr, sizeof(detail.macaddr));
const pb_size_t keySize = std::min(lite.user.public_key.size, static_cast<pb_size_t>(sizeof(detail.public_key.bytes)));
memcpy(detail.public_key.bytes, lite.user.public_key.bytes, keySize);
detail.public_key.size = keySize;
if (lite.user.is_licensed) {
detail.flags |= NODEDETAIL_FLAG_IS_LICENSED;
}
if (lite.user.has_is_unmessagable) {
detail.flags |= NODEDETAIL_FLAG_HAS_UNMESSAGABLE;
if (lite.user.is_unmessagable) {
detail.flags |= NODEDETAIL_FLAG_IS_UNMESSAGABLE;
}
}
}
if (lite.has_position) {
detail.flags |= NODEDETAIL_FLAG_HAS_POSITION;
detail.latitude_i = lite.position.latitude_i;
detail.longitude_i = lite.position.longitude_i;
detail.altitude = lite.position.altitude;
detail.position_time = lite.position.time;
detail.position_source = lite.position.location_source;
}
if (lite.has_device_metrics) {
detail.flags |= NODEDETAIL_FLAG_HAS_DEVICE_METRICS;
const meshtastic_DeviceMetrics &metrics = lite.device_metrics;
if (metrics.has_battery_level) {
detail.flags |= NODEDETAIL_FLAG_HAS_BATTERY_LEVEL;
uint32_t battery = metrics.battery_level;
if (battery > 255u) {
battery = 255u;
}
detail.battery_level = static_cast<uint8_t>(battery);
}
if (metrics.has_voltage) {
detail.flags |= NODEDETAIL_FLAG_HAS_VOLTAGE;
double limitedVoltage = clampValue(static_cast<double>(metrics.voltage), 0.0, 65.535);
int millivolts = static_cast<int>(std::lround(limitedVoltage * 1000.0));
millivolts = clampValue<int>(millivolts, 0, 0xFFFF);
detail.voltage_millivolts = static_cast<uint16_t>(millivolts);
}
if (metrics.has_channel_utilization) {
detail.flags |= NODEDETAIL_FLAG_HAS_CHANNEL_UTIL;
double limitedUtil = clampValue(static_cast<double>(metrics.channel_utilization), 0.0, 100.0);
int permille = static_cast<int>(std::lround(limitedUtil * 10.0));
permille = clampValue<int>(permille, 0, 1000);
detail.channel_utilization_permille = static_cast<uint16_t>(permille);
}
if (metrics.has_air_util_tx) {
detail.flags |= NODEDETAIL_FLAG_HAS_AIR_UTIL_TX;
double limitedAirUtil = clampValue(static_cast<double>(metrics.air_util_tx), 0.0, 100.0);
int permille = static_cast<int>(std::lround(limitedAirUtil * 10.0));
permille = clampValue<int>(permille, 0, 1000);
detail.air_util_tx_permille = static_cast<uint16_t>(permille);
}
if (metrics.has_uptime_seconds) {
detail.flags |= NODEDETAIL_FLAG_HAS_UPTIME;
detail.uptime_seconds = metrics.uptime_seconds;
}
}
return detail;
}
meshtastic_PositionLite TypeConversions::ConvertToPositionLite(meshtastic_Position position)
{
meshtastic_PositionLite lite = meshtastic_PositionLite_init_default;

View File

@@ -8,6 +8,8 @@ class TypeConversions
{
public:
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite);
static meshtastic_NodeInfo ConvertToNodeInfo(const meshtastic_NodeDetail &detail);
static meshtastic_NodeDetail ConvertToNodeDetail(const meshtastic_NodeInfoLite &lite);
static meshtastic_PositionLite ConvertToPositionLite(meshtastic_Position position);
static meshtastic_Position ConvertToPosition(meshtastic_PositionLite lite);
static meshtastic_UserLite ConvertToUserLite(meshtastic_User user);

View File

@@ -15,6 +15,9 @@ PB_BIND(meshtastic_UserLite, meshtastic_UserLite, AUTO)
PB_BIND(meshtastic_NodeInfoLite, meshtastic_NodeInfoLite, AUTO)
PB_BIND(meshtastic_NodeDetail, meshtastic_NodeDetail, AUTO)
PB_BIND(meshtastic_DeviceState, meshtastic_DeviceState, 2)

View File

@@ -101,6 +101,49 @@ typedef struct _meshtastic_NodeInfoLite {
uint32_t bitfield;
} meshtastic_NodeInfoLite;
typedef PB_BYTES_ARRAY_T(32) meshtastic_NodeDetail_public_key_t;
/* Flattened node representation used for the compact NodeDB rewrite.
Uses integer scaling where possible and a single flags bitfield for booleans. */
typedef struct _meshtastic_NodeDetail {
/* The node number */
uint32_t num;
/* 48-bit hardware identifier copied from the radio */
pb_byte_t macaddr[6];
/* Cached long display name */
char long_name[40];
/* Cached short display name */
char short_name[5];
/* Hardware model reported by the node */
meshtastic_HardwareModel hw_model;
/* Role assigned to the node */
meshtastic_Config_DeviceConfig_Role role;
/* Public key broadcast by the node */
meshtastic_NodeDetail_public_key_t public_key;
/* Position data flattened from PositionLite */
int32_t latitude_i;
int32_t longitude_i;
int32_t altitude;
uint32_t position_time;
meshtastic_Position_LocSource position_source;
/* Radio performance metrics */
float snr;
/* Last packet timestamp */
uint32_t last_heard;
/* Mesh routing metadata */
uint8_t channel;
uint8_t hops_away;
uint8_t next_hop;
/* Device metrics cached using integer scaling */
uint8_t battery_level;
uint32_t uptime_seconds;
uint16_t channel_utilization_permille;
uint16_t air_util_tx_permille;
uint16_t voltage_millivolts;
/* Bitset storing boolean flags and presence markers.
See NodeDetailFlag shifts for decoded meaning. */
uint32_t flags;
} meshtastic_NodeDetail;
/* This message is never sent over the wire, but it is used for serializing DB
state to flash in the device code
FIXME, since we write this each time we enter deep sleep (and have infinite
@@ -148,7 +191,7 @@ typedef struct _meshtastic_NodeDatabase {
NodeDB.cpp in the device code. */
uint32_t version;
/* New lite version of NodeDB to decrease memory footprint */
std::vector<meshtastic_NodeInfoLite> nodes;
std::vector<meshtastic_NodeDetail> nodes;
} meshtastic_NodeDatabase;
/* The on-disk saved channels */
@@ -191,6 +234,7 @@ extern "C" {
#define meshtastic_PositionLite_init_default {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
#define meshtastic_UserLite_init_default {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_default {0, false, meshtastic_UserLite_init_default, false, meshtastic_PositionLite_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_NodeDetail_init_default {0, {0}, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, 0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_DeviceState_init_default {false, meshtastic_MyNodeInfo_init_default, false, meshtastic_User_init_default, 0, {meshtastic_MeshPacket_init_default}, false, meshtastic_MeshPacket_init_default, 0, 0, 0, false, meshtastic_MeshPacket_init_default, 0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_NodeDatabase_init_default {0, {0}}
#define meshtastic_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}
@@ -198,6 +242,7 @@ extern "C" {
#define meshtastic_PositionLite_init_zero {0, 0, 0, 0, _meshtastic_Position_LocSource_MIN}
#define meshtastic_UserLite_init_zero {{0}, "", "", _meshtastic_HardwareModel_MIN, 0, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, false, 0}
#define meshtastic_NodeInfoLite_init_zero {0, false, meshtastic_UserLite_init_zero, false, meshtastic_PositionLite_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0, 0}
#define meshtastic_NodeDetail_init_zero {0, {0}, "", "", _meshtastic_HardwareModel_MIN, _meshtastic_Config_DeviceConfig_Role_MIN, {0, {0}}, 0, 0, 0, 0, _meshtastic_Position_LocSource_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define meshtastic_DeviceState_init_zero {false, meshtastic_MyNodeInfo_init_zero, false, meshtastic_User_init_zero, 0, {meshtastic_MeshPacket_init_zero}, false, meshtastic_MeshPacket_init_zero, 0, 0, 0, false, meshtastic_MeshPacket_init_zero, 0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_NodeDatabase_init_zero {0, {0}}
#define meshtastic_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}
@@ -230,6 +275,29 @@ extern "C" {
#define meshtastic_NodeInfoLite_is_ignored_tag 11
#define meshtastic_NodeInfoLite_next_hop_tag 12
#define meshtastic_NodeInfoLite_bitfield_tag 13
#define meshtastic_NodeDetail_num_tag 1
#define meshtastic_NodeDetail_macaddr_tag 2
#define meshtastic_NodeDetail_long_name_tag 3
#define meshtastic_NodeDetail_short_name_tag 4
#define meshtastic_NodeDetail_hw_model_tag 5
#define meshtastic_NodeDetail_role_tag 6
#define meshtastic_NodeDetail_public_key_tag 7
#define meshtastic_NodeDetail_latitude_i_tag 8
#define meshtastic_NodeDetail_longitude_i_tag 9
#define meshtastic_NodeDetail_altitude_tag 10
#define meshtastic_NodeDetail_position_time_tag 11
#define meshtastic_NodeDetail_position_source_tag 12
#define meshtastic_NodeDetail_snr_tag 13
#define meshtastic_NodeDetail_last_heard_tag 14
#define meshtastic_NodeDetail_channel_tag 15
#define meshtastic_NodeDetail_hops_away_tag 16
#define meshtastic_NodeDetail_next_hop_tag 17
#define meshtastic_NodeDetail_battery_level_tag 18
#define meshtastic_NodeDetail_uptime_seconds_tag 19
#define meshtastic_NodeDetail_channel_utilization_permille_tag 20
#define meshtastic_NodeDetail_air_util_tx_permille_tag 21
#define meshtastic_NodeDetail_voltage_millivolts_tag 22
#define meshtastic_NodeDetail_flags_tag 23
#define meshtastic_DeviceState_my_node_tag 2
#define meshtastic_DeviceState_owner_tag 3
#define meshtastic_DeviceState_receive_queue_tag 5
@@ -292,6 +360,33 @@ X(a, STATIC, SINGULAR, UINT32, bitfield, 13)
#define meshtastic_NodeInfoLite_position_MSGTYPE meshtastic_PositionLite
#define meshtastic_NodeInfoLite_device_metrics_MSGTYPE meshtastic_DeviceMetrics
#define meshtastic_NodeDetail_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, num, 1) \
X(a, STATIC, SINGULAR, FIXED_LENGTH_BYTES, macaddr, 2) \
X(a, STATIC, SINGULAR, STRING, long_name, 3) \
X(a, STATIC, SINGULAR, STRING, short_name, 4) \
X(a, STATIC, SINGULAR, UENUM, hw_model, 5) \
X(a, STATIC, SINGULAR, UENUM, role, 6) \
X(a, STATIC, SINGULAR, BYTES, public_key, 7) \
X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 8) \
X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 9) \
X(a, STATIC, SINGULAR, INT32, altitude, 10) \
X(a, STATIC, SINGULAR, FIXED32, position_time, 11) \
X(a, STATIC, SINGULAR, UENUM, position_source, 12) \
X(a, STATIC, SINGULAR, FLOAT, snr, 13) \
X(a, STATIC, SINGULAR, FIXED32, last_heard, 14) \
X(a, STATIC, SINGULAR, UINT32, channel, 15) \
X(a, STATIC, SINGULAR, UINT32, hops_away, 16) \
X(a, STATIC, SINGULAR, UINT32, next_hop, 17) \
X(a, STATIC, SINGULAR, UINT32, battery_level, 18) \
X(a, STATIC, SINGULAR, UINT32, uptime_seconds, 19) \
X(a, STATIC, SINGULAR, UINT32, channel_utilization_permille, 20) \
X(a, STATIC, SINGULAR, UINT32, air_util_tx_permille, 21) \
X(a, STATIC, SINGULAR, UINT32, voltage_millivolts, 22) \
X(a, STATIC, SINGULAR, UINT32, flags, 23)
#define meshtastic_NodeDetail_CALLBACK NULL
#define meshtastic_NodeDetail_DEFAULT NULL
#define meshtastic_DeviceState_FIELDLIST(X, a) \
X(a, STATIC, OPTIONAL, MESSAGE, my_node, 2) \
X(a, STATIC, OPTIONAL, MESSAGE, owner, 3) \
@@ -317,7 +412,7 @@ X(a, CALLBACK, REPEATED, MESSAGE, nodes, 2)
extern bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field);
#define meshtastic_NodeDatabase_CALLBACK meshtastic_NodeDatabase_callback
#define meshtastic_NodeDatabase_DEFAULT NULL
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeInfoLite
#define meshtastic_NodeDatabase_nodes_MSGTYPE meshtastic_NodeDetail
#define meshtastic_ChannelFile_FIELDLIST(X, a) \
X(a, STATIC, REPEATED, MESSAGE, channels, 1) \
@@ -343,6 +438,7 @@ X(a, STATIC, OPTIONAL, MESSAGE, owner, 6)
extern const pb_msgdesc_t meshtastic_PositionLite_msg;
extern const pb_msgdesc_t meshtastic_UserLite_msg;
extern const pb_msgdesc_t meshtastic_NodeInfoLite_msg;
extern const pb_msgdesc_t meshtastic_NodeDetail_msg;
extern const pb_msgdesc_t meshtastic_DeviceState_msg;
extern const pb_msgdesc_t meshtastic_NodeDatabase_msg;
extern const pb_msgdesc_t meshtastic_ChannelFile_msg;
@@ -352,6 +448,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define meshtastic_PositionLite_fields &meshtastic_PositionLite_msg
#define meshtastic_UserLite_fields &meshtastic_UserLite_msg
#define meshtastic_NodeInfoLite_fields &meshtastic_NodeInfoLite_msg
#define meshtastic_NodeDetail_fields &meshtastic_NodeDetail_msg
#define meshtastic_DeviceState_fields &meshtastic_DeviceState_msg
#define meshtastic_NodeDatabase_fields &meshtastic_NodeDatabase_msg
#define meshtastic_ChannelFile_fields &meshtastic_ChannelFile_msg
@@ -363,6 +460,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
#define meshtastic_BackupPreferences_size 2277
#define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737
#define meshtastic_NodeDetail_size 182
#define meshtastic_NodeInfoLite_size 196
#define meshtastic_PositionLite_size 28
#define meshtastic_UserLite_size 98

View File

@@ -288,6 +288,12 @@ typedef enum _meshtastic_HardwareModel {
meshtastic_HardwareModel_WISMESH_TAP_V2 = 116,
/* RAK3401 */
meshtastic_HardwareModel_RAK3401 = 117,
/* RAK6421 Hat+ */
meshtastic_HardwareModel_RAK6421 = 118,
/* Elecrow ThinkNode M4 */
meshtastic_HardwareModel_THINKNODE_M4 = 119,
/* Elecrow ThinkNode M6 */
meshtastic_HardwareModel_THINKNODE_M6 = 120,
/* ------------------------------------------------------------------------------------------------------------------------------------------
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
------------------------------------------------------------------------------------------------------------------------------------------ */
@@ -825,7 +831,11 @@ typedef struct _meshtastic_MeshPacket {
Note: Our crypto implementation uses this field as well.
See [crypto](/docs/overview/encryption) for details. */
uint32_t from;
/* The (immediate) destination for this packet */
/* The (immediate) destination for this packet
If the value is 4,294,967,295 (maximum value of an unsigned 32bit integer), this indicates that the packet was
not destined for a specific node, but for a channel as indicated by the value of `channel` below.
If the value is another, this indicates that the packet was destined for a specific
node (i.e. a kind of "Direct Message" to this node) and not broadcast on a channel. */
uint32_t to;
/* (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on.
If unset, packet was on the primary channel.

View File

@@ -738,9 +738,9 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
JSONArray nodesArray;
uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
const meshtastic_NodeDetail *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user) {
if (detailHasFlag(*tempNodeInfo, NODEDETAIL_FLAG_HAS_USER)) {
JSONObject node;
char id[16];
@@ -748,26 +748,25 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
node["id"] = new JSONValue(id);
node["snr"] = new JSONValue(tempNodeInfo->snr);
node["via_mqtt"] = new JSONValue(BoolToString(tempNodeInfo->via_mqtt));
node["via_mqtt"] = new JSONValue(BoolToString(detailViaMqtt(*tempNodeInfo)));
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
node["position"] = new JSONValue();
if (nodeDB->hasValidPosition(tempNodeInfo)) {
JSONObject position;
position["latitude"] = new JSONValue((float)tempNodeInfo->position.latitude_i * 1e-7);
position["longitude"] = new JSONValue((float)tempNodeInfo->position.longitude_i * 1e-7);
position["altitude"] = new JSONValue((int)tempNodeInfo->position.altitude);
position["latitude"] = new JSONValue(static_cast<float>(tempNodeInfo->latitude_i) * 1e-7f);
position["longitude"] = new JSONValue(static_cast<float>(tempNodeInfo->longitude_i) * 1e-7f);
position["altitude"] = new JSONValue(static_cast<int>(tempNodeInfo->altitude));
node["position"] = new JSONValue(position);
}
node["long_name"] = new JSONValue(tempNodeInfo->user.long_name);
node["short_name"] = new JSONValue(tempNodeInfo->user.short_name);
node["long_name"] = new JSONValue(tempNodeInfo->long_name);
node["short_name"] = new JSONValue(tempNodeInfo->short_name);
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->user.macaddr[0],
tempNodeInfo->user.macaddr[1], tempNodeInfo->user.macaddr[2], tempNodeInfo->user.macaddr[3],
tempNodeInfo->user.macaddr[4], tempNodeInfo->user.macaddr[5]);
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", tempNodeInfo->macaddr[0], tempNodeInfo->macaddr[1],
tempNodeInfo->macaddr[2], tempNodeInfo->macaddr[3], tempNodeInfo->macaddr[4], tempNodeInfo->macaddr[5]);
node["mac_address"] = new JSONValue(macStr);
node["hw_model"] = new JSONValue(tempNodeInfo->user.hw_model);
node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
nodesArray.push_back(new JSONValue(node));
}

View File

@@ -39,7 +39,7 @@
/// Verify baseline assumption of node size. If it increases, we need to reevaluate
/// the impact of its memory footprint, notably on MAX_NUM_NODES.
static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increased. Reconsider impact on MAX_NUM_NODES.");
static_assert(sizeof(meshtastic_NodeDetail) <= 200, "NodeDetail size increased. Reconsider impact on MAX_NUM_NODES.");
/// max number of nodes allowed in the nodeDB
#ifndef MAX_NUM_NODES

View File

@@ -107,14 +107,14 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
// Automatically favorite the node that is using the admin key
auto remoteNode = nodeDB->getMeshNode(mp.from);
if (remoteNode && !remoteNode->is_favorite) {
if (remoteNode && !detailIsFavorite(*remoteNode)) {
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
// without the user doing so deliberately.
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
} else {
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
remoteNode->is_favorite = true;
nodeDB->set_favorite(true, mp.from);
}
}
} else {
@@ -341,9 +341,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
}
case meshtastic_AdminMessage_set_favorite_node_tag: {
LOG_INFO("Client received set_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(r->set_favorite_node);
if (node != NULL) {
node->is_favorite = true;
detailSetFlag(*node, NODEDETAIL_FLAG_IS_FAVORITE, true);
saveChanges(SEGMENT_NODEDATABASE, false);
if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
@@ -352,9 +352,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
}
case meshtastic_AdminMessage_remove_favorite_node_tag: {
LOG_INFO("Client received remove_favorite_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(r->remove_favorite_node);
if (node != NULL) {
node->is_favorite = false;
detailSetFlag(*node, NODEDETAIL_FLAG_IS_FAVORITE, false);
saveChanges(SEGMENT_NODEDATABASE, false);
if (screen)
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
@@ -363,31 +363,32 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
}
case meshtastic_AdminMessage_set_ignored_node_tag: {
LOG_INFO("Client received set_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(r->set_ignored_node);
if (node != NULL) {
node->is_ignored = true;
node->has_device_metrics = false;
node->has_position = false;
node->user.public_key.size = 0;
node->user.public_key.bytes[0] = 0;
detailSetFlag(*node, NODEDETAIL_FLAG_IS_IGNORED, true);
clearMetricsFromDetail(*node);
clearPositionFromDetail(*node);
node->public_key.size = 0;
memset(node->public_key.bytes, 0, sizeof(node->public_key.bytes));
saveChanges(SEGMENT_NODEDATABASE, false);
}
break;
}
case meshtastic_AdminMessage_remove_ignored_node_tag: {
LOG_INFO("Client received remove_ignored_node command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(r->remove_ignored_node);
if (node != NULL) {
node->is_ignored = false;
detailSetFlag(*node, NODEDETAIL_FLAG_IS_IGNORED, false);
saveChanges(SEGMENT_NODEDATABASE, false);
}
break;
}
case meshtastic_AdminMessage_set_fixed_position_tag: {
LOG_INFO("Client received set_fixed_position command");
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
node->has_position = true;
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node) {
applyPositionToDetail(*node, r->set_fixed_position);
}
nodeDB->setLocalPosition(r->set_fixed_position);
config.position.fixed_position = true;
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
@@ -773,6 +774,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
config.lora = validatedLora;
// If we're setting region for the first time, init the region and regenerate the keys
if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
if (!owner.is_licensed) {
bool keygenSuccess = false;
if (config.security.private_key.size == 32) {
@@ -791,6 +793,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
}
}
#endif
config.lora.tx_enabled = true;
initRegion();
if (myRegion->dutyCycle < 100) {

View File

@@ -131,9 +131,9 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t
}
static bool returnToCannedList = false;
bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
bool hasKeyForNode(const meshtastic_NodeDetail *node)
{
return node && node->has_user && node->user.public_key.size > 0;
return node && detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER) && node->public_key.size > 0;
}
/**
* @brief Items in array this->messages will be set to be pointing on the right
@@ -254,11 +254,11 @@ void CannedMessageModule::updateDestinationSelectionList()
this->filteredNodes.reserve(numMeshNodes);
for (size_t i = 0; i < numMeshNodes; ++i) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32)
meshtastic_NodeDetail *node = nodeDB->getMeshNodeByIndex(i);
if (!node || node->num == myNodeNum || !detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER) || node->public_key.size != 32)
continue;
const String &nodeName = node->user.long_name;
const String nodeName = node->long_name;
if (searchQuery.length() == 0) {
this->filteredNodes.push_back({node, sinceLastSeen(node)});
@@ -525,7 +525,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event
} else {
int nodeIndex = destIndex - static_cast<int>(activeChannelIndices.size());
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(filteredNodes.size())) {
const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node;
const meshtastic_NodeDetail *selectedNode = filteredNodes[nodeIndex].node;
if (selectedNode) {
dest = selectedNode->num;
channel = selectedNode->channel;
@@ -1253,9 +1253,9 @@ const char *CannedMessageModule::getNodeName(NodeNum node)
if (node == NODENUM_BROADCAST)
return "Broadcast";
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user && strlen(info->user.long_name) > 0) {
return info->user.long_name;
meshtastic_NodeDetail *info = nodeDB->getMeshNode(node);
if (info && detailHasFlag(*info, NODEDETAIL_FLAG_HAS_USER) && strlen(info->long_name) > 0) {
return info->long_name;
}
static char fallback[12];
@@ -1565,18 +1565,18 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
else {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
meshtastic_NodeDetail *node = this->filteredNodes[nodeIndex].node;
if (node) {
if (node->is_favorite) {
if (detailIsFavorite(*node)) {
#if defined(M5STACK_UNITC6L)
snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name);
snprintf(entryText, sizeof(entryText), "* %s", node->short_name);
} else {
snprintf(entryText, sizeof(entryText), "%s", node->user.short_name);
snprintf(entryText, sizeof(entryText), "%s", node->short_name);
}
#else
snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name);
snprintf(entryText, sizeof(entryText), "* %s", node->long_name);
} else {
snprintf(entryText, sizeof(entryText), "%s", node->user.long_name);
snprintf(entryText, sizeof(entryText), "%s", node->long_name);
}
#endif
}
@@ -1601,7 +1601,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
if (itemIndex >= numActiveChannels) {
int nodeIndex = itemIndex - numActiveChannels;
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
const meshtastic_NodeDetail *node = this->filteredNodes[nodeIndex].node;
if (node && hasKeyForNode(node)) {
int iconX = display->getWidth() - key_symbol_width - 15;
int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2;

View File

@@ -45,7 +45,7 @@ struct Letter {
};
struct NodeEntry {
meshtastic_NodeInfoLite *node;
meshtastic_NodeDetail *node;
uint32_t lastHeard;
};

View File

@@ -1,6 +1,7 @@
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RTC.h"
#include "graphics/draw/MenuHandler.h"
#include "main.h"
@@ -36,8 +37,9 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
request->key_verification.nonce == currentNonce) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
if (auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode)) {
detailSetFlag(*remoteNodePtr, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED);
}
resetToIdle();
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
resetToIdle();
@@ -72,8 +74,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
sprintf(cn->message, "Enter Security Number for Key Verification");
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag;
cn->payload_variant.key_verification_number_request.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
const meshtastic_NodeDetail *remoteNode = nodeDB->getMeshNode(currentRemoteNode);
const char *remoteLongName = (remoteNode && remoteNode->long_name[0] != '\0') ? remoteNode->long_name : "";
strncpy(cn->payload_variant.key_verification_number_request.remote_longname, remoteLongName,
sizeof(cn->payload_variant.key_verification_number_request.remote_longname));
service->sendClientNotification(cn);
LOG_INFO("Received hash2");
@@ -94,8 +97,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
options.bannerCallback =
[=](int selected) {
if (selected == 1) {
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
if (auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode)) {
detailSetFlag(*remoteNodePtr, NODEDETAIL_FLAG_IS_KEY_MANUALLY_VERIFIED);
}
}
};
screen->showOverlayBanner(options);)
@@ -104,8 +108,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
const meshtastic_NodeDetail *remoteNodeFinal = nodeDB->getMeshNode(currentRemoteNode);
const char *remoteFinalLongName =
(remoteNodeFinal && remoteNodeFinal->long_name[0] != '\0') ? remoteNodeFinal->long_name : "";
strncpy(cn->payload_variant.key_verification_final.remote_longname, remoteFinalLongName,
sizeof(cn->payload_variant.key_verification_final.remote_longname));
cn->payload_variant.key_verification_final.isSender = false;
service->sendClientNotification(cn);
@@ -202,8 +208,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
currentSecurityNumber % 1000);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag;
cn->payload_variant.key_verification_number_inform.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
meshtastic_NodeDetail *remoteNode = nodeDB->getMeshNode(currentRemoteNode);
const char *remoteName = (remoteNode && detailHasFlag(*remoteNode, NODEDETAIL_FLAG_HAS_USER)) ? remoteNode->long_name : "";
strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, remoteName,
sizeof(cn->payload_variant.key_verification_number_inform.remote_longname));
cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber;
service->sendClientNotification(cn);
@@ -217,9 +224,9 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
NodeNum ourNodeNum = nodeDB->getNodeNum();
uint8_t scratch_hash[32] = {0};
LOG_WARN("received security number: %u", incomingNumber);
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) {
meshtastic_NodeDetail *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr == nullptr || !detailHasFlag(*remoteNodePtr, NODEDETAIL_FLAG_HAS_USER) ||
remoteNodePtr->public_key.size != 32) {
currentState = KEY_VERIFICATION_IDLE;
return; // should we throw an error here?
}
@@ -232,7 +239,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(owner.public_key.bytes, owner.public_key.size);
hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size);
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
hash.finalize(hash1, 32);
hash.reset();
@@ -265,8 +272,10 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);
cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag;
cn->payload_variant.key_verification_final.nonce = currentNonce;
strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc
nodeDB->getMeshNode(currentRemoteNode)->user.long_name,
meshtastic_NodeDetail *remoteNodeFinal = nodeDB->getMeshNode(currentRemoteNode);
const char *finalRemoteName =
(remoteNodeFinal && detailHasFlag(*remoteNodeFinal, NODEDETAIL_FLAG_HAS_USER)) ? remoteNodeFinal->long_name : "";
strncpy(cn->payload_variant.key_verification_final.remote_longname, finalRemoteName,
sizeof(cn->payload_variant.key_verification_final.remote_longname));
cn->payload_variant.key_verification_final.isSender = true;
service->sendClientNotification(cn);

View File

@@ -169,8 +169,9 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
return nullptr;
}
meshtastic_NodeInfoLite *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
assert(node->has_position);
meshtastic_NodeDetail *node = service->refreshLocalMeshNode(); // should guarantee there is now a position
assert(detailHasFlag(*node, NODEDETAIL_FLAG_HAS_POSITION));
meshtastic_PositionLite nodePosition = detailToPositionLite(*node);
// configuration of POSITION packet
// consider making this a function argument?
@@ -180,7 +181,7 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket()
meshtastic_Position p = meshtastic_Position_init_default; // Start with an empty structure
// if localPosition is totally empty, put our last saved position (lite) in there
if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) {
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(node->position));
nodeDB->setLocalPosition(TypeConversions::ConvertToPosition(nodePosition));
}
localPosition.seq_number++;
@@ -401,7 +402,7 @@ int32_t PositionModule::runOnce()
doDeepSleep(nightyNightMs, false, false);
}
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (node == nullptr)
return RUNONCE_INTERVAL;
@@ -420,8 +421,9 @@ int32_t PositionModule::runOnce()
if (nodeDB->hasValidPosition(node)) {
lastGpsSend = now;
lastGpsLatitude = node->position.latitude_i;
lastGpsLongitude = node->position.longitude_i;
meshtastic_PositionLite nodePosition = detailToPositionLite(*node);
lastGpsLatitude = nodePosition.latitude_i;
lastGpsLongitude = nodePosition.longitude_i;
sendOurPosition();
if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
@@ -429,12 +431,11 @@ int32_t PositionModule::runOnce()
}
}
} else if (config.position.position_broadcast_smart_enabled) {
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
const meshtastic_NodeDetail *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
if (nodeDB->hasValidPosition(node2)) {
// The minimum time (in seconds) that would pass before we are able to send a new position packet.
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
auto smartPosition = getDistanceTraveledSinceLastSend(detailToPositionLite(*node));
msSinceLastSend = now - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold &&
@@ -448,8 +449,9 @@ int32_t PositionModule::runOnce()
msSinceLastSend, minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i;
lastGpsLongitude = node->position.longitude_i;
meshtastic_PositionLite nodePosition = detailToPositionLite(*node);
lastGpsLatitude = nodePosition.latitude_i;
lastGpsLongitude = nodePosition.longitude_i;
}
}
}
@@ -489,11 +491,11 @@ struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic
void PositionModule::handleNewPosition()
{
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
const meshtastic_NodeInfoLite *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
meshtastic_NodeDetail *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
const meshtastic_NodeDetail *node2 = service->refreshLocalMeshNode(); // should guarantee there is now a position
// We limit our GPS broadcasts to a max rate
if (nodeDB->hasValidPosition(node2)) {
auto smartPosition = getDistanceTraveledSinceLastSend(node->position);
auto smartPosition = getDistanceTraveledSinceLastSend(detailToPositionLite(*node));
uint32_t msSinceLastSend = millis() - lastGpsSend;
if (smartPosition.hasTraveledOverThreshold &&
Throttle::execute(
@@ -505,8 +507,9 @@ void PositionModule::handleNewPosition()
minimumTimeThreshold);
// Set the current coords as our last ones, after we've compared distance with current and decided to send
lastGpsLatitude = node->position.latitude_i;
lastGpsLongitude = node->position.longitude_i;
meshtastic_PositionLite nodePosition = detailToPositionLite(*node);
lastGpsLatitude = nodePosition.latitude_i;
lastGpsLongitude = nodePosition.longitude_i;
}
}
}

View File

@@ -151,7 +151,7 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket
}
/*
NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
meshtastic_NodeDetail *n = nodeDB->getMeshNode(getFrom(&mp));
LOG_DEBUG("-----------------------------------------");
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
@@ -189,7 +189,7 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
#ifdef ARCH_ESP32
auto &p = mp.decoded;
meshtastic_NodeInfoLite *n = nodeDB->getMeshNode(getFrom(&mp));
meshtastic_NodeDetail *n = nodeDB->getMeshNode(getFrom(&mp));
/*
LOG_DEBUG("-----------------------------------------");
LOG_DEBUG("p.payload.bytes \"%s\"", p.payload.bytes);
@@ -268,27 +268,33 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp)
fileToAppend.printf("??:??:??,"); // Time
}
fileToAppend.printf("%d,", getFrom(&mp)); // From
fileToAppend.printf("%s,", n->user.long_name); // Long Name
fileToAppend.printf("%f,", n->position.latitude_i * 1e-7); // Sender Lat
fileToAppend.printf("%f,", n->position.longitude_i * 1e-7); // Sender Long
fileToAppend.printf("%d,", getFrom(&mp)); // From
const char *senderName = (n && strlen(n->long_name) > 0) ? n->long_name : "?";
fileToAppend.printf("%s,", senderName); // Long Name
double senderLat = (n && detailHasFlag(*n, NODEDETAIL_FLAG_HAS_POSITION)) ? n->latitude_i * 1e-7 : 0.0;
double senderLon = (n && detailHasFlag(*n, NODEDETAIL_FLAG_HAS_POSITION)) ? n->longitude_i * 1e-7 : 0.0;
fileToAppend.printf("%f,", senderLat); // Sender Lat
fileToAppend.printf("%f,", senderLon); // Sender Long
if (gpsStatus->getIsConnected() || config.position.fixed_position) {
fileToAppend.printf("%f,", gpsStatus->getLatitude() * 1e-7); // RX Lat
fileToAppend.printf("%f,", gpsStatus->getLongitude() * 1e-7); // RX Long
fileToAppend.printf("%d,", gpsStatus->getAltitude()); // RX Altitude
} else {
// When the phone API is in use, the node info will be updated with position
meshtastic_NodeInfoLite *us = nodeDB->getMeshNode(nodeDB->getNodeNum());
fileToAppend.printf("%f,", us->position.latitude_i * 1e-7); // RX Lat
fileToAppend.printf("%f,", us->position.longitude_i * 1e-7); // RX Long
fileToAppend.printf("%d,", us->position.altitude); // RX Altitude
meshtastic_NodeDetail *us = nodeDB->getMeshNode(nodeDB->getNodeNum());
double rxLat = (us && detailHasFlag(*us, NODEDETAIL_FLAG_HAS_POSITION)) ? us->latitude_i * 1e-7 : 0.0;
double rxLon = (us && detailHasFlag(*us, NODEDETAIL_FLAG_HAS_POSITION)) ? us->longitude_i * 1e-7 : 0.0;
int32_t rxAlt = (us && detailHasFlag(*us, NODEDETAIL_FLAG_HAS_POSITION)) ? us->altitude : 0;
fileToAppend.printf("%f,", rxLat); // RX Lat
fileToAppend.printf("%f,", rxLon); // RX Long
fileToAppend.printf("%d,", rxAlt); // RX Altitude
}
fileToAppend.printf("%f,", mp.rx_snr); // RX SNR
if (n->position.latitude_i && n->position.longitude_i && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
float distance = GeoCoord::latLongToMeter(n->position.latitude_i * 1e-7, n->position.longitude_i * 1e-7,
gpsStatus->getLatitude() * 1e-7, gpsStatus->getLongitude() * 1e-7);
if (n && detailHasFlag(*n, NODEDETAIL_FLAG_HAS_POSITION) && gpsStatus->getLatitude() && gpsStatus->getLongitude()) {
float distance = GeoCoord::latLongToMeter(n->latitude_i * 1e-7, n->longitude_i * 1e-7, gpsStatus->getLatitude() * 1e-7,
gpsStatus->getLongitude() * 1e-7);
fileToAppend.printf("%f,", distance); // Distance in meters
} else {
fileToAppend.printf("0,");

View File

@@ -17,8 +17,10 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh
config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY)) {
if (!maybePKI)
return false;
if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) &&
(nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user))
const meshtastic_NodeDetail *fromDetail = nodeDB->getMeshNode(mp.from);
const meshtastic_NodeDetail *toDetail = nodeDB->getMeshNode(mp.to);
if ((!fromDetail || !detailHasFlag(*fromDetail, NODEDETAIL_FLAG_HAS_USER)) &&
(!toDetail || !detailHasFlag(*toDetail, NODEDETAIL_FLAG_HAS_USER)))
return false;
} else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) {
// Don't let licensed users to rebroadcast packets from unlicensed users

View File

@@ -84,6 +84,42 @@ SerialModule::SerialModule() : StreamAPI(&Serial2), concurrency::OSThread("Seria
static Print *serialPrint = &Serial2;
#endif
namespace
{
const char *resolveNodeName(const meshtastic_NodeDetail *node, char *buffer, size_t bufferSize, bool preferLongName)
{
if (!node) {
return "???";
}
if (detailHasFlag(*node, NODEDETAIL_FLAG_HAS_USER)) {
if (preferLongName) {
if (node->long_name[0]) {
return node->long_name;
}
if (node->short_name[0]) {
return node->short_name;
}
} else {
if (node->short_name[0]) {
return node->short_name;
}
if (node->long_name[0]) {
return node->long_name;
}
}
}
if (buffer && bufferSize > 0) {
snprintf(buffer, bufferSize, "(%04X)", static_cast<unsigned int>(node->num & 0xFFFF));
buffer[bufferSize - 1] = '\0';
return buffer;
}
return "???";
}
} // namespace
char serialBytes[512];
size_t serialPayloadSize;
@@ -249,13 +285,15 @@ int32_t SerialModule::runOnce()
if (!Throttle::isWithinTimespanMs(lastNmeaTime, 10000)) {
lastNmeaTime = millis();
uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) {
if (tempNodeInfo->has_user && nodeDB->hasValidPosition(tempNodeInfo)) {
printWPL(outbuf, sizeof(outbuf), tempNodeInfo->position, tempNodeInfo->user.long_name, true);
const meshtastic_NodeDetail *tempNode = nodeDB->readNextMeshNode(readIndex);
while (tempNode != NULL) {
if (detailHasFlag(*tempNode, NODEDETAIL_FLAG_HAS_USER) && nodeDB->hasValidPosition(tempNode)) {
char nameBuffer[12] = {0};
const char *name = resolveNodeName(tempNode, nameBuffer, sizeof(nameBuffer), true);
printWPL(outbuf, sizeof(outbuf), detailToPositionLite(*tempNode), name, true);
serialPrint->printf("%s", outbuf);
}
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
tempNode = nodeDB->readNextMeshNode(readIndex);
}
}
}
@@ -402,8 +440,9 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE) {
serialPrint->write(p.payload.bytes, p.payload.size);
} else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
meshtastic_NodeDetail *node = nodeDB->getMeshNode(getFrom(&mp));
char senderBuffer[12] = {0};
const char *sender = resolveNodeName(node, senderBuffer, sizeof(senderBuffer), false);
serialPrint->println();
serialPrint->printf("%s: %s", sender, p.payload.bytes);
serialPrint->println();
@@ -419,7 +458,10 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp
decoded = &scratch;
}
// send position packet as WPL to the serial port
printWPL(outbuf, sizeof(outbuf), *decoded, nodeDB->getMeshNode(getFrom(&mp))->user.long_name,
meshtastic_NodeDetail *node = nodeDB->getMeshNode(getFrom(&mp));
char nameBuffer[12] = {0};
const char *name = resolveNodeName(node, nameBuffer, sizeof(nameBuffer), true);
printWPL(outbuf, sizeof(outbuf), *decoded, name,
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO);
serialPrint->printf("%s", outbuf);
}

View File

@@ -85,10 +85,8 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
switch (event->inputEvent) {
// GPS
case INPUT_BROKER_GPS_TOGGLE:
LOG_WARN("GPS Toggle");
#if !MESHTASTIC_EXCLUDE_GPS
if (gps) {
LOG_WARN("GPS Toggle2");
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
config.position.fixed_position == false) {
nodeDB->clearLocalPosition();

View File

@@ -13,7 +13,10 @@ DFRobotGravitySensor::DFRobotGravitySensor() : TelemetrySensor(meshtastic_Teleme
DFRobotGravitySensor::~DFRobotGravitySensor()
{
if (gravity) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdelete-non-virtual-dtor"
delete gravity;
#pragma GCC diagnostic pop
gravity = nullptr;
}
}

View File

@@ -11,6 +11,113 @@ extern graphics::Screen *screen;
TraceRouteModule *traceRouteModule;
void TraceRouteModule::setResultText(const String &text)
{
resultText = text;
resultLines.clear();
resultLinesDirty = true;
}
void TraceRouteModule::clearResultLines()
{
resultLines.clear();
resultLinesDirty = false;
}
#if HAS_SCREEN
void TraceRouteModule::rebuildResultLines(OLEDDisplay *display)
{
if (!display) {
resultLinesDirty = false;
return;
}
resultLines.clear();
if (resultText.length() == 0) {
resultLinesDirty = false;
return;
}
int maxWidth = display->getWidth() - 4;
if (maxWidth <= 0) {
resultLinesDirty = false;
return;
}
int start = 0;
int textLength = resultText.length();
while (start <= textLength) {
int newlinePos = resultText.indexOf('\n', start);
String segment;
if (newlinePos != -1) {
segment = resultText.substring(start, newlinePos);
start = newlinePos + 1;
} else {
segment = resultText.substring(start);
start = textLength + 1;
}
if (segment.length() == 0) {
resultLines.push_back("");
continue;
}
if (display->getStringWidth(segment) <= maxWidth) {
resultLines.push_back(segment);
continue;
}
String remaining = segment;
while (remaining.length() > 0) {
String tempLine = "";
int lastGoodBreak = -1;
bool lineComplete = false;
for (int i = 0; i < static_cast<int>(remaining.length()); i++) {
char ch = remaining.charAt(i);
String testLine = tempLine + ch;
if (display->getStringWidth(testLine) > maxWidth) {
if (lastGoodBreak >= 0) {
resultLines.push_back(remaining.substring(0, lastGoodBreak + 1));
remaining = remaining.substring(lastGoodBreak + 1);
lineComplete = true;
break;
} else if (tempLine.length() > 0) {
resultLines.push_back(tempLine);
remaining = remaining.substring(i);
lineComplete = true;
break;
} else {
resultLines.push_back(String(ch));
remaining = remaining.substring(i + 1);
lineComplete = true;
break;
}
} else {
tempLine = testLine;
if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')' || ch == ',') {
lastGoodBreak = i;
}
}
}
if (!lineComplete) {
if (tempLine.length() > 0) {
resultLines.push_back(tempLine);
}
break;
}
}
}
resultLinesDirty = false;
}
#endif
bool TraceRouteModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r)
{
// We only alter the packet in alterReceivedProtobuf()
@@ -210,7 +317,7 @@ void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte)
if (target == NODENUM_BROADCAST)
return;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(target);
meshtastic_NodeDetail *node = nodeDB->getMeshNode(target);
if (node && node->next_hop != nextHopByte) {
LOG_INFO("Updating next-hop for 0x%08x to 0x%02x based on traceroute", target, nextHopByte);
node->next_hop = nextHopByte;
@@ -383,13 +490,13 @@ TraceRouteModule::TraceRouteModule()
const char *TraceRouteModule::getNodeName(NodeNum node)
{
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
if (info && info->has_user) {
if (strlen(info->user.short_name) > 0) {
return info->user.short_name;
const meshtastic_NodeDetail *info = nodeDB->getMeshNode(node);
if (info && detailHasFlag(*info, NODEDETAIL_FLAG_HAS_USER)) {
if (strlen(info->short_name) > 0) {
return info->short_name;
}
if (strlen(info->user.long_name) > 0) {
return info->user.long_name;
if (strlen(info->long_name) > 0) {
return info->long_name;
}
}
@@ -406,7 +513,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
if (node == 0 || node == NODENUM_BROADCAST) {
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
runState = TRACEROUTE_STATE_RESULT;
resultText = "Invalid node";
setResultText("Invalid node");
resultShowTime = millis();
tracingNode = 0;
@@ -420,7 +527,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
if (node == nodeDB->getNodeNum()) {
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
runState = TRACEROUTE_STATE_RESULT;
resultText = "Cannot trace self";
setResultText("Cannot trace self");
resultShowTime = millis();
tracingNode = 0;
@@ -447,6 +554,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
bannerText = String("Wait for ") + String(wait) + String("s");
runState = TRACEROUTE_STATE_COOLDOWN;
resultText = "";
clearResultLines();
requestFocus();
UIFrameEvent e;
@@ -459,6 +568,8 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
tracingNode = node;
lastTraceRouteTime = now;
runState = TRACEROUTE_STATE_TRACKING;
resultText = "";
clearResultLines();
bannerText = String("Tracing ") + getNodeName(node);
LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node);
@@ -501,7 +612,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
} else {
LOG_ERROR("MeshService is NULL!");
runState = TRACEROUTE_STATE_RESULT;
resultText = "Service unavailable";
setResultText("Service unavailable");
resultShowTime = millis();
tracingNode = 0;
@@ -514,7 +625,7 @@ bool TraceRouteModule::startTraceRoute(NodeNum node)
} else {
LOG_ERROR("Failed to allocate TraceRoute packet from router");
runState = TRACEROUTE_STATE_RESULT;
resultText = "Failed to send";
setResultText("Failed to send");
resultShowTime = millis();
tracingNode = 0;
@@ -532,7 +643,7 @@ void TraceRouteModule::launch(NodeNum node)
if (node == 0 || node == NODENUM_BROADCAST) {
LOG_ERROR("Invalid node number for trace route: 0x%08x", node);
runState = TRACEROUTE_STATE_RESULT;
resultText = "Invalid node";
setResultText("Invalid node");
resultShowTime = millis();
tracingNode = 0;
@@ -546,7 +657,7 @@ void TraceRouteModule::launch(NodeNum node)
if (node == nodeDB->getNodeNum()) {
LOG_ERROR("Cannot trace route to self: 0x%08x", node);
runState = TRACEROUTE_STATE_RESULT;
resultText = "Cannot trace self";
setResultText("Cannot trace self");
resultShowTime = millis();
tracingNode = 0;
@@ -568,6 +679,8 @@ void TraceRouteModule::launch(NodeNum node)
unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000;
bannerText = String("Wait for ") + String(wait) + String("s");
runState = TRACEROUTE_STATE_COOLDOWN;
resultText = "";
clearResultLines();
requestFocus();
UIFrameEvent e;
@@ -580,6 +693,8 @@ void TraceRouteModule::launch(NodeNum node)
runState = TRACEROUTE_STATE_TRACKING;
tracingNode = node;
lastTraceRouteTime = now;
resultText = "";
clearResultLines();
bannerText = String("Tracing ") + getNodeName(node);
requestFocus();
@@ -614,14 +729,14 @@ void TraceRouteModule::launch(NodeNum node)
} else {
LOG_ERROR("MeshService is NULL!");
runState = TRACEROUTE_STATE_RESULT;
resultText = "Service unavailable";
setResultText("Service unavailable");
resultShowTime = millis();
tracingNode = 0;
}
} else {
LOG_ERROR("Failed to allocate TraceRoute packet from router");
runState = TRACEROUTE_STATE_RESULT;
resultText = "Failed to send";
setResultText("Failed to send");
resultShowTime = millis();
tracingNode = 0;
}
@@ -629,7 +744,7 @@ void TraceRouteModule::launch(NodeNum node)
void TraceRouteModule::handleTraceRouteResult(const String &result)
{
resultText = result;
setResultText(result);
runState = TRACEROUTE_STATE_RESULT;
resultShowTime = millis();
tracingNode = 0;
@@ -679,83 +794,15 @@ void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state
display->setFont(FONT_SMALL);
if (resultText.length() > 0) {
std::vector<String> lines;
String currentLine = "";
int maxWidth = display->getWidth() - 4;
int start = 0;
int newlinePos = resultText.indexOf('\n', start);
while (newlinePos != -1 || start < static_cast<int>(resultText.length())) {
String segment;
if (newlinePos != -1) {
segment = resultText.substring(start, newlinePos);
start = newlinePos + 1;
newlinePos = resultText.indexOf('\n', start);
} else {
segment = resultText.substring(start);
start = resultText.length();
}
if (display->getStringWidth(segment) <= maxWidth) {
lines.push_back(segment);
} else {
// Try to break at better positions (space, >, <, -)
String remaining = segment;
while (remaining.length() > 0) {
String tempLine = "";
int lastGoodBreak = -1;
bool lineComplete = false;
for (int i = 0; i < static_cast<int>(remaining.length()); i++) {
char ch = remaining.charAt(i);
String testLine = tempLine + ch;
if (display->getStringWidth(testLine) > maxWidth) {
if (lastGoodBreak >= 0) {
// Break at the last good position
lines.push_back(remaining.substring(0, lastGoodBreak + 1));
remaining = remaining.substring(lastGoodBreak + 1);
lineComplete = true;
break;
} else if (tempLine.length() > 0) {
lines.push_back(tempLine);
remaining = remaining.substring(i);
lineComplete = true;
break;
} else {
// Single character exceeds width
lines.push_back(String(ch));
remaining = remaining.substring(i + 1);
lineComplete = true;
break;
}
} else {
tempLine = testLine;
// Mark good break positions
if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') {
lastGoodBreak = i;
}
}
}
if (!lineComplete) {
// Reached end of remaining text
if (tempLine.length() > 0) {
lines.push_back(tempLine);
}
break;
}
}
}
if (resultLinesDirty) {
rebuildResultLines(display);
}
int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing
for (size_t i = 0; i < lines.size(); i++) {
for (size_t i = 0; i < resultLines.size(); i++) {
int lineY = contentStartY + (i * lineHeight);
if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) {
display->drawString(x + 2, lineY, lines[i]);
display->drawString(x + 2, lineY, resultLines[i]);
}
}
}
@@ -779,7 +826,7 @@ int32_t TraceRouteModule::runOnce()
if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) {
LOG_INFO("TraceRoute timeout, no response received");
runState = TRACEROUTE_STATE_RESULT;
resultText = "No response received";
setResultText("No response received");
resultShowTime = now;
tracingNode = 0;
@@ -815,6 +862,8 @@ int32_t TraceRouteModule::runOnce()
// Cooldown finished
LOG_INFO("TraceRoute cooldown finished, returning to IDLE");
runState = TRACEROUTE_STATE_IDLE;
resultText = "";
clearResultLines();
bannerText = "";
UIFrameEvent e;
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
@@ -828,6 +877,7 @@ int32_t TraceRouteModule::runOnce()
LOG_INFO("TraceRoute result display timeout, returning to IDLE");
runState = TRACEROUTE_STATE_IDLE;
resultText = "";
clearResultLines();
bannerText = "";
tracingNode = 0;
UIFrameEvent e;

View File

@@ -7,6 +7,7 @@
#if HAS_SCREEN
#include "OLEDDisplayUi.h"
#endif
#include <vector>
#define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0])
@@ -49,6 +50,11 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
virtual int32_t runOnce() override;
private:
void setResultText(const String &text);
void clearResultLines();
#if HAS_SCREEN
void rebuildResultLines(OLEDDisplay *display);
#endif
// Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit
void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination);
@@ -74,6 +80,8 @@ class TraceRouteModule : public ProtobufModule<meshtastic_RouteDiscovery>,
unsigned long trackingTimeoutMs = 10000;
String bannerText;
String resultText;
std::vector<String> resultLines;
bool resultLinesDirty = false;
NodeNum tracingNode = 0;
bool initialized = false;
};

View File

@@ -115,7 +115,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
static char distStr[20];
// Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
meshtastic_NodeDetail *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// Text fields to draw (left of compass)
// Last element must be NULL. This signals the end of the char*[] to drawColumns
@@ -135,7 +135,11 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
// If our node has a position:
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
const meshtastic_PositionLite &op = ourNode->position;
meshtastic_PositionLite op = meshtastic_PositionLite_init_default;
op.latitude_i = ourNode->latitude_i;
op.longitude_i = ourNode->longitude_i;
op.altitude = ourNode->altitude;
op.location_source = ourNode->position_source;
float myHeading;
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
myHeading = 0;

View File

@@ -116,6 +116,7 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
{
#if !defined(MESHTASTIC_EXCLUDE_SCREEN)
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
doCalibration = true;
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
@@ -127,4 +128,4 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
#endif
#endif
#endif

View File

@@ -157,6 +157,7 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds)
{
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
doCalibration = true;
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
@@ -295,4 +296,4 @@ bool ICM20948Singleton::setWakeOnMotion()
return true;
}
#endif
#endif

View File

@@ -69,7 +69,8 @@ void MotionSensor::wakeScreen()
{
if (powerFSM.getState() == &stateDARK) {
LOG_DEBUG("Motion wakeScreen detected");
powerFSM.trigger(EVENT_INPUT);
if (config.display.wake_on_tap_or_motion)
powerFSM.trigger(EVENT_INPUT);
}
}
@@ -87,4 +88,4 @@ void MotionSensor::buttonPress() {}
#endif
#endif
#endif

View File

@@ -131,11 +131,12 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
// PKI messages get accepted even if we can't decrypt
if (router && p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && strcmp(e.channel_id, "PKI") == 0) {
const meshtastic_NodeInfoLite *tx = nodeDB->getMeshNode(getFrom(p.get()));
const meshtastic_NodeInfoLite *rx = nodeDB->getMeshNode(p->to);
meshtastic_NodeDetail *tx = nodeDB->getMeshNode(getFrom(p.get()));
meshtastic_NodeDetail *rx = nodeDB->getMeshNode(p->to);
// Only accept PKI messages to us, or if we have both the sender and receiver in our nodeDB, as then it's
// likely they discovered each other via a channel we have downlink enabled for
if (isToUs(p.get()) || (tx && tx->has_user && rx && rx->has_user))
if (isToUs(p.get()) ||
(tx && detailHasFlag(*tx, NODEDETAIL_FLAG_HAS_USER) && rx && detailHasFlag(*rx, NODEDETAIL_FLAG_HAS_USER)))
router->enqueueReceivedMessage(p.release());
} else if (router &&
perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key
@@ -692,7 +693,10 @@ void MQTT::publishNodeInfo()
}
void MQTT::publishQueuedMessages()
{
if (mqttQueue.isEmpty() || !isConnected)
if (mqttQueue.isEmpty())
return;
if (!moduleConfig.mqtt.proxy_to_client_enabled && !isConnected)
return;
LOG_DEBUG("Publish enqueued MQTT message");

View File

@@ -20,13 +20,14 @@
#include "PowerStatus.h"
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
#if defined(CONFIG_NIMBLE_CPP_IDF)
#include "host/ble_gap.h"
#else
#include "nimble/nimble/host/include/host/ble_gap.h"
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
namespace
{
constexpr uint16_t kPreferredBleMtu = 517;
@@ -776,16 +777,35 @@ bool NimbleBluetooth::isConnected()
int NimbleBluetooth::getRssi()
{
if (bleServer && isConnected()) {
auto service = bleServer->getServiceByUUID(MESH_SERVICE_UUID);
uint16_t handle = service->getHandle();
#ifdef NIMBLE_TWO
return NimBLEDevice::getClientByHandle(handle)->getRssi();
#else
return NimBLEDevice::getClientByID(handle)->getRssi();
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
if (!bleServer || !isConnected()) {
return 0; // No active BLE connection
}
return 0; // FIXME figure out where to source this
uint16_t connHandle = nimbleBluetoothConnHandle.load();
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
const auto peers = bleServer->getPeerDevices();
if (!peers.empty()) {
connHandle = peers.front();
nimbleBluetoothConnHandle = connHandle;
}
}
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
return 0; // Connection handle not available yet
}
int8_t rssi = 0;
const int rc = ble_gap_conn_rssi(connHandle, &rssi);
if (rc == 0) {
return rssi;
}
LOG_DEBUG("BLE RSSI read failed, rc=%d", rc);
#endif
return 0;
}
void NimbleBluetooth::setup()

View File

@@ -68,6 +68,8 @@
#define HW_VENDOR meshtastic_HardwareModel_T_ECHO_LITE
#elif defined(ELECROW_ThinkNode_M1)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M1
#elif defined(ELECROW_ThinkNode_M6)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
#elif defined(NANO_G2_ULTRA)
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
#elif defined(CANARYONE)

View File

@@ -4,6 +4,14 @@
#include <InternalFileSystem.h>
#include <SPI.h>
#include <Wire.h>
#define APP_WATCHDOG_SECS 90
#define NRFX_WDT_ENABLED 1
#define NRFX_WDT0_ENABLED 1
#define NRFX_WDT_CONFIG_NO_IRQ 1
#include <nrfx_wdt.c>
#include <nrfx_wdt.h>
#include <assert.h>
#include <ble_gap.h>
#include <memory.h>
@@ -22,6 +30,9 @@
#include "BQ25713.h"
#endif
static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0);
static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main;
static inline void debugger_break(void)
{
__asm volatile("bkpt #0x01\n\t"
@@ -205,6 +216,15 @@ void checkSDEvents()
void nrf52Loop()
{
{
static bool watchdog_running = false;
if (!watchdog_running) {
nrfx_wdt_enable(&nrfx_wdt);
watchdog_running = true;
}
}
nrfx_wdt_channel_feed(&nrfx_wdt, nrfx_wdt_channel_id_nrf52_main);
checkSDEvents();
reportLittleFSCorruptionOnce();
}
@@ -272,6 +292,22 @@ void nrf52Setup()
LOG_DEBUG("Set random seed %u", seed.seed32);
randomSeed(seed.seed32);
nRFCrypto.end();
// Set up nrfx watchdog. Do not enable the watchdog yet (we do that
// the first time through the main loop), so that other threads can
// allocate their own wdt channel to protect themselves from hangs.
nrfx_wdt_config_t wdt0_config = {
.behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000,
// Note: Not using wdt interrupts.
// .interrupt_priority = NRFX_WDT_DEFAULT_CONFIG_IRQ_PRIORITY
};
nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config,
nullptr // Watchdog event handler, not used, we just reset.
);
assert(r == NRFX_SUCCESS);
r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main);
assert(r == NRFX_SUCCESS);
}
void cpuDeepSleep(uint32_t msecToWake)

View File

@@ -34,8 +34,8 @@
#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300
#elif defined(SEEED_SOLAR_NODE)
#define OCV_ARRAY 4200, 3986, 3922, 3812, 3734, 3645, 3527, 3420, 3281, 3087, 2786
#elif defined(R1_NEO)
#define OCV_ARRAY 4330, 4292, 4254, 4216, 4178, 4140, 4102, 4064, 4026, 3988, 3950
#elif defined(WISMESH_TAG)
#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072
#else // LiIon
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
#endif
@@ -138,7 +138,7 @@ class Power : private concurrency::OSThread
void reboot();
// open circuit voltage lookup table
uint8_t low_voltage_counter;
int32_t lastLogTime = 0;
uint32_t lastLogTime = 0;
#ifdef DEBUG_HEAP
uint32_t lastheap;
#endif

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