Compare 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
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
Avi0nandJonathan Bennett 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]andcaveman99 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]andcaveman99 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 PandCopilot 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
Austin ef298814f2 CI: Submit Bump Version PR against master (#8668) 2025-11-19 16:00:13 -06:00
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
github-actions[bot]andthebentern f9433a31d1 Automated version bumps (#8684)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-11-19 12:13:28 -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
viricandTom Fifield d18f3f7a65 Allow deepsleep in rak4630 and make it restart well when power comes back (#7882)
* Make RAK4631 nodes power back on deep sleep

The devices will hang if the VBAT goes under 1.7V (Brown-out reset) and
they will never come back unless power supply goes completely off.

This kills unattended nodes.

Using the SystemOff the LPCOMP we can get the nodes back again when
power comes back, even if VBAT goes under 1.7V, which moreover is more
unlikely because the device is off.

* Adding support for heltec t114

And moved particularities to variant.h

* Remove old cpp comment that belongs to variant.h

It was a leftover.

* Trunk fix

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-11-18 11:23:39 -06:00
Jonathan BennettandTom Fifield 6c09cf9d3d Gps reset detect (#8302)
* Properly format timestamp in log message

* Better formatting of GPS_DEBUG logging in gps probe

* Reset GPS after serial speed change, and look for magic string to identify chip

* Add UC6580 to boot message detection, for Heltec Tracker

* Add L76K detect from boot string, for Heltec-v4

* Slightly more useful GPS debugging

* Back out detection of L76K via startup messages.

* Ignore PIN_GPS_RESET = -1 and rename passive_detect array.

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-11-18 15:04:44 +08:00
omgbebebe 79a91578b7 mqtt: do not try to send packets when it disconnected (#8658) 2025-11-17 16:54:02 -06:00
Jason P 17cd83085b Remove gating for Display Options (#8651) 2025-11-16 22:05:24 -06:00
Chloe Bethel b7bdcbe43e Address review comments 2025-11-16 17:18:27 -06:00
Chloe Bethel df063f40ff Try to look for a config file based on the HAT vendor/product for autoconfig 2025-11-16 17:18:27 -06:00
Jason P 84bb1e33a6 Add code for preserving favorites, also move to Home screen before reseting (#8647) 2025-11-16 14:18:16 -06:00
Jason PandCopilot 955347bf50 Remove fixed scaling in Digital Clock (#8620)
* Update digital clock draw to auto scale to correct size; no more fixed scaling

* Static scale calcuation to improve performance

* Update src/graphics/draw/ClockRenderer.cpp

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

* Back off for width or height exceeds

* Fixes for some calcuations

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 08:42:51 -06:00
Dane EvansandBen Meadors 4284fc2aec Feat/6704 neighbor info on demand (#8523)
* full thing. works

* works

* minimal changes

* roll back previous changes, move to using the alloc() overrride

* clean up comments

* format

* run clang-format manually.
Trunk may be the absolute worst formatter in existance

* format on WSL to fix trunks awfulness

* add a 3 minute cooldown to prevent messages going back and forth

* add ignoring the dummy neighbor.

* fix or.

* fix spelling, increase logging

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-15 19:49:46 -06:00
github-actions[bot]andthebentern 034aaa376a Automated version bumps (#8626)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-11-14 06:09:25 -06:00
Andrik45719 7212fb9caa Fix null pointer dereference in radio chip region check (#8613) 2025-11-11 10:00:08 -06:00
Jason B. CoxandBen Meadors 4118e1c0f6 Cleanup unnecessary global dereferencing in CryptoEngine (#8611)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-10 20:19:15 -06:00
Thomas Göttgens a62fed3289 Merge pull request #8610 from meshtastic/fix-ldro
Change RadioLib to commit zip til 7.4.1+ is released
2025-11-10 22:31:03 +01:00
Thomas Göttgens 7d2744fae0 Change RadioLib to commit zip til 7.4.1+ is released
fixes regression for SX127x chips per @GUVWAF
2025-11-10 16:16:24 +01:00
Chloe Bethel beaebda4de stm32wl: Wrap and remove some functions that pull in large amounts of code/data to claw back even more flash space (#8609) 2025-11-10 09:08:04 -06:00
TomandTom 36c2178570 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-09 09:24:03 -06:00
Quency-DandBen Meadors 50f9be9a2b 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:47:24 -06:00
Benjamin Faershtein 8fe98db5dd 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 05:59:45 -06:00
531cad5e88 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-07 15:03:56 -06:00
Benjamin Faershtein 77e0a24838 Discard everything if downlink isn't on (#8578) 2025-11-06 21:01:15 -06:00
Ford Jones 6cad393688 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 16:06:37 -06:00
Ben Meadors 0725b46744 Merge branch 'master' into develop 2025-11-06 07:14:51 -06:00
Mike RobbinsandBen Meadors 6a6c409b9a addFromContact: Don't auto-favorite when CLIENT_BASE; don't update last_heard unless CLIENT_BASE (#8495)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-11-06 07:10:20 -06:00
Wessel 7b14b173d9 Store hop/mqtt/transport mechanism info in S&F (#8560)
Before this, all messages received when enabling S&F server would return
Hops away: -1
2025-11-06 06:27:25 -06:00
Jason P 3e40d7896b Revert "nrf52: add watchdog (#8485)" (#8554)
This reverts commit 83954293d8.
2025-11-05 10:07:28 -06:00
03f69b3b77 Update RadioLib to v7.4.0 (#8456)
* fix strlcpy compile error in Ubuntu 22.04 (#8520)

* fix strlcpy error in Ubuntu 20.04

* add to native after tests

* Add support for RAK_WISMESH_TAP_V2 and RAK3401 hardware models (#8537)

* Update RadioLib to v7.4.0

---------

Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
Co-authored-by: Daniel.Cao <144674500+DanielCao0@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:04:56 -06:00
Sebastian KuzminskyandBen Meadors 83954293d8 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>
2025-11-04 05:35:44 -06:00
Austin 91621427f1 Packaging: Add libbsd where needed (#8533) 2025-11-03 17:56:31 -06:00
Ben Meadors 8d1b9c9dce Merge pull request #8531 from meshtastic/master
Master
2025-11-03 15:56:42 -06:00
Ben Meadors 538c05ad6c Revert "ADD - heltec v4 support to device install bat (#8528)" (#8532)
This reverts commit 468247fb94.
2025-11-03 15:56:23 -06:00
Quency-D f6370bea8f Add the identification code for the DA217 triaxial accelerometer. (#8526) 2025-11-04 07:18:52 +11:00
Melon 468247fb94 ADD - heltec v4 support to device install bat (#8528) 2025-11-04 07:14:24 +11:00
github-actions[bot]andthebentern 3ae7e54681 Automated version bumps (#8527)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2025-11-03 10:14:08 -06:00
Dmitry Ivanishkin 0a124b7f3d Fix SHT4x detection by reading unique serial nubmer (#8525) 2025-11-03 08:17:52 -06:00
Jonathan Bennett 7def82595d Rename screen options to display options and add units chooser (#8517)
* Rename screen options to display options and add units chooser

* Add InitialSelected to DisplayUnits_menu
2025-11-02 21:30:41 -06:00
Melon 3a67204f6d Update device-install.sh to support heltec-v4 (#8509)
* Update device-install.sh

* Update device-install.sh
2025-11-02 06:09:15 -06:00
Jonathan Bennett d1b66782d1 Hide nodes that don't have position in the distance and bearings nodelists (#8518) 2025-11-02 05:57:15 -06:00
Xavier horwoodandTom Fifield 718fd118b0 Add IPv6 Support for esp32 (#6866)
* Update Default.h

* Update NodeDB.cpp

* Update WiFiAPClient.cpp

* Update userPrefs.jsonc

* set ipv6 to off by default

* Trunk fix

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-11-02 09:25:59 +11:00
Ben Meadors 75f7ded12c Merge pull request #8513 from meshtastic/master
Master backmerge
2025-11-01 17:17:14 -05:00
pa0lin082andTom Fifield 7f78a624cd Add support for Bh1750 Light Sensor (#8376)
* regenerate protobuf with bh1750 TelemetrySensorType

* Added wollewald/BH1750_WE@^1.1.10 dependecy

* Added support for BH1750 during i2C detection

* Create new BH1750Sensor and added in EnvironmentTelemetry

* clean code

* Attempt to fix protobuf include

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>
2025-11-01 09:40:36 +11:00
shortwavesurfer2009andHarukiToreda 17324fa725 adjust battery curve (#8137)
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2025-11-01 09:39:30 +11:00
Marius Faber 001654e90a Add basic LR1121 support for T-Beam S3, full support needs #4775 fixed (#8349) 2025-11-01 09:38:37 +11:00
Andrew Yong 4f817d69eb fix(wio-e5): Fix LED state inversion (#8500)
Wio-E5 currently has LED appearing to be steadily on, due to incorrect LED_STATE_ON (it is actually briefly flashing off, but visually it is hard to perceive).

Wio-E5 has LED between GPIO PB5 and VCC, so LED_STATE_ON should be 0 for LED to blink correctly. With this commit, it is now flashing correctly.

Refer to schematics:

* [Wio-E5 Development Kit](https://files.seeedstudio.com/products/113990934/LoRa-E5%20Dev%20Board%20v1.0.pdf)
* [Wio-E5 mini](https://files.seeedstudio.com/products/113990939/LoRa-E5%20mini%20v1.0.pdf)

Signed-off-by: Andrew Yong <me@ndoo.sg>
2025-10-31 05:55:07 -05:00
Chloe Bethel de83b448f9 Force stdout to be line buffered - this fixes logs ending early if meshtasticd crashes (#8499) 2025-10-31 05:54:35 -05:00
Ixitxachitl c145be8e05 Refactor emote dimensions to 16x16 pixels (#8493)
Updated the dimensions of various emotes in emotes.h from 30x30 or 25x25 to 16x16 pixels for consistency and optimization. Added new emotes including heart_smile, Heart_eyes, and others, all with the same 16x16 size. This change improves memory usage and aligns with the design specifications for smaller emotes.
2025-10-31 21:20:29 +11:00
Clive Blackledge 28f53d132a refactor: change node count variables from uint8_t to uint16_t (#8478)
This is a non-breaking change that increases the internal representation
of node counts from uint8_t (max 255) to uint16_t (max 65535) to support
larger mesh networks, particularly on ESP32-S3 devices with PSRAM.

Changes:
- NodeStatus: numOnline, numTotal, lastNumTotal (uint8_t -> uint16_t)
- ProtobufModule: numOnlineNodes (uint8_t -> uint16_t)
- MapApplet: loop counters changed to size_t for consistency with getNumMeshNodes()
- NodeStatus: Fixed log format to use %u for unsigned integers

Note: Default class methods keep uint32_t for numOnlineNodes parameter
to match the public API and allow flexibility, even though internal node
counts use uint16_t (max 65535 nodes).

This change does NOT affect protobuf definitions, maintaining wire
compatibility with existing clients and devices.
2025-10-28 15:32:08 -05:00
Erayd f045ca2303 Fix type to ensure correct alignment; saves 4B per entry (#8465) 2025-10-27 06:05:59 -05:00
Tom Fifield b6830a68a0 Migrate test workflow to use Node 24 (#8466)
Node 24 is now the common version amoungst all of our actions.
2025-10-27 19:47:34 +11:00
Ben Meadors 13c4c2037d Merge pull request #8444 from meshtastic/master
Master --> develop
2025-10-24 21:06:29 -05:00
184 changed files with 2265 additions and 4202 deletions
+1 -1
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}}
+1 -1
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
+1 -1
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}}
+3 -3
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}}
+3 -3
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}}
+1 -1
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}}
+1 -1
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}}
+1 -1
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 }}
+7 -7
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
+7 -7
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
+2 -2
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
+1 -1
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
+1 -1
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}}
+1 -1
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
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
checks: write
pull-requests: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: recursive
+4 -1
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
+1 -1
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
+1 -1
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
+3 -3
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}}
+1 -1
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:
+1 -1
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
+1 -1
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
+1 -1
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}}
+1 -1
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
-1
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")
+1 -1
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
+1 -1
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.
+3
View File
@@ -37,6 +37,9 @@ build_flags =
-DRADIOLIB_EXCLUDE_LR11X0=1
-DHAL_DAC_MODULE_ONLY
-DHAL_RNG_MODULE_ENABLED
-Wl,--wrap=__assert_func
-Wl,--wrap=strerror
-Wl,--wrap=_tzset_unlocked_r
build_src_filter =
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<mesh/api/> -<mesh/wifi/> -<mesh/http/> -<modules/esp32> -<mesh/eth/> -<input> -<buzz> -<modules/RemoteHardwareModule.cpp> -<platform/nrf52> -<platform/portduino> -<platform/rp2xx0> -<mesh/raspihttp>
@@ -87,6 +87,9 @@
</screenshots>
<releases>
<release version="2.7.16" date="2025-11-19">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16</url>
</release>
<release version="2.7.15" date="2025-11-13">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.15</url>
</release>
-53
View File
@@ -1,53 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_eink",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M3",
"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 nrf",
"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": "",
"vendor": "ELECROW"
}
-53
View File
@@ -1,53 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ELECROW_M4 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m4",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M4",
"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 m4",
"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-m4-power-bank-lora-device-with-meshtastic-lora-tracker-function-powered-by-nrf52840.html",
"vendor": "ELECROW"
}
-42
View File
@@ -1,42 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-D BOARD_HAS_PSRAM",
"-D ARDUINO_USB_CDC_ON_BOOT=0",
"-D ARDUINO_USB_MODE=0",
"-D ARDUINO_RUNNING_CORE=1",
"-D ARDUINO_EVENT_RUNNING_CORE=0"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qio_opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "ELECROW-ThinkNode-M7"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "ELECROW ThinkNode M7",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 524288,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com",
"vendor": "ELECROW"
}
-56
View File
@@ -1,56 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_MUZI_BASE -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [["0x239A", "0xcafe"]],
"mcu": "nrf52840",
"variant": "muzi-base",
"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": "Muzi Base",
"url": "https://muzi.works/",
"vendor": "MuziWorks",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"blackmagic",
"cmsis-dap",
"mbed",
"stlink"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
}
}
+7
View File
@@ -1,3 +1,10 @@
meshtasticd (2.7.16.0) unstable; urgency=medium
* Version 2.7.16
-- 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
+1
View File
@@ -33,6 +33,7 @@ BuildRequires: python3dist(grpcio[protobuf])
BuildRequires: python3dist(grpcio-tools)
BuildRequires: git-core
BuildRequires: gcc-c++
BuildRequires: (glibc-devel >= 2.38) or pkgconfig(libbsd-overlay)
BuildRequires: pkgconfig(yaml-cpp)
BuildRequires: pkgconfig(libgpiod)
BuildRequires: pkgconfig(bluez)
+4 -1
View File
@@ -115,7 +115,8 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.3.0
# jgromes/RadioLib@7.4.0
https://github.com/jgromes/RadioLib/archive/536c7267362e2c1345be7054ba45e503252975ff.zip
[device-ui_base]
lib_deps =
@@ -181,6 +182,8 @@ lib_deps =
dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/BH1750_WE@^1.1.10
wollewald/BH1750_WE@^1.1.10
; (not included in native / portduino)
[environmental_extra]
+1 -1
View File
@@ -154,7 +154,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
if (!this->_enabled)
return false;
if ((this->_server == NULL && this->_ip == IPAddress(0, 0, 0, 0)) || this->_port == 0)
if ((this->_server == NULL && this->_ip == INADDR_NONE) || this->_port == 0)
return false;
// Check priority against priMask values.
+1 -8
View File
@@ -13,11 +13,6 @@ extern MemGet memGet;
#define LED_STATE_ON 1
#endif
// WIFI LED
#ifndef WIFI_STATE_ON
#define WIFI_STATE_ON 1
#endif
// -----------------------------------------------------------------------------
// DEBUG
// -----------------------------------------------------------------------------
@@ -152,9 +147,7 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN
#define defaultBLEPin 123456
#if HAS_ETHERNET && defined(USE_CH390D)
#include <ESP32_CH390.h>
#elif HAS_ETHERNET && !defined(USE_WS5500)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET
+6 -6
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;
}
+8 -8
View File
@@ -14,16 +14,16 @@ class NodeStatus : public Status
CallbackObserver<NodeStatus, const NodeStatus *> statusObserver =
CallbackObserver<NodeStatus, const NodeStatus *>(this, &NodeStatus::updateStatus);
uint8_t numOnline = 0;
uint8_t numTotal = 0;
uint16_t numOnline = 0;
uint16_t numTotal = 0;
uint8_t lastNumTotal = 0;
uint16_t lastNumTotal = 0;
public:
bool forceUpdate = false;
NodeStatus() { statusType = STATUS_TYPE_NODE; }
NodeStatus(uint8_t numOnline, uint8_t numTotal, bool forceUpdate = false) : Status()
NodeStatus(uint16_t numOnline, uint16_t numTotal, bool forceUpdate = false) : Status()
{
this->forceUpdate = forceUpdate;
this->numOnline = numOnline;
@@ -34,11 +34,11 @@ class NodeStatus : public Status
void observe(Observable<const NodeStatus *> *source) { statusObserver.observe(source); }
uint8_t getNumOnline() const { return numOnline; }
uint16_t getNumOnline() const { return numOnline; }
uint8_t getNumTotal() const { return numTotal; }
uint16_t getNumTotal() const { return numTotal; }
uint8_t getLastNumTotal() const { return lastNumTotal; }
uint16_t getLastNumTotal() const { return lastNumTotal; }
bool matches(const NodeStatus *newStatus) const
{
@@ -56,7 +56,7 @@ class NodeStatus : public Status
numTotal = newStatus->getNumTotal();
}
if (isDirty || newStatus->forceUpdate) {
LOG_DEBUG("Node status update: %d online, %d total", numOnline, numTotal);
LOG_DEBUG("Node status update: %u online, %u total", numOnline, numTotal);
onNewStatus.notifyObservers(this);
}
return 0;
+10 -179
View File
@@ -194,7 +194,7 @@ static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level se
#ifdef BATTERY_PIN
static void adcEnable()
void battery_adcEnable()
{
#ifdef ADC_CTRL // enable adc voltage divider when we need to read
#ifdef ADC_USE_PULLUP
@@ -214,7 +214,7 @@ static void adcEnable()
#endif
}
static void adcDisable()
static void battery_adcDisable()
{
#ifdef ADC_CTRL // disable adc voltage divider when we need to read
#ifdef ADC_USE_PULLUP
@@ -278,11 +278,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
break;
}
}
#if defined(BATTERY_CHARGING_INV)
// bit of trickery to show 99% up until the charge finishes
if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99)
battery_SOC = 99;
#endif
return clamp((int)(battery_SOC), 0, 100);
}
@@ -325,7 +320,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
uint32_t raw = 0;
float scaled = 0;
adcEnable();
battery_adcEnable();
#ifdef ARCH_ESP32 // ADC block for espressif platforms
raw = espAdcRead();
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
@@ -337,7 +332,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
raw = raw / BATTERY_SENSE_SAMPLES;
scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw;
#endif
adcDisable();
battery_adcDisable();
if (!initial_read_done) {
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
@@ -455,13 +450,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
// if it's not LOW - check the battery
#else
// if external powered that pin will be pulled up
return digitalRead(EXT_PWR_DETECT) == HIGH;
// don't check the battery.
// very narrow backport from later 2.7 releases
if (digitalRead(EXT_PWR_DETECT) == HIGH) {
return true;
}
// if it's not HIGH - check the battery
#endif
#elif defined(MUZI_BASE)
return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk;
#endif
return getBattVoltage() > chargingVolt;
}
@@ -475,12 +468,8 @@ class AnalogBatteryLevel : public HasBatteryLevel
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
}
#endif
#if defined(ELECROW_ThinkNode_M6)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
#elif EXT_CHRG_DETECT
#ifdef EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#elif defined(BATTERY_CHARGING_INV)
return !digitalRead(BATTERY_CHARGING_INV);
#else
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
if (hasINA()) {
@@ -694,8 +683,6 @@ bool Power::setup()
found = true;
} else if (lipoChargerInit()) {
found = true;
} else if (serialBatteryInit()) {
found = true;
} else if (meshSolarInit()) {
found = true;
} else if (analogInit()) {
@@ -715,26 +702,7 @@ bool Power::setup()
},
CHANGE);
#endif
#ifdef BATTERY_CHARGING_INV
attachInterrupt(
BATTERY_CHARGING_INV,
[]() {
power->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
},
CHANGE);
#endif
#ifdef EXT_CHRG_DETECT
attachInterrupt(
EXT_CHRG_DETECT,
[]() {
power->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
},
CHANGE);
#endif
enabled = found;
low_voltage_counter = 0;
@@ -938,13 +906,8 @@ void Power::readPowerStatus()
low_voltage_counter++;
LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter);
if (low_voltage_counter > 10) {
#if defined(ARCH_NRF52) && !defined(ELECROW_ThinkNode_M6)
// We can't trigger deep sleep on NRF52, it's freezing the board
LOG_DEBUG("Low voltage detected, but not trigger deep sleep");
#else
LOG_INFO("Low voltage detected, trigger deep sleep");
powerFSM.trigger(EVENT_LOW_BATTERY);
#endif
}
} else {
low_voltage_counter = 0;
@@ -1585,135 +1548,3 @@ bool Power::meshSolarInit()
return false;
}
#endif
#ifdef HAS_SERIAL_BATTERY_LEVEL
#include <SoftwareSerial.h>
/**
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
*/
class SerialBatteryLevel : public HasBatteryLevel
{
public:
/**
* Init the I2C meshSolar battery level sensor
*/
bool runOnce()
{
BatterySerial.begin(4800);
return true;
}
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override { return v_percent; }
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return voltage * 1000; }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() override
{
// definitely need to gobble up more bytes at once
if (BatterySerial.available() > 5) {
// LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
while (BatterySerial.available() > 11) {
BatterySerial.read(); // flush old data
}
// LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
int tries = 0;
while (BatterySerial.read() != 0xFE) {
tries++; // wait for start byte
if (tries > 10) {
LOG_WARN("SerialBatteryLevel: no start byte found");
return 1;
}
}
Data[1] = BatterySerial.read();
Data[2] = BatterySerial.read();
Data[3] = BatterySerial.read();
Data[4] = BatterySerial.read();
Data[5] = BatterySerial.read();
if (Data[5] != 0xFD) {
LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
return true;
}
v_percent = Data[1];
voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
voltage *= 2;
// LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
return true;
}
// This function runs first, so use it to grab the latest data from the secondary MCU
return true;
}
/**
* return true if there is an external power source detected
*/
virtual bool isVbusIn() override
{
#if defined(EXT_CHRG_DETECT)
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#endif
return false;
}
virtual bool isCharging() override
{
#ifdef EXT_CHRG_DETECT
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
#endif
// by default, we check the battery voltage only
return isVbusIn();
}
private:
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
uint8_t Data[6] = {0};
int v_percent = 0;
float voltage = 0.0;
};
SerialBatteryLevel serialBatteryLevel;
/**
* Init the serial battery level sensor
*/
bool Power::serialBatteryInit()
{
#ifdef EXT_PWR_DETECT
pinMode(EXT_PWR_DETECT, INPUT);
#endif
#ifdef EXT_CHRG_DETECT
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
#endif
bool result = serialBatteryLevel.runOnce();
LOG_DEBUG("Power::serialBatteryInit serial battery sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &serialBatteryLevel;
return true;
}
#else
/**
* If this device has no serial battery level sensor, don't try to use it.
*/
bool Power::serialBatteryInit()
{
return false;
}
#endif
+3 -79
View File
@@ -6,11 +6,6 @@
#include "Tone.h"
#endif
#if defined(HAS_I2S)
#include "main.h"
#include <unordered_map>
#endif
#if !defined(ARCH_PORTDUINO)
extern "C" void delay(uint32_t dwMs);
#endif
@@ -21,7 +16,6 @@ struct ToneDuration {
};
// Some common frequencies.
#define NOTE_SILENT 1
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
@@ -35,70 +29,13 @@ struct ToneDuration {
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_CS4 277
#define NOTE_B4 494
#define NOTE_F5 698
#define NOTE_G6 1568
#define NOTE_E7 2637
#define NOTE_C4 262
#define NOTE_E4 330
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_C5 523
#define NOTE_E5 659
#define NOTE_G5 784
const int DURATION_1_16 = 62; // 1/16 note
const int DURATION_1_8 = 125; // 1/8 note
const int DURATION_1_4 = 250; // 1/4 note
const int DURATION_1_2 = 500; // 1/2 note
const int DURATION_3_4 = 750; // 1/4 note
const int DURATION_1_1 = 1000; // 1/1 note
#ifdef HAS_I2S
void playTonesRTTTL(const ToneDuration *tone_durations, int size)
{
// translate ToneDuration[] to RTTTL string and play using audioThread
static std::unordered_map<int, std::string> freqToNote = {
{NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"},
{NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"},
{NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"},
{NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}};
char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave
for (int i = 0; i < size; i++) {
const auto &td = tone_durations[i];
std::string note = "b4";
if (freqToNote.find(td.frequency_khz) != freqToNote.end()) {
note = freqToNote[td.frequency_khz];
}
int dur = 32; // default duration
if (td.duration_ms >= 1000)
dur = 1;
else if (td.duration_ms >= 500)
dur = 2;
else if (td.duration_ms >= 250)
dur = 4;
else if (td.duration_ms >= 125)
dur = 8;
else if (td.duration_ms >= 62)
dur = 16;
else
dur = 32;
char noteStr[64];
snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur);
strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1);
audioThread->beginRttl(rtttl, strlen(rtttl));
while (audioThread->isPlaying()) {
delay(10);
}
return;
}
}
#endif
void playTones(const ToneDuration *tone_durations, int size)
{
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
@@ -106,13 +43,7 @@ void playTones(const ToneDuration *tone_durations, int size)
// Buzzer is disabled or not set to system tones
return;
}
#ifdef HAS_I2S
if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) {
playTonesRTTTL(tone_durations, size);
return;
}
#endif
#if defined(PIN_BUZZER)
#ifdef PIN_BUZZER
if (!config.device.buzzer_gpio)
config.device.buzzer_gpio = PIN_BUZZER;
#endif
@@ -128,7 +59,7 @@ void playTones(const ToneDuration *tone_durations, int size)
void playBeep()
{
ToneDuration melody[] = {{NOTE_B3, DURATION_1_16}};
ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}};
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
@@ -165,14 +96,7 @@ void playShutdownMelody()
void playChirp()
{
// A short, friendly "chirp" sound for key presses
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
void playClick()
{
// A very short "click" sound with minimum delay; ideal for rotary encoder events
ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3
ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note
playTones(melody, sizeof(melody) / sizeof(ToneDuration));
}
-9
View File
@@ -229,8 +229,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BHI260AP_ADDR 0x28
#define BMM150_ADDR 0x13
#define DA217_ADDR 0x26
#define BMI270_ADDR 0x68
#define BMI270_ADDR_ALT 0x69
// -----------------------------------------------------------------------------
// LED
@@ -398,13 +396,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_RGB_LED
#endif
#ifndef LED_STATE_OFF
#define LED_STATE_OFF 0
#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
#endif
// default mapping of pins
#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN)
#define ALT_BUTTON_PIN PIN_BUTTON2
+2 -2
View File
@@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270};
return firstOfOrNONE(10, types);
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150};
return firstOfOrNONE(9, types);
}
ScanI2C::FoundDevice ScanI2C::firstAQI() const
+1 -3
View File
@@ -85,9 +85,7 @@ class ScanI2C
DRV2605,
BH1750,
DA217,
CHSC6X,
CST226SE,
BMI270
CHSC6X
} DeviceType;
// typedef uint8_t DeviceAddress;
+25 -19
View File
@@ -106,6 +106,7 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation
if (i2cBus->available())
i2cBus->read();
}
LOG_DEBUG("Register value: 0x%x", value);
return value;
}
@@ -377,14 +378,13 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0x11f3 || registerValue == 0xe9c ||
registerValue == 0xc8d) {
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2);
if (registerValue == 0x5449) {
type = OPT3001;
logFoundDevice("OPT3001", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2) != 0) { // unique SHT4x serial number
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else {
type = SHT31;
logFoundDevice("SHT31", (uint8_t)addr.address);
@@ -465,8 +465,23 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break;
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
case TCA9555_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1);
if (registerValue == 0x13) {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
if (registerValue == 0x81) {
type = DA217;
logFoundDevice("DA217", (uint8_t)addr.address);
} else {
type = TCA9555;
logFoundDevice("TCA9555", (uint8_t)addr.address);
}
} else {
type = TCA9555;
logFoundDevice("TCA9555", (uint8_t)addr.address);
}
break;
case TSL25911_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1);
if (registerValue == 0x50) {
@@ -529,23 +544,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
#endif
if (registerValue == 0xEA) {
type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address);
break;
} else if (registerValue == 0x24) {
type = BMI270;
logFoundDevice("BMI270", (uint8_t)addr.address);
break;
} else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00
} else if (addr.address == BMX160_ADDR) {
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
+35 -13
View File
@@ -240,6 +240,9 @@ GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis)
buffer[bytesRead] = b;
bytesRead++;
if ((bytesRead == 767) || (b == '\r')) {
#ifdef GPS_DEBUG
LOG_DEBUG(debugmsg.c_str());
#endif
if (strnstr((char *)buffer, message, bytesRead) != nullptr) {
#ifdef GPS_DEBUG
LOG_DEBUG("Found: %s", message); // Log the found message
@@ -247,9 +250,6 @@ GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis)
return GNSS_RESPONSE_OK;
} else {
bytesRead = 0;
#ifdef GPS_DEBUG
LOG_DEBUG(debugmsg.c_str());
#endif
}
}
}
@@ -894,11 +894,14 @@ void GPS::writePinEN(bool on)
void GPS::writePinStandby(bool standby)
{
#ifdef PIN_GPS_STANDBY // Specifically the standby pin for L76B, L76K and clones
bool val;
if (standby)
val = GPS_STANDBY_ACTIVE;
else
val = !GPS_STANDBY_ACTIVE;
// Determine the new value for the pin
// Normally: active HIGH for awake
#ifdef PIN_GPS_STANDBY_INVERTED
bool val = standby;
#else
bool val = !standby;
#endif
// Write and log
pinMode(PIN_GPS_STANDBY, OUTPUT);
@@ -1272,6 +1275,24 @@ GnssModel_t GPS::probe(int serialSpeed)
memset(&ublox_info, 0, sizeof(ublox_info));
delay(100);
#if defined(PIN_GPS_RESET) && PIN_GPS_RESET != -1
digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms
delay(10);
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
// attempt to detect the chip based on boot messages
std::vector<ChipInfo> passive_detect = {
{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352},
{"UC6580", "UC6580", GNSS_MODEL_UC6580},
// as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now.
/*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/};
GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed);
if (detectedDriver != GNSS_MODEL_UNKNOWN) {
return detectedDriver;
}
#endif
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
_serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n");
delay(20);
@@ -1470,12 +1491,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
}
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
#endif
// check if we can see our chips
for (const auto &chipInfo : responseMap) {
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
#endif
LOG_INFO("%s detected", chipInfo.chipName.c_str());
delete[] response; // Cleanup before return
return chipInfo.driver;
@@ -1483,6 +1504,9 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
}
}
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
#ifdef GPS_DEBUG
LOG_DEBUG(response);
#endif
// Reset the response buffer for the next potential message
responseLen = 0;
response[0] = '\0';
@@ -1569,8 +1593,6 @@ GPS *GPS::createGps()
#ifdef PIN_GPS_RESET
pinMode(PIN_GPS_RESET, OUTPUT);
digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms
delay(10);
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
#endif
-5
View File
@@ -16,11 +16,6 @@
#define GPS_EN_ACTIVE 1
#endif
// Allow defining the polarity of the STANDBY output. default is LOW for standby
#ifndef GPS_STANDBY_ACTIVE
#define GPS_STANDBY_ACTIVE LOW
#endif
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
+2 -10
View File
@@ -112,11 +112,7 @@ RTCSetResult readFromRTC()
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
uint32_t now = millis();
#ifdef MUZI_BASE
ArtronShop_RX8130CE rtc(&Wire1);
#else
ArtronShop_RX8130CE rtc(&Wire);
#endif
tm t;
if (rtc.getTime(&t)) {
tv.tv_sec = gm_mktime(&t);
@@ -249,11 +245,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
#ifdef MUZI_BASE
ArtronShop_RX8130CE rtc(&Wire1);
#else
ArtronShop_RX8130CE rtc(&Wire);
#endif
tm *t = gmtime(&tv->tv_sec);
if (rtc.setTime(*t)) {
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
@@ -318,7 +310,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
@@ -327,7 +319,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
// Calculate max allowed time safely to avoid overflow in logging
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch,
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
lastTimeValidationWarning = millis();
}
+6 -30
View File
@@ -435,17 +435,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
PMU->enablePowerOutput(XPOWERS_ALDO2);
#endif
// some screens seem to need a kick in the pants to turn back on
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
dispdev->init();
dispdev->setBrightness(brightness);
dispdev->flipScreenVertically();
dispdev->resetDisplay();
#ifdef SCREEN_12V_ENABLE
digitalWrite(SCREEN_12V_ENABLE, HIGH);
#endif
delay(100);
#endif
#if !ARCH_PORTDUINO
dispdev->displayOn();
#endif
@@ -467,11 +456,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
ui->init();
#endif
#if defined(USE_ST7789) && defined(VTFT_LEDA)
#ifdef VTFT_CTRL
#ifdef USE_ST7789
pinMode(VTFT_CTRL, OUTPUT);
digitalWrite(VTFT_CTRL, LOW);
#endif
ui->init();
#ifdef ESP_PLATFORM
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
@@ -497,19 +484,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#endif
dispdev->displayOff();
#ifdef SCREEN_12V_ENABLE
digitalWrite(SCREEN_12V_ENABLE, LOW);
#endif
#ifdef USE_ST7789
SPI1.end();
#if defined(ARCH_ESP32)
#ifdef VTFT_LEDA
pinMode(VTFT_LEDA, ANALOG);
#endif
#ifdef VTFT_CTRL
pinMode(VTFT_CTRL, ANALOG);
#endif
pinMode(ST7789_RESET, ANALOG);
pinMode(ST7789_RS, ANALOG);
pinMode(ST7789_NSS, ANALOG);
@@ -555,7 +534,7 @@ void Screen::setup()
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
#endif
#if defined(USE_SH1107_128_64) || defined(USE_SH1107)
#ifdef USE_SH1107_128_64
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
#endif
@@ -563,9 +542,6 @@ void Screen::setup()
// Apply custom RGB color (e.g. Heltec T114/T190)
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
#if defined(MUZI_BASE)
dispdev->delayPoweron = true;
#endif
// === Initialize display and UI system ===
ui->init();
@@ -1130,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);
}
}
@@ -1484,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);
-6
View File
@@ -249,8 +249,6 @@ class Screen : public concurrency::OSThread
bool isOverlayBannerShowing();
bool isScreenOn() { return screenOn; }
// Stores the last 4 of our hardware ID, to make finding the device for pairing easier
// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class
char ourId[5];
@@ -728,11 +726,7 @@ class Screen : public concurrency::OSThread
DebugInfo debugInfo;
/// Display device
#ifdef USE_ST7789
ST7789Spi *dispdev;
#else
OLEDDisplay *dispdev;
#endif
/// UI helper for rendering to frames and switching between them
OLEDDisplayUi *ui;
-2
View File
@@ -483,8 +483,6 @@ class LGFX : public lgfx::LGFX_Device
lgfx::Touch_FT5x06 _touch_instance;
#elif defined(HELTEC_V4_TFT)
lgfx::TOUCH_CHSC6X _touch_instance;
#elif defined(HELTEC_V4_TFT)
lgfx::TOUCH_CHSC6X _touch_instance;
#else
lgfx::Touch_GT911 _touch_instance;
#endif
+20
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);
}
}
+7
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);
+61 -46
View File
@@ -194,17 +194,12 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
}
#endif
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16];
int hour = 0;
int minute = 0;
int second = 0;
if (rtc_sec > 0) {
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
@@ -215,11 +210,11 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
}
bool isPM = hour >= 12;
// hour = hour > 12 ? hour - 12 : hour;
if (config.display.use_12h_clock) {
hour %= 12;
if (hour == 0)
if (hour == 0) {
hour = 12;
}
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
} else {
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
@@ -229,24 +224,56 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
char secondString[8];
snprintf(secondString, sizeof(secondString), "%02d", second);
#ifdef T_WATCH_S3
float scale = 1.5;
#elif defined(CHATTER_2)
float scale = 1.1;
#else
float scale = 0.75;
if (isHighResolution) {
scale = 1.5;
}
#endif
static bool scaleInitialized = false;
static float scale = 0.75f;
static float segmentWidth = SEGMENT_WIDTH * 0.75f;
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
if (!scaleInitialized) {
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
float step = 0.05f; // Step increment per iteration
float target_width = display->getWidth() * screenwidth_target_ratio;
float target_height =
display->getHeight() -
(isHighResolution
? 46
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
float calculated_width_size = 0.0f;
float calculated_height_size = 0.0f;
while (true) {
segmentWidth = SEGMENT_WIDTH * scale;
segmentHeight = SEGMENT_HEIGHT * scale;
calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4);
calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2);
if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) {
break;
}
scale += step;
}
// If we overshot width, back off one step and recompute segment sizes
if (calculated_width_size > target_width || calculated_height_size > target_height) {
scale -= step;
segmentWidth = SEGMENT_WIDTH * scale;
segmentHeight = SEGMENT_HEIGHT * scale;
}
scaleInitialized = true;
}
size_t len = strlen(timeString);
// calculate hours:minutes string width
uint16_t timeStringWidth = strlen(timeString) * 5;
uint16_t timeStringWidth = len * 5; // base spacing between characters
for (uint8_t i = 0; i < strlen(timeString); i++) {
for (size_t i = 0; i < len; i++) {
char character = timeString[i];
if (character == ':') {
@@ -257,19 +284,21 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
}
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2);
uint16_t startingHourMinuteTextX = hourMinuteTextX;
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2);
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2;
// iterate over characters in hours:minutes string and draw segmented characters
for (uint8_t i = 0; i < strlen(timeString); i++) {
for (size_t i = 0; i < len; i++) {
char character = timeString[i];
if (character == ':') {
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
hourMinuteTextX += segmentHeight + 6;
if (scale >= 2.0f) {
hourMinuteTextX += (uint16_t)(4.5f * scale);
}
} else {
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
@@ -279,38 +308,29 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
hourMinuteTextX += 5;
}
// draw seconds string
// draw seconds string + AM/PM
display->setFont(FONT_SMALL);
int xOffset = (isHighResolution) ? 0 : -1;
if (hour >= 10) {
xOffset += (isHighResolution) ? 32 : 18;
}
int yOffset = (isHighResolution) ? 3 : 1;
#ifdef SENSECAP_INDICATOR
yOffset -= 3;
#endif
#ifdef T_DECK
yOffset -= 5;
#endif
if (config.display.use_12h_clock) {
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
isPM ? "pm" : "am");
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am");
}
#ifndef USE_EINK
xOffset = (isHighResolution) ? 18 : 10;
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
if (scale >= 2.0f) {
xOffset -= (int)(4.5f * scale);
}
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1,
secondString);
#endif
graphics::drawCommonFooter(display, x, y);
}
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
{
display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon);
}
// Draw an analog clock
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
@@ -321,11 +341,6 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
#ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
}
#endif
// clock face center coordinates
int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2;
-1
View File
@@ -24,7 +24,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
// UI elements for clock displays
// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
} // namespace ClockRenderer
+3 -12
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
+59 -28
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) {
@@ -574,21 +576,16 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu()
{
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd };
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
optionsArray[options] = "Notifications";
optionsEnumArray[options++] = Notifications;
#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \
defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
optionsArray[options] = "Screen Options";
optionsArray[options] = "Display Options";
optionsEnumArray[options++] = ScreenOptions;
#endif
optionsArray[options] = "Frame Visiblity Toggle";
optionsEnumArray[options++] = FrameToggles;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth";
#else
@@ -626,9 +623,6 @@ void menuHandler::systemBaseMenu()
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
@@ -1197,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;
@@ -1226,9 +1220,9 @@ void menuHandler::traceRouteMenu()
void menuHandler::testMenu()
{
enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
@@ -1236,10 +1230,6 @@ void menuHandler::testMenu()
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
#ifdef HAS_I2S
optionsArray[options] = "Test Announce";
optionsEnumArray[options++] = TestAnnounce;
#endif
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Hidden Test Menu";
@@ -1254,10 +1244,6 @@ void menuHandler::testMenu()
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else if (selected == TestAnnounce) {
#ifdef HAS_I2S
audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test.");
#endif
} else {
menuQueue = system_base_menu;
screen->runNow();
@@ -1347,7 +1333,7 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false;
#endif
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor };
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
int options = 1;
@@ -1369,8 +1355,14 @@ void menuHandler::screenOptionsMenu()
optionsEnumArray[options++] = ScreenColor;
#endif
optionsArray[options] = "Frame Visibility Toggle";
optionsEnumArray[options++] = FrameToggles;
optionsArray[options] = "Display Units";
optionsEnumArray[options++] = DisplayUnits;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Screen Options";
bannerOptions.message = "Display Options";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -1384,6 +1376,12 @@ void menuHandler::screenOptionsMenu()
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == DisplayUnits) {
menuHandler::menuQueue = menuHandler::DisplayUnits;
screen->runNow();
} else {
menuQueue = system_base_menu;
screen->runNow();
@@ -1461,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);
@@ -1595,6 +1595,34 @@ void menuHandler::FrameToggles_menu()
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DisplayUnits_menu()
{
enum optionsNumbers { Back, MetricUnits, ImperialUnits };
static const char *optionsArray[] = {"Back", "Metric", "Imperial"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = " Select display units";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
bannerOptions.InitialSelected = 2;
else
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == MetricUnits) {
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC;
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == ImperialUnits) {
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL;
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuHandler::menuQueue = menuHandler::screen_options_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
@@ -1709,6 +1737,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case FrameToggles:
FrameToggles_menu();
break;
case DisplayUnits:
DisplayUnits_menu();
break;
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;
@@ -1723,4 +1754,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics
#endif
#endif
+3 -1
View File
@@ -44,7 +44,8 @@ class menuHandler
trace_route_menu,
throttle_message,
node_name_length_menu,
FrameToggles
FrameToggles,
DisplayUnits
};
static screenMenus menuQueue;
@@ -88,6 +89,7 @@ class menuHandler
static void powerMenu();
static void nodeNameLengthMenu();
static void FrameToggles_menu();
static void DisplayUnits_menu();
static void textMessageMenu();
private:
+7 -7
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
+58 -35
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);
@@ -425,6 +431,12 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
{
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
bool locationScreen = false;
if (strcmp(title, "Bearings") == 0)
locationScreen = true;
else if (strcmp(title, "Distance") == 0)
locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
@@ -440,7 +452,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int numskipped = 0;
int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
int totalColumns = 1;
@@ -460,12 +472,17 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int rowCount = 0;
for (int i = startIndex; i < endIndex; ++i) {
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);
@@ -482,6 +499,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
}
}
// This should correct the scrollbar
totalEntries -= numskipped;
#if !defined(M5STACK_UNITC6L)
// Draw column separator
if (shownCount > 0) {
@@ -565,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();
+9 -9
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
+8 -5
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);
+45 -84
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
+1 -1
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
+144 -191
View File
@@ -18,6 +18,8 @@ const Emote emotes[] = {
{"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face
{"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face
{"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // 😁 Grinning Smiling Eyes
{"\U0001F60D", Heart_eyes, Heart_eyes_width, Heart_eyes_height}, // 😍 Heart Eyes
{"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts
// --- Question/Alert ---
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
@@ -30,11 +32,15 @@ const Emote emotes[] = {
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
{"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width,
Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes
{"\U0001F62D", Loudly_Crying_Face, Loudly_Crying_Face_width, Loudly_Crying_Face_height}, // 😭 Loudly Crying Face
// --- Gestures and People ---
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
{"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // ✌️ Victory Hand
{"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // 🖖 Vulcan Salute
{"\U0001F64F", Praying, Praying_width, Praying_height}, // 🙏 Praying Hands
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
// --- Weather ---
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
@@ -45,8 +51,12 @@ const Emote emotes[] = {
// --- Misc Faces ---
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
{"\U0001F921", clown, clown_width, clown_height}, // 🤡 Clown Face
{"\U0001F916", robo, robo_width, robo_height}, // 🤖 Robot Face
// --- Hearts (Multiple Unicode Aliases) ---
{"\u2665", heart, heart_width, heart_height}, // ♥ Black Heart Suit
{"\u2665\uFE0F", heart, heart_width, heart_height}, // ♥️ Black Heart Suit (emoji presentation)
{"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart
{"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart
{"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation
@@ -57,223 +67,166 @@ const Emote emotes[] = {
{"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow
// --- Objects ---
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
{"\U0001F514", bell_icon, bell_icon_width, bell_icon_height} // 🔔 Bell
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
{"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell
{"\U0001F36A", cookie, cookie_width, cookie_height}, // 🍪 Cookie
{"\U0001F525", Fire, Fire_width, Fire_height}, // 🔥 Fire
{"\u2728", Sparkles, Sparkles_width, Sparkles_height}, // ✨ Sparkles
{"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳️ Hole
{"\U0001F3B3", bowling, bowling_width, bowling_height} // 🎳 Bowling
#endif
};
const int numEmotes = sizeof(emotes) / sizeof(emotes[0]);
#ifndef EXCLUDE_EMOJI
const unsigned char thumbup[] PROGMEM = {
0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00,
0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00,
0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01,
0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00,
0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00,
};
const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18,
0x02, 0x06, 0x3F, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70,
0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00};
const unsigned char thumbdown[] PROGMEM = {
0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00,
0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01,
0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00,
0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00,
0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00,
};
const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06,
0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02,
0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00};
const unsigned char Smiling_Eyes[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xcf,
0x7e, 0xf8, 0xc3, 0xdf, 0x3e, 0xf0, 0x81, 0xdf, 0xbf, 0xf7, 0xbd, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0x3f, 0xff,
0x6f, 0xff, 0xdf, 0xfe, 0x6f, 0xff, 0xdf, 0xfe, 0x9f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0};
const unsigned char Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43,
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf,
0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf,
0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Slightly_Smiling[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf9, 0xf3, 0xcf, 0xfc, 0xf0, 0xe1, 0xcf,
0xfe, 0xf0, 0xe1, 0xdf, 0xfe, 0xf0, 0xe1, 0xdf, 0xff, 0xf0, 0xe1, 0xff, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Slightly_Smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Winking_Face[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xf0, 0xff, 0xc3, 0x78, 0xef, 0xc3, 0xc7, 0xb8, 0xdf, 0xbd, 0xcf, 0xfc, 0xf9, 0x7f, 0xcf, 0xfc, 0xf0, 0xff, 0xcf,
0xfe, 0xf0, 0xc3, 0xdf, 0xfe, 0xf0, 0x81, 0xdf, 0xff, 0xf0, 0xbf, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0x7e, 0xff, 0xdf, 0xdf,
0x7c, 0xff, 0xdf, 0xcf, 0xfc, 0xfe, 0xef, 0xcf, 0xf8, 0xf9, 0xf7, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x07, 0xc0};
const unsigned char Winking_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0xfc, 0xf8, 0xe3, 0xcf, 0x7c, 0xf7, 0xdd, 0xcf,
0xbe, 0xef, 0xbe, 0xdf, 0xbe, 0xef, 0xbe, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0xff, 0x5e, 0x55, 0x55, 0xdf, 0x5e, 0x55, 0x55, 0xdf,
0x3c, 0x00, 0x80, 0xcf, 0x7c, 0x55, 0xd5, 0xcf, 0xf8, 0x54, 0xe5, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char question[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00,
0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00,
0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52,
0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23,
0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00};
const unsigned char bang[] PROGMEM = {
0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F,
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F,
0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F,
0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F,
0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07,
};
const unsigned char Heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char haha[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xf9, 0xf3, 0xc0,
0xf0, 0xfe, 0xef, 0xc1, 0x38, 0xff, 0x9f, 0xc3, 0xd8, 0xff, 0x7f, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xcf,
0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xef, 0xff, 0xff, 0xde, 0xe7, 0xff, 0xff, 0xdc, 0xeb, 0xff, 0xff, 0xda,
0xed, 0xff, 0xff, 0xd6, 0xee, 0xff, 0xff, 0xce, 0x36, 0x00, 0x80, 0xcd, 0xb8, 0xff, 0xbf, 0xc3, 0x7e, 0x00, 0xc0, 0xdf,
0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48,
0x12, 0x48, 0x12, 0x30, 0x11, 0x80, 0x08, 0x40, 0x04, 0x40, 0x02,
0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01};
const unsigned char ROFL[] PROGMEM = {
0x00, 0x00, 0x00, 0xc0, 0x00, 0xfc, 0x07, 0xc0, 0x00, 0xff, 0x1f, 0xc0, 0x80, 0xff, 0x7f, 0xc0, 0xc0, 0xff, 0xff, 0xc0,
0xe0, 0x9f, 0xff, 0xc1, 0xf0, 0x9f, 0xff, 0xc0, 0xf8, 0x9f, 0x7f, 0xcb, 0xf8, 0x9f, 0xbf, 0xcb, 0xfc, 0x9f, 0xdf, 0xdb,
0xfc, 0x1f, 0x08, 0xdc, 0xfe, 0x1f, 0xf8, 0xfe, 0xfe, 0xff, 0xff, 0xfe, 0x1e, 0xf0, 0x7f, 0xfe, 0x1e, 0xf0, 0xbf, 0xfe,
0xfe, 0xf3, 0xdf, 0xfe, 0xfe, 0xf3, 0x6f, 0xfe, 0xfe, 0xf3, 0x37, 0xfe, 0xfe, 0xeb, 0x1b, 0xfe, 0xfc, 0xef, 0x0d, 0xde,
0xfc, 0xe7, 0x06, 0xcf, 0xf8, 0x6b, 0x83, 0xcf, 0xf8, 0x0d, 0xc0, 0xc7, 0xf0, 0xed, 0xff, 0xc7, 0xe0, 0xee, 0xff, 0xc3,
0xc0, 0xee, 0xff, 0xc1, 0x80, 0xee, 0xff, 0xc0, 0x00, 0xe6, 0x3f, 0xc0, 0x00, 0xf0, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0xc0};
const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48,
0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C,
0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00};
const unsigned char Smiling_Closed_Eyes[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0xff, 0xc0, 0xe0, 0xff, 0xff, 0xc1,
0xf0, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xcf, 0x7c, 0xfe, 0xcf, 0xcf, 0xfc, 0xfc, 0xe7, 0xcf,
0xfe, 0xf9, 0xf3, 0xdf, 0xfe, 0xf3, 0xf9, 0xdf, 0xff, 0xf9, 0xf3, 0xff, 0xff, 0xfc, 0xe7, 0xff, 0x7f, 0xfe, 0xcf, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0x80, 0xff, 0xbe, 0xff, 0xbf, 0xdf, 0x7e, 0x00, 0xc0, 0xdf,
0x7c, 0x00, 0xc0, 0xcf, 0xfc, 0x00, 0xe0, 0xcf, 0xf8, 0x01, 0xf0, 0xc7, 0xf8, 0x03, 0xf8, 0xc7, 0xf0, 0xff, 0xff, 0xc3,
0xe0, 0xff, 0xff, 0xc1, 0xc0, 0xff, 0xff, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Grinning_SmilingEyes2[] PROGMEM = {
0x00, 0xf8, 0x03, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0xe0, 0xff, 0xff, 0xc0,
0xf0, 0xff, 0xff, 0xc1, 0xf8, 0xff, 0xff, 0xc3, 0xf8, 0xff, 0xff, 0xc3, 0xfc, 0xf8, 0xe3, 0xc7, 0x7c, 0xf7, 0xdd, 0xc7,
0xbe, 0xef, 0xbe, 0xcf, 0xfe, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf,
0xff, 0xff, 0xff, 0xdf, 0xff, 0xff, 0xff, 0xdf, 0x3f, 0x00, 0x80, 0xdf, 0xbe, 0xff, 0xbf, 0xcf, 0x7e, 0x00, 0xc0, 0xcf,
0x7c, 0x00, 0xc0, 0xc7, 0xfc, 0x00, 0xe0, 0xc7, 0xf8, 0x01, 0xf0, 0xc3, 0xf8, 0x03, 0xf8, 0xc3, 0xf0, 0xff, 0xff, 0xc1,
0xe0, 0xff, 0xff, 0xc0, 0xc0, 0xff, 0x7f, 0xc0, 0x80, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0};
const unsigned char ROFL[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44,
0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char wave_icon[] PROGMEM = {
0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0xc7,
0x00, 0x00, 0x1e, 0xcc, 0x00, 0x00, 0x30, 0xc8, 0x00, 0x00, 0x60, 0xd8, 0x00, 0x08, 0xc0, 0xd0, 0x00, 0x1a, 0x81, 0xd1,
0x00, 0x36, 0x03, 0xd3, 0x80, 0x6d, 0x06, 0xd2, 0x00, 0xdb, 0x0c, 0xc2, 0x80, 0xb6, 0x1d, 0xc0, 0x80, 0x6d, 0x1f, 0xc0,
0x00, 0xdb, 0x3f, 0xc0, 0x00, 0xf6, 0x7f, 0xc0, 0x00, 0xfc, 0x7f, 0xc0, 0x08, 0xf8, 0x7f, 0xc0, 0x48, 0xf0, 0x7f, 0xc0,
0x48, 0xe0, 0x7f, 0xc0, 0xc8, 0xc0, 0x3f, 0xc0, 0x98, 0x81, 0x1f, 0xc0, 0x10, 0x03, 0x00, 0xc0, 0x30, 0x0e, 0x00, 0xc0,
0x20, 0x38, 0x00, 0xc0, 0xe0, 0x00, 0x00, 0xc0, 0x80, 0x07, 0x00, 0xc0, 0x00, 0x1e, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0};
const unsigned char Smiling_Closed_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char cowboy[] PROGMEM = {
0x00, 0x0c, 0x0c, 0xc0, 0x00, 0x02, 0x10, 0xc0, 0x00, 0x01, 0x20, 0xc0, 0xbc, 0x00, 0x40, 0xcf, 0xc2, 0x01, 0xe0, 0xd0,
0x01, 0x01, 0x20, 0xe0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0,
0xc1, 0x3f, 0xff, 0xe0, 0xe1, 0xff, 0xff, 0xe1, 0xf2, 0xf3, 0xf3, 0xd3, 0xf4, 0xf1, 0xe3, 0xcb, 0xfc, 0xf1, 0xe3, 0xc7,
0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xf1, 0xe3, 0xc7, 0xf8, 0xfb, 0xf7, 0xc7, 0xf8, 0xff, 0xff, 0xc7, 0xf8, 0xff, 0xff, 0xc7,
0x70, 0xf8, 0x8f, 0xc3, 0x70, 0x03, 0xb0, 0xc3, 0x70, 0xfe, 0xbf, 0xc3, 0x60, 0x00, 0x80, 0xc1, 0xc0, 0x00, 0xc0, 0xc0,
0x80, 0x01, 0x60, 0xc0, 0x00, 0x07, 0x38, 0xc0, 0x00, 0xfe, 0x1f, 0xc0, 0x00, 0xf0, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0};
const unsigned char Grinning_SmilingEyes2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char deadmau5[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00,
0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00,
0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07,
0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00,
0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC,
0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00,
0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF,
0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const unsigned char Loudly_Crying_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A,
0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00};
const unsigned char sun[] PROGMEM = {
0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03,
0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00,
0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E,
0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00,
0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03,
0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00,
};
const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24,
0x25, 0x4A, 0x24, 0x12, 0x44, 0x22, 0x44, 0x04, 0x40, 0x08, 0x40,
0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00};
const unsigned char rain[] PROGMEM = {
0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00,
0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20,
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00,
0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00,
0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C,
0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00,
};
const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E,
0x78, 0xE2, 0x47, 0x42, 0x42, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char cloud[] PROGMEM = {
0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00,
0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01,
0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10,
0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10,
0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03,
};
const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A,
0x50, 0x0E, 0x70, 0x11, 0x88, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98,
0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00};
const unsigned char fog[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01,
0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00,
0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC,
0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F,
0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00};
const unsigned char devil[] PROGMEM = {
0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0xf0, 0x0f, 0xfc, 0x0f, 0xfc,
0x3f, 0xff, 0x3f, 0xff, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfe, 0xff, 0xff, 0xdf, 0xfc, 0xff, 0xff, 0xcf,
0xfc, 0xff, 0xff, 0xcf, 0xf8, 0xff, 0xff, 0xc7, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xff, 0xff, 0xc3, 0xf0, 0xf1, 0xe3, 0xc3,
0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe3, 0xf1, 0xc3, 0xf0, 0xe7, 0xf9, 0xc3,
0xf0, 0xff, 0xff, 0xc3, 0xe0, 0xfd, 0xef, 0xc1, 0xe0, 0xf3, 0xf3, 0xc1, 0xc0, 0x07, 0xf8, 0xc0, 0x80, 0x1f, 0x7e, 0xc0,
0x00, 0xff, 0x3f, 0xc0, 0x00, 0xfe, 0x0f, 0xc0, 0x00, 0xf8, 0x03, 0xc0, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0xc0};
const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12,
0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
const unsigned char heart[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18,
0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37,
0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F,
0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03,
0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00,
0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00,
};
const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC,
0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned char poo[] PROGMEM = {
0x00, 0x1c, 0x00, 0xc0, 0x00, 0x7c, 0x00, 0xc0, 0x00, 0xfc, 0x00, 0xc0, 0x00, 0x7c, 0x03, 0xc0, 0x00, 0xbe, 0x03, 0xc0,
0x00, 0xdf, 0x0f, 0xc0, 0x80, 0xcf, 0x0f, 0xc0, 0xc0, 0xf1, 0x0f, 0xc0, 0x60, 0xfc, 0x0f, 0xc0, 0x30, 0xff, 0x07, 0xc0,
0x90, 0xff, 0x3b, 0xc0, 0xc0, 0xff, 0x7d, 0xc0, 0xf8, 0xff, 0xfc, 0xc0, 0xf8, 0x3f, 0xf0, 0xc0, 0x78, 0x88, 0xc0, 0xc0,
0x20, 0xe3, 0x18, 0xc0, 0x98, 0xe7, 0xbc, 0xc1, 0x9c, 0x64, 0xa4, 0xc3, 0x9e, 0x64, 0xa4, 0xc7, 0xbe, 0xe4, 0xa4, 0xc7,
0xbc, 0x27, 0xbc, 0xc7, 0x38, 0x03, 0xd9, 0xc3, 0x00, 0xf0, 0x63, 0xc0, 0xf8, 0xfc, 0x3f, 0xcf, 0xfc, 0xff, 0x87, 0xdf,
0xfe, 0xff, 0xe0, 0xdf, 0xfc, 0x1f, 0xfe, 0xdf, 0xf8, 0x07, 0xf8, 0xcf, 0xf0, 0x03, 0xe0, 0xc7, 0x00, 0x00, 0x00, 0xc0};
const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00,
0x00, 0x44, 0x44, 0xAA, 0x2A, 0x11, 0x11, 0x00, 0x00, 0x88, 0x88,
0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const unsigned char bell_icon[] PROGMEM = {
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000,
0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000,
0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000,
0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011,
0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100,
0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000,
0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000};
const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A,
0x5C, 0x32, 0x4C, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE,
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F,
0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00};
const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0,
0x08, 0x10, 0x10, 0x48, 0x12, 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40,
0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00};
const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0,
0x0F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F,
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00};
const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32,
0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40,
0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70,
0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C};
const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38,
0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
const unsigned char Praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21,
0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC};
const unsigned char Sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E,
0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00};
const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72,
0x4E, 0x22, 0x44, 0x82, 0x41, 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F,
0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74,
0x2E, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A,
0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00};
const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C,
0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00};
const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00,
0x38, 0x00, 0x28, 0x78, 0x44, 0x84, 0x82, 0x22, 0x83, 0x52, 0x83,
0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00};
const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A,
0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
#endif
} // namespace graphics
+99 -47
View File
@@ -17,98 +17,150 @@ extern const int numEmotes;
#ifndef EXCLUDE_EMOJI
// === Emote Bitmaps ===
#define thumbs_height 25
#define thumbs_width 25
#define thumbs_height 16
#define thumbs_width 16
extern const unsigned char thumbup[] PROGMEM;
extern const unsigned char thumbdown[] PROGMEM;
#define Smiling_Eyes_height 30
#define Smiling_Eyes_width 30
#define Smiling_Eyes_height 16
#define Smiling_Eyes_width 16
extern const unsigned char Smiling_Eyes[] PROGMEM;
#define Grinning_height 30
#define Grinning_width 30
#define Grinning_height 16
#define Grinning_width 16
extern const unsigned char Grinning[] PROGMEM;
#define Slightly_Smiling_height 30
#define Slightly_Smiling_width 30
#define Slightly_Smiling_height 16
#define Slightly_Smiling_width 16
extern const unsigned char Slightly_Smiling[] PROGMEM;
#define Winking_Face_height 30
#define Winking_Face_width 30
#define Winking_Face_height 16
#define Winking_Face_width 16
extern const unsigned char Winking_Face[] PROGMEM;
#define Grinning_Smiling_Eyes_height 30
#define Grinning_Smiling_Eyes_width 30
#define Grinning_Smiling_Eyes_height 16
#define Grinning_Smiling_Eyes_width 16
extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM;
#define question_height 25
#define question_width 25
#define heart_smile_height 16
#define heart_smile_width 16
extern const unsigned char heart_smile[] PROGMEM;
#define Heart_eyes_height 16
#define Heart_eyes_width 16
extern const unsigned char Heart_eyes[] PROGMEM;
#define question_height 16
#define question_width 16
extern const unsigned char question[] PROGMEM;
#define bang_height 30
#define bang_width 30
#define bang_height 16
#define bang_width 16
extern const unsigned char bang[] PROGMEM;
#define haha_height 30
#define haha_width 30
#define haha_height 16
#define haha_width 16
extern const unsigned char haha[] PROGMEM;
#define ROFL_height 30
#define ROFL_width 30
#define ROFL_height 16
#define ROFL_width 16
extern const unsigned char ROFL[] PROGMEM;
#define Smiling_Closed_Eyes_height 30
#define Smiling_Closed_Eyes_width 30
#define Smiling_Closed_Eyes_height 16
#define Smiling_Closed_Eyes_width 16
extern const unsigned char Smiling_Closed_Eyes[] PROGMEM;
#define Grinning_SmilingEyes2_height 30
#define Grinning_SmilingEyes2_width 30
#define Grinning_SmilingEyes2_height 16
#define Grinning_SmilingEyes2_width 16
extern const unsigned char Grinning_SmilingEyes2[] PROGMEM;
#define wave_icon_height 30
#define wave_icon_width 30
#define Loudly_Crying_Face_height 16
#define Loudly_Crying_Face_width 16
extern const unsigned char Loudly_Crying_Face[] PROGMEM;
#define wave_icon_height 16
#define wave_icon_width 16
extern const unsigned char wave_icon[] PROGMEM;
#define cowboy_height 30
#define cowboy_width 30
#define cowboy_height 16
#define cowboy_width 16
extern const unsigned char cowboy[] PROGMEM;
#define deadmau5_height 30
#define deadmau5_width 60
#define deadmau5_height 16
#define deadmau5_width 16
extern const unsigned char deadmau5[] PROGMEM;
#define sun_height 30
#define sun_width 30
#define sun_height 16
#define sun_width 16
extern const unsigned char sun[] PROGMEM;
#define rain_height 30
#define rain_width 30
#define rain_height 16
#define rain_width 16
extern const unsigned char rain[] PROGMEM;
#define cloud_height 30
#define cloud_width 30
#define cloud_height 16
#define cloud_width 16
extern const unsigned char cloud[] PROGMEM;
#define fog_height 25
#define fog_width 25
#define fog_height 16
#define fog_width 16
extern const unsigned char fog[] PROGMEM;
#define devil_height 30
#define devil_width 30
#define devil_height 16
#define devil_width 16
extern const unsigned char devil[] PROGMEM;
#define heart_height 30
#define heart_width 30
#define heart_height 16
#define heart_width 16
extern const unsigned char heart[] PROGMEM;
#define poo_height 30
#define poo_width 30
#define poo_height 16
#define poo_width 16
extern const unsigned char poo[] PROGMEM;
#define bell_icon_width 30
#define bell_icon_height 30
#define bell_icon_width 16
#define bell_icon_height 16
extern const unsigned char bell_icon[] PROGMEM;
#define cookie_width 16
#define cookie_height 16
extern const unsigned char cookie[] PROGMEM;
#define Fire_width 16
#define Fire_height 16
extern const unsigned char Fire[] PROGMEM;
#define peace_sign_width 16
#define peace_sign_height 16
extern const unsigned char peace_sign[] PROGMEM;
#define Praying_width 16
#define Praying_height 16
extern const unsigned char Praying[] PROGMEM;
#define Sparkles_width 16
#define Sparkles_height 16
extern const unsigned char Sparkles[] PROGMEM;
#define clown_width 16
#define clown_height 16
extern const unsigned char clown[] PROGMEM;
#define robo_width 16
#define robo_height 16
extern const unsigned char robo[] PROGMEM;
#define hole_width 16
#define hole_height 16
extern const unsigned char hole[] PROGMEM;
#define bowling_width 16
#define bowling_height 16
extern const unsigned char bowling[] PROGMEM;
#define vulcan_salute_width 16
#define vulcan_salute_height 16
extern const unsigned char vulcan_salute[] PROGMEM;
#endif // EXCLUDE_EMOJI
} // namespace graphics
} // namespace graphics
+7 -4
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())
+10 -9
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
@@ -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
@@ -287,8 +289,8 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
float easternmost = lngCenter;
float westernmost = lngCenter;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); 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
@@ -474,9 +476,9 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
// Need at least two, to draw a sensible map
bool InkHUD::MapApplet::enoughMarkers()
{
uint8_t count = 0;
for (uint8_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
size_t count = 0;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); 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
));
}
}
@@ -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
@@ -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);
@@ -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
@@ -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?
@@ -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);
@@ -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);
@@ -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);
@@ -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);
}
@@ -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
@@ -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
-1
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
-200
View File
@@ -1,200 +0,0 @@
#if defined(M5STACK_CARDPUTER_ADV)
#include "CardputerKeyboard.h"
#include "main.h"
#define _TCA8418_COLS 8
#define _TCA8418_ROWS 7
#define _TCA8418_NUM_KEYS 56
#define _TCA8418_MULTI_TAP_THRESHOLD 1500
using Key = TCA8418KeyboardBase::TCA8418Key;
constexpr uint8_t modifierFnKey = 2;
constexpr uint8_t modifierFn = 0b0010;
constexpr uint8_t modifierCtrlKey = 3;
constexpr uint8_t modifierShiftKey = 6;
constexpr uint8_t modifierShift = 0b0001;
constexpr uint8_t modifierOptKey = 7;
constexpr uint8_t modifierAltKey = 11;
// Num chars per key, Modulus for rotating through characters
static uint8_t CardputerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3};
static unsigned char CardputerTapMap[_TCA8418_NUM_KEYS][3] = {{'`', '~', Key::ESC},
{Key::TAB, 0x00, 0x00},
{0x00, 0x00, 0x00},
{0x00, 0x00, 0x00},
{'1', '!', 0x00},
{'q', 'Q', Key::REBOOT},
{0x00, 0x00, 0x00},
{0x00, 0x00, 0x00},
{'2', '@', 0x00},
{'w', 'W', 0x00},
{'a', 'A', 0x00},
{0x00, 0x00, 0x00},
{'3', '#', 0x00},
{'e', 'E', 0x00},
{'s', 'S', 0x00},
{'z', 'Z', 0x00},
{'4', '$', 0x00},
{'r', 'R', 0x00},
{'d', 'D', 0x00},
{'x', 'X', 0x00},
{'5', '%', 0x00},
{'t', 'T', 0x00},
{'f', 'F', 0x00},
{'c', 'C', 0x00},
{'6', '^', 0x00},
{'y', 'Y', 0x00},
{'g', 'G', Key::GPS_TOGGLE},
{'v', 'V', 0x00},
{'7', '&', 0x00},
{'u', 'U', 0x00},
{'h', 'H', 0x00},
{'b', 'B', Key::BT_TOGGLE},
{'8', '*', 0x00},
{'i', 'I', 0x00},
{'j', 'J', 0x00},
{'n', 'N', 0x00},
{'9', '(', 0x00},
{'o', 'O', 0x00},
{'k', 'K', 0x00},
{'m', 'M', Key::MUTE_TOGGLE},
{'0', ')', 0x00},
{'p', 'P', Key::SEND_PING},
{'l', 'L', 0x00},
{',', '<', Key::LEFT},
{'_', '-', 0x00},
{'[', '{', 0x00},
{';', ':', Key::UP},
{'.', '>', Key::DOWN},
{'=', '+', 0x00},
{']', '}', 0x00},
{'\'', '"', 0x00},
{'/', '?', Key::RIGHT},
{Key::BSP, 0x00, 0x00},
{'\\', '|', 0x00},
{Key::SELECT, 0x00, 0x00},
{' ', ' ', ' '}};
CardputerKeyboard::CardputerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
void CardputerKeyboard::reset(void)
{
TCA8418KeyboardBase::reset();
}
// handle multi-key presses (shift and alt)
void CardputerKeyboard::trigger()
{
uint8_t count = keyCount();
if (count == 0)
return;
for (uint8_t i = 0; i < count; ++i) {
uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
uint8_t key = k & 0x7F;
if (k & 0x80) {
pressed(key);
} else {
released();
state = Idle;
}
}
}
void CardputerKeyboard::pressed(uint8_t key)
{
if (state == Init || state == Busy) {
return;
}
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
return; // Invalid key
}
next_key = row * _TCA8418_COLS + col;
state = Held;
uint32_t now = millis();
tap_interval = now - last_tap;
updateModifierFlag(next_key);
if (isModifierKey(next_key)) {
last_modifier_time = now;
}
if (tap_interval < 0) {
last_tap = 0;
state = Busy;
return;
}
if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
char_idx = 0;
} else {
char_idx += 1;
}
last_key = next_key;
last_tap = now;
}
void CardputerKeyboard::released()
{
if (state != Held) {
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
state = Idle;
return;
}
uint32_t now = millis();
last_tap = now;
if (inputBroker->menuMode && modifierFlag == 0) {
if (last_key == 0 || last_key == 43 || last_key == 46 || last_key == 47 ||
last_key == 51) { // esc, left, up, down, right key
modifierFlag = modifierFn;
}
}
queueEvent(CardputerTapMap[last_key][modifierFlag % CardputerTapMod[last_key]]);
if (isModifierKey(last_key) == false)
modifierFlag = 0;
}
void CardputerKeyboard::updateModifierFlag(uint8_t key)
{
if (key == modifierShiftKey) {
modifierFlag ^= modifierShift;
} else if (key == modifierFnKey) {
modifierFlag ^= modifierFn;
}
}
bool CardputerKeyboard::isModifierKey(uint8_t key)
{
return (key == modifierShiftKey || key == modifierFnKey);
}
#endif
-26
View File
@@ -1,26 +0,0 @@
#include "TCA8418KeyboardBase.h"
class CardputerKeyboard : public TCA8418KeyboardBase
{
public:
CardputerKeyboard();
void reset(void);
void trigger(void) override;
virtual ~CardputerKeyboard() {}
protected:
void pressed(uint8_t key) override;
void released(void) override;
void updateModifierFlag(uint8_t key);
bool isModifierKey(uint8_t key);
private:
uint8_t modifierFlag;
uint32_t last_modifier_time;
int8_t last_key;
int8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
};
-2
View File
@@ -18,7 +18,6 @@ enum input_broker_event {
INPUT_BROKER_USER_PRESS,
INPUT_BROKER_ALT_PRESS,
INPUT_BROKER_ALT_LONG,
INPUT_BROKER_FACTORY_RST = 0x9a,
INPUT_BROKER_SHUTDOWN = 0x9b,
INPUT_BROKER_GPS_TOGGLE = 0x9e,
INPUT_BROKER_SEND_PING = 0xaf,
@@ -58,7 +57,6 @@ class InputBroker : public Observable<const InputEvent *>
public:
InputBroker();
bool menuMode = true;
void registerSource(Observable<const InputEvent *> *source);
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
-8
View File
@@ -7,10 +7,6 @@
#include "TDeckProKeyboard.h"
#elif defined(T_LORA_PAGER)
#include "TLoraPagerKeyboard.h"
#elif defined(M5STACK_CARDPUTER_ADV)
#include "CardputerKeyboard.h"
#elif defined(HACKADAY_COMMUNICATOR)
#include "HackadayCommunicatorKeyboard.h"
#else
#include "TCA8418Keyboard.h"
#endif
@@ -24,10 +20,6 @@ KbI2cBase::KbI2cBase(const char *name)
TCAKeyboard(*(new TDeckProKeyboard()))
#elif defined(T_LORA_PAGER)
TCAKeyboard(*(new TLoraPagerKeyboard()))
#elif defined(M5STACK_CARDPUTER_ADV)
TCAKeyboard(*(new CardputerKeyboard()))
#elif defined(HACKADAY_COMMUNICATOR)
TCAKeyboard(*(new HackadayCommunicatorKeyboard()))
#else
TCAKeyboard(*(new TCA8418Keyboard()))
#endif
+10 -9
View File
@@ -59,12 +59,12 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
NRF52Bluetooth *nrf52Bluetooth = nullptr;
#endif
#if HAS_WIFI || defined(USE_WS5500) || defined(USE_CH390D)
#if HAS_WIFI || defined(USE_WS5500)
#include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h"
#endif
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include "mesh/api/ethServerAPI.h"
#include "mesh/eth/ethClient.h"
#endif
@@ -330,7 +330,7 @@ void setup()
#ifdef WIFI_LED
pinMode(WIFI_LED, OUTPUT);
digitalWrite(WIFI_LED, HIGH ^ WIFI_STATE_ON);
digitalWrite(WIFI_LED, LOW);
#endif
#ifdef BLE_LED
@@ -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
@@ -1118,12 +1124,7 @@ void setup()
userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS;
userConfigNoScreen.longPress = INPUT_BROKER_NONE;
userConfigNoScreen.longPressTime = 500;
#if defined(ELECROW_ThinkNode_M7)
userConfigNoScreen.longLongPressTime = 15 * 1000;
userConfigNoScreen.longLongPress = INPUT_BROKER_FACTORY_RST;
#else
userConfigNoScreen.longLongPress = INPUT_BROKER_SHUTDOWN;
#endif
userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING;
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfigNoScreen);
@@ -1406,7 +1407,7 @@ void setup()
#endif
// check if the radio chip matches the selected region
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && (!rIf->wideLora())) {
if ((config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) && rIf && (!rIf->wideLora())) {
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
nodeDB->saveToDisk(SEGMENT_CONFIG);
+5 -1
View File
@@ -29,6 +29,7 @@
#else
#define default_ringtone_nag_secs 15
#endif
#define default_network_ipv6_enabled false
#define default_mqtt_address "mqtt.meshtastic.org"
#define default_mqtt_username "meshdev"
@@ -46,12 +47,15 @@ class Default
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval);
static uint32_t getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval);
static uint32_t getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue);
// Note: numOnlineNodes uses uint32_t to match the public API and allow flexibility,
// even though internal node counts use uint16_t (max 65535 nodes)
static uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes);
static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured);
static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue);
private:
static float congestionScalingCoefficient(int numOnlineNodes)
// Note: Kept as uint32_t to match the public API parameter type
static float congestionScalingCoefficient(uint32_t numOnlineNodes)
{
// Increase frequency of broadcasts for small networks regardless of preset
if (numOnlineNodes <= 10) {
+1 -1
View File
@@ -25,7 +25,7 @@ template class LR11x0Interface<LR1121>;
template class SX126xInterface<STM32WLx>;
#endif
#if HAS_ETHERNET && !defined(USE_WS5500) && !defined(USE_CH390D)
#if HAS_ETHERNET && !defined(USE_WS5500)
#include "api/ethServerAPI.h"
template class ServerAPI<EthernetClient>;
template class APIServerPort<ethServerAPI, EthernetServer>;
+3 -1
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
+18 -14
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
+1 -1
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);
+3 -3
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;
+149 -145
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>
@@ -77,14 +78,6 @@ static unsigned char userprefs_admin_key_1[] = USERPREFS_USE_ADMIN_KEY_1;
static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2;
#endif
// Weak empty variant initialization function.
// May be redefined by variant files.
void variantDefaultConfig() __attribute__((weak));
void variantDefaultConfig() {}
void variantDefaultModuleConfig() __attribute__((weak));
void variantDefaultModuleConfig() {}
#ifdef HELTEC_MESH_NODE_T114
uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
@@ -160,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;
@@ -303,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
@@ -528,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;
}
@@ -726,6 +719,12 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk));
#endif
#if defined(USERPREFS_NETWORK_IPV6_ENABLED)
config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED;
#else
config.network.ipv6_enabled = default_network_ipv6_enabled;
#endif
#ifdef DISPLAY_FLIP_SCREEN
config.display.flip_screen = true;
#endif
@@ -736,9 +735,6 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.display.screen_on_secs = 30;
config.display.wake_on_tap_or_motion = true;
#endif
#ifdef COMPASS_ORIENTATION
config.display.compass_orientation = COMPASS_ORIENTATION;
#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
if (WiFiOTA::isUpdated()) {
WiFiOTA::recoverConfig(&config.network);
@@ -751,8 +747,6 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
#endif
initConfigIntervals();
variantDefaultConfig();
variantDefaultModuleConfig();
}
void NodeDB::initConfigIntervals()
@@ -808,16 +802,11 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \
defined(ELECROW_ThinkNode_M4) || defined(ELECROW_ThinkNode_M6)
// Default to PIN_LED2 for external notification output (LED color depends on device variant)
#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312)
// Default to RAK led pin 2 (blue)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2;
#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
moduleConfig.external_notification.active = false;
#else
moduleConfig.external_notification.active = true;
#endif
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
@@ -1004,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;
@@ -1033,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);
}
@@ -1053,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)
@@ -1069,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);
}
@@ -1121,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);
@@ -1427,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);
}
@@ -1520,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++);
@@ -1529,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();
@@ -1559,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++;
}
@@ -1575,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;
}
@@ -1586,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
@@ -1601,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
}
@@ -1620,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;
}
@@ -1632,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
}
@@ -1643,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!
*/
@@ -1682,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
}
@@ -1705,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;
}
@@ -1731,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;
}
@@ -1746,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,
@@ -1789,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();
}
@@ -1813,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();
}
@@ -1829,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;
}
@@ -1848,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;
@@ -1900,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));
@@ -1917,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
@@ -1933,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)
@@ -1949,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());
@@ -1964,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;
@@ -1991,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) {
@@ -2034,6 +2037,7 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub
}
return false;
}
#endif
bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location)
{
+276 -11
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.
+4 -4
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;
+2 -2
View File
@@ -13,7 +13,7 @@ template <class T> class ProtobufModule : protected SinglePortModule
const pb_msgdesc_t *fields;
public:
uint8_t numOnlineNodes = 0;
uint16_t numOnlineNodes = 0;
/** Constructor
* name is for debugging output
*/
@@ -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;
}
+10 -5
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(),
+47 -36
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;
+193
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;
+2
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);

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