Compare commits

..
Author SHA1 Message Date
Jonathan Bennett 689e290670 Use the right transport 2026-06-08 19:17:02 -05:00
Jonathan Bennett 708a194675 Fix double bracket 2026-06-08 18:42:07 -05:00
Jonathan Bennett b8edc9ca96 trunk 2026-06-08 18:37:03 -05:00
8e29d44e19 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:35:46 -05:00
dad92dea9e Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:35:35 -05:00
e05b112f68 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:34:51 -05:00
fe615d7bf1 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 18:34:25 -05:00
Jonathan Bennett bcd9b5508f trunk 2026-06-08 16:50:58 -05:00
Jonathan Bennett ac1f7f0f80 UDP Unicast for Meshtasticd 2026-06-08 16:48:18 -05:00
Jonathan Bennett 38f15db1d0 Bump protos to latest develop and regen 2026-06-08 16:28:40 -05:00
AustinandJonathan Bennett da821ec663 Actions: Update protobufs using the triggering branch (#10612) 2026-06-08 16:21:52 -05:00
Jonathan Bennett 124bffad84 Update Thinknode m7 pins (#10635) 2026-06-08 16:10:51 -05:00
Jason PandJonathan Bennett f98abe00f3 Update clock to be 70% max versus 80% to avoid unintended overlaps (#10516) 2026-06-08 16:01:02 -05:00
Thomas GöttgensandJonathan Bennett 56a33a07f7 remove private flag 2026-06-08 15:54:38 -05:00
Thomas GöttgensandJonathan Bennett ce80433e43 activate HWID 2026-06-08 15:54:24 -05:00
3d98622b96 Add hex picker (#10650)
* Add hex picker

* Potential fix for pull request finding

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-08 14:50:55 -05:00
Jonathan BennettandGitHub d3691258d3 Update nanopb download URL in workflow 2026-06-08 14:23:22 -05:00
Jason PandGitHub 360c54f1f9 Random 2.8 Warning cleanups (#10649)
* Clean up Compass warning

* Update ICM42607PSensor.cpp
2026-06-08 08:12:57 -05:00
Thomas GöttgensandGitHub 8c4900a52f Prevent ghost nodes during onboarding (#10647)
* Prevent ghost nodes during onboarding
* Coplilot is exceptionally nit-picky today
2026-06-07 22:54:25 +02:00
bfb833982e Flip C6 to supported. (#10646)
* Flip C6 to supported.

* Re-add board_level pr

and remove redundant lib_deps

---------

Co-authored-by: Austin <vidplace7@gmail.com>
2026-06-07 15:12:59 -05:00
TomandGitHub 1410f170f9 makes clod format as it goes (#10645) 2026-06-07 08:09:19 -05:00
AustinandGitHub 14e998e6c3 ESP32: Update pioarduino to v3.3.9 (#10637)
Arduino Release v3.3.9 based on ESP-IDF v5.5.4

May help with recent compile troubles?
2026-06-06 17:03:41 -04:00
AustinandGitHub 57f678240d Add 1.25 Meter '125cm' amateur radio region support (#10638) 2026-06-06 07:43:49 -05:00
AustinandGitHub 99df0a6fe9 Update protobufs (#10636) 2026-06-05 14:46:29 -05:00
ce7444c1a1 feat: add WIZnet W5500-EVB-Pico2 + E22P-868M30S variant (#10552)
* feat: add WIZnet W5500-EVB-Pico2 + E22P-868M30S variant

Adds a community variant for the WIZnet W5500-EVB-Pico2 development
board (https://docs.wiznet.io/Product/iEthernet/W5500/w5500-evb-pico2)
paired with an external EBYTE E22P-868M30S LoRa module.

This is the hardware twin of variants/rp2350/diy/pico2_w5500_e22 — same
LoRa pinout, same W5500 pin mapping — and reuses its variant.h verbatim
via `-I variants/rp2350/diy/pico2_w5500_e22`. No duplicated pinout file.

Two differences vs pico2_w5500_e22 justify the separate env:

1. Board target: `wiznet_5500_evb_pico2` (2 MB flash) instead of
   `rpipico2` (4 MB flash). The WIZnet PCB ships with a smaller Q-SPI
   chip than a stock Pi Pico 2. Selecting the correct board makes the
   linker fail fast on overflow instead of producing a UF2 that gets
   silently truncated when flashed to the device.
2. W5500 PHY is integrated on the PCB (hard-wired SPI0 GP16-20) rather
   than supplied as an external module the user wires manually.

The E22P-868M30S uses a single combined RFEN pin (LNA + PA enable)
instead of the older E22-900M30S's split RXEN/TXEN pins. RFEN is held
HIGH at all times via `SX126X_ANT_SW 3`; TX switching still uses the
on-module DIO2 → TXEN bridge through `SX126X_DIO2_AS_RF_SWITCH`.

Build verified: wiznet_5500_evb_pico2_e22p SUCCESS
(RAM 19.2%, Flash 65.6% of 1.5 MB partition / 2 MB chip).

* style(wiznet-evb-pico2-e22p): prettier-format README tables

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-05 09:08:43 -05:00
LittleatonandGitHub e3ae0a6ab5 missing module config (#10496)
13302 and 13300 had missing module config for slot 2, also the rf switch and voltage info was missing.

Without that the auto setting in the config.yaml will try to use the default  lora-hat-rak-6421-pi-hat.yaml and things do not work correctly.
2026-06-05 08:34:28 -05:00
pdxlocationsandGitHub 733430ed45 feat: Add Module configuration for RAK13300 and RAK13302 in Slot 2 (#10632) 2026-06-05 06:10:25 -05:00
cd2466692f fix: FEM Sleep/Wake Fix - Enable PA after sleep (#10617)
* fix: release Heltec v4 FEM sleep holds

* fix: increase FEM power settle delay

* Fix heltec_V4 documentation

Fixing the heltec_v4 documentation for enabling the PA after sleep.

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

* Add comment for the next guy about why 5ms was chosen.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-04 20:40:05 -05:00
AustinandGitHub 4b424f0234 Add support for T-Beam Supreme 144mhz variant (#10624)
Adds support for T-Beam-Supreme 144mhz version
https://github.com/Xinyuan-LilyGO/LilyGo-LoRa-Series/blob/master/docs/en/t_beam_supreme/t_beam_supreme_144_hw.md

Tested / working.
2026-06-04 16:03:39 -05:00
a212d2e84f Save Frame Visibility Actions (#10576)
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-06-04 10:05:51 -05:00
HarukiToredaandGitHub 5154e81d0b Unify InkHUD message storage with BaseUI's MessageStore (#10596)
* Unified Messagestore.

* DM fix

* Copilot fixes
2026-06-04 10:05:31 -05:00
TomGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Austin
5c1b6b2a23 Size change reporting (#10488)
* feat: add firmware size reporting and comparison scripts from #9860

* feat: add silence output feature to size_report and implement tests for size reporting scripts

* rm shame

* Fix baseline artifact paths in size report workflow

* feat: add firmware size reporting and comparison scripts from #9860

* feat: add silence output feature to size_report and implement tests for size reporting scripts

* rm shame

* Fix baseline artifact paths in size report workflow

* fix write permissions

* tunk

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Austin <vidplace7@gmail.com>
2026-06-04 06:31:27 -05:00
de345939af Automatic variable hop limits based on mesh activity and size estimation (#10176)
* asdf

* Implement SphereOfInfluenceModule for traffic management and eviction tracking

* Implement Sphere of Influence module for dynamic hop limit adjustment and role-based floor

* Update SAMPLING_DENOMINATOR to improve mesh size estimation accuracy

* Add debug logging for scale factor estimation and per-hop node counts in SphereOfInfluenceModule

* Enable variable hop limits and role-based hop floors in Sphere of Influence module

* Respond to copilot review

* Disable variable hop limits and role-based hop floors in Sphere of Influence module

* Apply suggestions from code review

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

* Implement adaptive sampling for unique node ID tracking in Sphere of Influence module

* Add state persistence for Sphere of Influence module

* Enhance Sphere of Influence module with state management and adaptive sampling adjustments

* Refactor hop scaling functionality: remove SphereOfInfluenceModule and introduce HopScalingModule

- Deleted SphereOfInfluenceModule.h, consolidating its responsibilities into the new HopScalingModule.
- Added HopScalingModule.h and HopScalingModule.cpp to manage hop scaling logic, including eviction tracking and sampling-based mesh size estimation.
- Implemented methods for recording evictions and packet senders, estimating scale factors, and computing required hops based on node activity.
- Introduced state persistence for hop scaling parameters to maintain continuity across reboots.
- Enhanced thread safety and modularity by utilizing concurrency features.

* Guard out STM32. Sowwy.

* Refactor HopScalingModule: enhance sampling logic and improve state management

* Add unit tests for HopScalingModule: implement mock database and various test scenarios

* Refactor test output in HopScalingModule tests: replace printf with TEST_MESSAGE for better integration with Unity

* Refactor HopScalingModule logging: replace lastStatusMode with descriptive mode names for improved readability

* Refactor test_main.cpp: change NodeNum variable to static and improve comments for clarity

* Remove unnecessary delay in setup function for improved test performance

* Add missing include for MeshTypes in test_main.cpp

* Refactor HopScalingModule tests: enhance mesh topology scenarios and improve test clarity

* Update HopScalingModule tests: flesh out node scenarios and improve clarity for dense and sparse mesh cases

* Fix politeness factor calculations in HopScalingModule and update related test scenarios for clarity. Remove outdated design doc.

* Enhance HopScalingModule: add sampled estimate for scaling decisions and refactor initial run state management

* Add sample traffic injection for HopScaling tests to enhance sampledEst visibility

* Enhance HopScalingModule: adjust windowFraction calculation for early triggers and improve test output formatting

* Enhance HopScalingModule: add jitter functionality to sampling denominator and update tests for consistent behavior

* Enhance HopScalingModule: implement adaptive sampling denominator adjustment and add reset functionality for tests

* Enhance HopScalingTestShim: add test-only clock and window helpers, update injectSampleTraffic for adaptive sampling, and improve scenario summary output

* Enhance HopScalingModule: add detailed documentation for functions, improve clarity of jitter and sampling logic, and reset functionality in tests

* Enhance HopScalingModule: add evictionEstimate parameter to estimateScaleFactor and update related logging for improved mesh size estimation

* Enhance HopScalingModule: adjust effective rolls calculation for improved accuracy, add eviction estimate logic, responding to all copilot review points

* Implement CompactHistogram for parallel hop scaling sampling

- Added CompactHistogram class to track node hop distances with bitwise sampling.
- Integrated CompactHistogram into HopScalingModule for independent packet sampling.
- Updated NodeDB to feed both the hop scaling module and the new histogram sampler.
- Enhanced HopScalingModule with methods to sample packets for the histogram and retrieve hop distribution statistics.
- Implemented tests for CompactHistogram functionality, including sampling, window rolling, and adaptive denominator scaling.
- Updated existing tests to validate the integration of the new histogram sampling mechanism.

* Enhance CompactHistogram and HopScalingModule: add per-hop distribution functionality, improve time handling for unit tests, and refine test setup for deterministic behavior

* CompactHistogram: add mesh size estimation, improve entry replacement logic, and update logging for per-hop distribution

* Refactor HopScalingModule and CompactHistogram integration

- Removed the suggestedHopFromCompactHistogram function to streamline hop suggestion logic.
- Updated HopScalingModule to directly utilize CompactHistogram's internal methods for hop suggestions and sampling.
- Enhanced logging in HopScalingModule to provide detailed histogram statistics.
- Modified test cases to ensure comprehensive coverage of new histogram behaviors and sampling logic.
- Improved node ID distribution in tests to better exercise sampling mechanisms.
- Ensured that filtering denominators are held for 12 hours before dropping, enhancing stability in sampling.

* Refactor CompactHistogram to support 13-hour activity tracking and introduce politeness regimes

- Updated the bitfield structure to accommodate 13-hour seen tracking.
- Changed the logic in rollHour() to analyze activity over the last 0-2 hours vs. 1-3 hours for politeness factor calculation.
- Introduced three politeness levels: GENEROUS, DEFAULT, and STRICT based on recent activity ratios.
- Adjusted filtering and sampling logic to reflect the new 13-hour tracking period.
- Updated unit tests to validate new behavior and ensure proper functionality of politeness regimes.

* Enhance CompactHistogram and HopScalingModule for improved sampling and decision-making

- Introduced a session-specific hash seed in CompactHistogram to reduce bias in node ID sampling.
- Updated sampling logic to use hashed node IDs instead of raw IDs for filtering and entry management.
- Added histogram rollover tracking in HopScalingModule to ensure proper decision-making after initial data collection.
- Adjusted logging to reflect the active state of the histogram and its comparison with NodeDB advisory hops.
- Enhanced unit tests to validate new sampling logic and memory layout changes.

* Expose hashNodeId for testing in CompactHistogram

* Add mesh trend statistics to CompactHistogram for enhanced activity tracking

* Implement histogram state persistence in CompactHistogram with save and load functions

* Refactor CompactHistogram to improve entry management and enhance rollHour logging

* feat: add HopScalingModule for adaptive hop limit recommendations

Introduces HopScalingModule, a sampled hop-distance histogram that
recommends the minimum hop limit needed to reach ~40 nodes, and
automatically reducing the hops as the mesh grows.

Key design:
- 512-byte packed histogram (128 × 4-byte Record entries) embedded
   in a new HopScalingModule.
- Each Record: 16-bit node hash, 3-bit hop distance, 13-bit seen bitmap
- Sampling filter: only nodes where (hash & (denom-1)) == 0 are kept;
  denominator doubles on overflow and halves when utilisation is low
- Hourly rollHour(): tallies per-hop counts, walks scaled buckets to
  find the minimum hop satisfying TARGET_AFFECTED_NODES (40), applies a
  politeness extension based on recent/older activity ratio, shifts all
  seen bitmaps, and persists state to /prefs/hopScalingState.bin
- Hop recommendation gated by bootstrap (requires >=1 rollHour before
  overriding HOP_MAX)
- NodeDB calls samplePacketForHistogram() on every non-MQTT rx packet
- Module also estimates total mesh size and logs useful information about
  mesh characteristics.

Changes:
- src/modules/HopScalingModule.h/.cpp: new module
- src/mesh/NodeDB.cpp: wire up samplePacketForHistogram
- src/mesh/Router.cpp: consume getLastRequiredHop()
- test/test_hop_scaling/: 12-test suite covering all mesh topologies and
  anticipated operational requirements

* test: increase run iterations in sparse to dense transition test

* feat: refactor HopScalingModule to use RUNS_PER_HOUR constant and improve logging

* feat: enhance HopScalingModule with filtering denominator management and add tests for state transitions

* refactor: remove CompactHistogram module and related files

* address copilot review comments

* Tweak: packet sampling only lora

* ove role-based hop floor logic and related definitions into the module - keep it in one place.

* Refactor MockNodeDB to use nodeInfoLiteSetBit for MQTT flag setting

* Refactor hop scaling parameters and logic to integer maths and put default values in defaults.h. Small flash size reduction, no functional impact.

* Update unit test preprocessor directives to PIO_UNIT_TESTING for consistency

* refactor: improve test output organization and clarity in hop scaling tests

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-04 05:59:49 -05:00
AustinandGitHub ef51c7ec11 Add 2 Meter (~144mhz) Amateur Radio Regions (#10623)
Default slots:
ITU1_2M: Slot 26 (144.510 MHz)
ITU2_2M: Slot 51 (145.010 MHz)
ITU3_2M: Slot 33 (144.650 MHz)
2026-06-03 18:05:03 -04:00
AustinandGitHub 3e873c51b7 Add TinyFast and TinySlow presets to modem configuration and menu actions (#10597) 2026-06-03 16:42:39 +01:00
Thomas GöttgensandGitHub f86cb7781e Remove fragile JSON libraries from the firmware while retaining Meshtasticd JSON support (#10152) 2026-06-03 16:47:30 +02:00
TomandGitHub fe23dcfa3a Merge pull request #10619 from NomDeTom/test_fixes
Some fixes and tidies for testing both online and in unit_tests
2026-06-03 13:43:54 +01:00
nomdetom 2d6f2ba1a4 docs: enhance README with verbose test output instructions and usage examples 2026-06-03 11:51:29 +01:00
nomdetom 5d55353939 Some fixes and tidies for testing both online and in unit_tests 2026-06-03 02:47:23 +01:00
AustinandGitHub c3a46d4d85 Update protobufs (#10614) 2026-06-02 19:25:35 -04:00
oscgonferandGitHub 0e0e17928b Remove reocurring pio deps for SHT sensors (#10601) 2026-06-02 06:23:23 -04:00
a522973fe7 fix(t5s3-epaper): ED047TC1 display margins, remove calibration diagnostic, fix touch threshold (#10304)
* fix(t5s3-epaper): increase ED047TC1 margins to 16px on all sides

Tested on device — 16px uniform margins look noticeably cleaner than
the previous asymmetric 8–9px values. Canvas shrinks from 944×523 to
928×508 (H_OFFSET_BYTES=2, V_OFFSET_TOP/BOTTOM=16).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(t5s3-epaper): remove EINK_EDGE_LINES calibration diagnostic from ED047TC1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(t5s3-epaper): align TOUCH_THRESHOLD_X with TOUCH_THRESHOLD_Y (40px)

X axis threshold was 60 while Y was 40, causing asymmetric swipe detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-06-01 21:29:51 -04:00
AustinGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cbfa8d7ffd Add low bandwidth conversions to MeshRadio (#10595)
* Add low bandwidth conversions to MeshRadio

* Add test cases for new bandwidth conversion values (8/10/16/21/42) and round-trip tests

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 18:24:41 -04:00
AustinandGitHub 3feb155a5f Fix null pointer dereference in updateBatteryLevel function (#10588) 2026-05-31 14:46:28 -04:00
Chloe BethelandGitHub 64e35908c9 Add Xiao RP2040 and RP2350 variants (#10109) 2026-05-31 12:51:15 -04:00
Ben Meadors 8a1d6d9285 Add stable uid generation for PLI entities in allocAtakPli function 2026-05-31 12:26:00 -04:00
TomandGitHub ee441dd7b2 Merge pull request #10560 from meshtastic/migrate-overrideslot
Move overrideSlot from RegionProfile to RegionInfo (override per-region)
2026-05-30 22:04:37 +01:00
vidplace7andCopilot 178ae0a7f1 Move overrideSlot from RegionProfile to RegionInfo (override per-region)
Move default frequency (slot) override from RegionProfile to RegionInfo (set per-region).

This is usually set to `0`, but will be especially useful for Ham modes where each region default must fit within a band plan.

Co-authored-by: Copilot <copilot@github.com>
2026-05-30 16:20:17 -04:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
a08872299e Fix thinknode_m7 esp32s3 CI build failure from missing esp_eth_driver.h (#10585)
* Initial plan

* Add ESP32 ethernet header compatibility shim

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-30 11:26:02 -05:00
CopilotGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cc9d433db0 Fix mini-epaper-s3 build: resolve SensorLib isBitSet macro conflict with SparkFun MMC5983MA (#10584)
* Initial plan

* Fix SensorLib isBitSet macro conflict with SparkFun MMC5983MA library

SensorLib 0.3.4 defines isBitSet as a C preprocessor macro in SensorLib.h,
which conflicts with SparkFun_MMC5983MA_IO.h's class method of the same name.
When both libraries are included in the same translation unit (e.g., via
configuration.h → SensorRtcHelper.hpp → SensorLib chain, alongside the
SparkFun MMC5983MA library in lib_deps), the macro expansion causes compile
errors like 'expected unqualified-id before const'.

Fix: undefine the isBitSet macro right after including SensorRtcHelper.hpp
in configuration.h, so it doesn't interfere with SparkFun's class method.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-30 08:59:26 -05:00
60303968bb Add Heltec mesh node t1 (#10416)
* add heltec-mesh-node-t1

* fixed low power

* Update the sensor enumeration values.

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

* Fix memory leak in ICM42607PSensor

* fix  ST7735_MISO  error

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 12:47:09 -05:00
Catalin PatuleaandGitHub d3c7f05baa Simplify tracking of BLE connection handle & improve thread safety. (#10390)
- Redefine isConnected in terms of nimbleBluetoothConnHandle. isConnected was not safe because BLEServer::getConnectedCount is not thread-safe (https://github.com/espressif/arduino-esp32/issues/12538) while isConnected is called from various threads. Now we can avoid checking bleServer every time before calling isConnected.
- getRssi: don't try to "populate nimbleBluetoothConnHandle", that requires calling BLEServer::getPeerDevices which is not thread-safe (same issue as above).
2026-05-29 06:48:43 -05:00
Ben Meadors 1971e5ab13 Remove now unused payload variant settings in allocAtakPli function 2026-05-29 06:15:11 -05:00
Ben Meadors e2fda6598c Update protobufs 2026-05-28 21:01:30 -05:00
982440d21d Noise floor (#9347)
* add noise floor

* Sliding window noise floor

* Add getCurrentRSSI() to SimRadio for noise floor support

* Remove sendLocalStatsToPhone call from runOnce

* Change noise floor to int32_t type

* Use int32_t for RSSI sample storage in noise floor

* Remove float cast from noise floor assignment

* Fix Copilot review issues: fix noise floor logic, types, and null pointer

- Use robust busyTx/busyRx checks instead of simple isReceiving check
- Initialize noiseFloorSamples to NOISE_FLOOR_MIN instead of 0
- Move noise_floor assignment inside null check to prevent potential crash
- Change getNoiseFloor() and getAverageNoiseFloor() to return int32_t
- Fix RSSI validation to check for positive values (rssi > 0)
- Fix format specifier from %.1f to %d for int32_t
- Update comments to accurately reflect the sampling logic

* Fix RSSI condition to include zero value

* Change noise floor initialization to zero

* Disable noise floor for LR11x0 chips: getRSSI(bool) unsupported

* Remove updateNoiseFloor call from onNotify to avoid radio queue overflow

Per PR review feedback, calling updateNoiseFloor() in onNotify() for every
ISR event (ISR_TX, ISR_RX, TRANSMIT_DELAY_COMPLETED) can cause the LoRa
radio queue to get full. The noise floor sampling still happens in
startReceive() and after transmitting.

* fix lr11x0 current rssi

* Address noise floor review comments

* Address Copilot SimRadio noise floor comments

* Fix RadioLibInterface formatting

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2026-05-28 19:39:41 -05:00
Ben Meadors f9fea562aa Add TAKTALK message and room data structures to support voice/text chat 2026-05-27 09:05:38 -05:00
Ben Meadors c366296ab4 Update LoRaConfig region codes and add new amateur radio bands 2026-05-26 18:56:16 -05:00
Ben MeadorsGitHubCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
a076d97eb6 Implement ATAK Plugin V2 and drop unishox2 compression support (#10105)
* Implement ATAK Plugin V2 and drop unishox2 compression support

* Fix course calculation and improve error handling in allocAtakPli

* Update src/modules/AtakPluginModule.cpp

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

* Fix buffer overflow in allocAtakPli by ensuring null termination for callsigns

* Add missing include for concurrency::OSThread in AtakPluginModule.h

* Update src/modules/AtakPluginModule.h

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

* Refactor allocAtakPli to use configurable team and role; improve position source mapping

* Update src/modules/PositionModule.cpp

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

* Remove OSThread inheritance from AtakPluginModule - pure passthrough needs no periodic scheduling

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/bdc82eb6-77c4-4711-839c-04bcbb1aa9cd

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

* Apply clang-format (trunk fmt)

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-26 15:16:47 -05:00
Clive BlackledgeandGitHub 32dcd90abf Preserve forwarded position payload precision (#10554) 2026-05-26 06:33:11 -05:00
5b7a5b2c22 feat: add Raspberry Pi Pico 2 + W5500 + E22-900M30S variant (#10135)
* feat: add Raspberry Pi Pico 2 + W5500 + E22-900M30S variant

Adds community variant for Raspberry Pi Pico 2 (RP2350, 4 MB flash)
with external WIZnet W5500 Ethernet module and EBYTE E22-900M30S LoRa
module (SX1262, 30 dBm PA, 868/915 MHz).

Key details:
- LoRa on SPI1: GP10/11/12/13 (SCK/MOSI/MISO/CS), RST=GP15,
  DIO1=GP14, BUSY=GP2, RXEN=GP3 (held HIGH via SX126X_ANT_SW)
- W5500 on SPI0: GP16/17/18/19/20 (MISO/CS/SCK/MOSI/RST)
- SX126X_DIO2_AS_RF_SWITCH: DIO2→TXEN bridge on module handles PA
- SX126X_DIO3_TCXO_VOLTAGE 1.8: TCXO support via EBYTE_E22 flags
- DHCP timeout reduced to 10 s to avoid blocking LoRa startup
- GPS on UART1/Serial2: GP8 TX, GP9 RX
- Reuses WIZNET_5500_EVB_PICO2 code paths for Ethernet init

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* pico2_w5500_e22: rename define and address review feedback

Rename WIZNET_5500_EVB_PICO2 to PICO2_W5500_E22 so the variant-specific
define matches the variant directory name and isn't confused with an
on-board EVB SKU.

Review fixes from PR #10135:
- Gate the 10 s Ethernet DHCP timeout behind PICO2_W5500_E22 so other
  Ethernet builds keep the default 60 s behavior; apply the same timeout
  to reconnectETH() for consistency.
- Drop the unused -D EBYTE_E22 flag; EBYTE_E22_900M30S already selects
  TX_GAIN_LORA / SX126X_MAX_POWER in src/configuration.h.
- Rewrite "on-board W5500" comments to describe the external module.
- Correct README TX_GAIN_LORA value (7, not 10) and drop the EBYTE_E22
  row.

* fix(pico2_w5500_e22): drop DEBUG_RP2040_PORT=Serial

The arduino-pico framework hooks _write() when DEBUG_RP2040_PORT=Serial
is set and dumps raw debug bytes onto USB CDC, corrupting any binary
protobuf stream sent through StreamAPI (e.g. `meshtastic --port COMx`).

The variant excludes BT and WiFi, so the primary client transport is
Ethernet TCP via ethServerAPI — unaffected — but users who configure
the node over USB serial would see protobuf decode failures from
debug-byte interleaving. Removing the flag restores clean USB CDC.

Debug output can still be enabled per-build by adding -D DEBUG_RP2040_PORT=Serial1
to redirect to UART0 instead of USB CDC.

* style(pico2_w5500_e22): apply trunk fmt — fixes Trunk Check

- variant.h: clang-format 16.0.3 (drop manual #define alignment)
- README.md: prettier + add `text` language to fenced code blocks (markdownlint MD040)
- wiring.svg: svgo optimization

Resolves the Trunk Check Runner failure on this PR (3 unformatted
files + 8 markdownlint issues). No functional changes.

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

* refactor(pico2_w5500_e22): address review — move to rp2350/diy, generic guards

Maintainer feedback from NomDeTom on PR #10135:

- Move the variant from variants/rp2350/ to variants/rp2350/diy/ to
  distinguish DIY from prebuilt boards (matches the variants/*/diy/
  pattern; still discovered via the existing variants/*/diy/*/platformio.ini
  glob in the root platformio.ini).
- Replace the board-name macro PICO2_W5500_E22 in shared code with a
  generic capability macro USE_ARDUINO_ETHERNET, defined from variant.h.
  DebugConfiguration.h / ethServerAPI.h / ethClient.cpp no longer
  reference a board name.
- Drop the architecture.h hook entirely: variant.h now defines PRIVATE_HW,
  which the existing `#elif defined(PRIVATE_HW)` branch already handles.

No functional change. Build verified: pico2_w5500_e22 SUCCESS
(RAM 19.2%, Flash 28.1%).

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

* chore(pico2_w5500_e22): drop unoptimized wiring.svg to fix trunk fmt check

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-25 14:05:46 -05:00
Alexander BalyaandGitHub 5bce26d9b7 Fix SHT2x detection for INA219 addresses (#10482) 2026-05-24 21:03:24 -05:00
ManuelandGitHub ef734b73c7 fix: mbed TLS crash in Arduino 3.x (pioarduino) (#10535)
* fix mbed TLS crash

* adapt SSL_MAX_CONTENT_LEN size to framework-libs

* found two more
2026-05-22 20:14:42 -05:00
Ben Meadors c7748a1602 Fix update neighbor_info before checking update_interval in handleSetModuleConfig 2026-05-22 14:53:52 -05:00
Jaime RoldanandGitHub 91f930d5c0 fix(telemetry): stop emitting -0.001V sentinel when battery unavailable (#7958) (#10217)
* fix(telemetry): stop emitting -0.001V sentinel when battery unavailable (#7958)

* address review: use int32_t for batteryMv to avoid uint16_t signed wrap
2026-05-22 06:58:21 -05:00
κρμγandGitHub f2c5cb0a05 fix: first set pinMode, then write to pin (#10520) 2026-05-21 13:30:27 -05:00
e10e13226d nrf54l15: fix SHT4x libdep -- arduino-sht, not Adafruit_SHT4X (#10515)
SHTXXSensor (the SHT4x driver) includes <SHTSensor.h>, gated by
__has_include(<SHTSensor.h>). That header ships in Sensirion/arduino-sht.
Adafruit_SHT4X ships Adafruit_SHT4X.h and has no consumer anywhere in
src/, so the SHT40 driver was silently excluded from the build -- the
nRF54L15 variant could not read an SHT4x sensor as committed in #10193.

Replace the dead Adafruit_SHT4X libdep with arduino-sht v1.2.6.
Validated on nRF54L15-DK: SHT40-AD1B @0x44, 24.3h soak, 0 reboots,
temperature/humidity telemetry stable end-to-end.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-21 11:00:10 -05:00
5e69bc6c3f Enable Narrow and Lite regions for EU (#10120)
* Enable Lite and Narrow regions and introduce getEffectiveDutyCycle for Lite profiles

* Add TrafficType enum and extend getConfiguredOrDefaultMsScaled to manage based on regionProfile settings

* Refactor telemetry modules to include TrafficType in getConfiguredOrDefaultMsScaled calls

* Update submodule protobufs to latest commit

* Add support for new region presets and modem presets in menu options

* Add new LoRa region codes and modem presets for EU bands

* boof

* Add modem presets for LITE and NARROW configurations

* Update subproject commit reference in protobufs

* Update protobufs

* Refactor modem preset definitions to use macro for consistency and clarity

* Refactor modem preset cases to use PRESET macro for consistency

* fix: update LoRa region code for EU 868 narrowband configuration

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

* Fix test suite failure

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

* Add override slot override - for when one override isn't enough.

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

* address copilot comments

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-21 10:20:09 -05:00
f3cb2bff78 Refactor keyboard cell height logic for consistency (#10501)
Adjust keyboard cell height calculation for better layout consistency across different screen sizes.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-21 06:12:34 -05:00
AustinandGitHub 894c5556cf Actions: Fix tagging upon release. (#10521)
Current release tags are actually based upon the latest state of `develop` currently...
Specify target_commitish to always use the commit that triggered the build
2026-05-21 06:09:57 -05:00
Ben MeadorsandGitHub 2f92eb8499 Refactor position precision handling to honor explicit channel settings and prevent location leaks (#10513) 2026-05-20 10:18:46 -05:00
181 changed files with 6968 additions and 6086 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json,sys,subprocess,shutil,os; f=json.load(sys.stdin).get('tool_input',{}).get('file_path',''); t=shutil.which('trunk') or os.path.expanduser('~/.cache/trunk/launcher/trunk'); f and os.path.exists(t) and subprocess.run([t,'fmt','--force',f],stderr=subprocess.DEVNULL)\" 2>/dev/null || true",
"statusMessage": "Formatting..."
}
]
}
]
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
# trunk-ignore(hadolint/DL3008): apt packages are not pinned.
# trunk-ignore(terrascan/AC_DOCKER_0002): apt packages are not pinned.
RUN apt-get update && apt-get install --no-install-recommends -y \
cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \
cmake git zip libgpiod-dev libjsoncpp-dev libbluetooth-dev libi2c-dev \
libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \
libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
+1 -1
View File
@@ -31,7 +31,7 @@ cmake --install "$WORK/ulfius/$SANITIZER" --prefix /usr
cd "$SRC/firmware"
PLATFORMIO_EXTRA_SCRIPTS=$(echo -e "pre:.clusterfuzzlite/platformio-clusterfuzzlite-pre.py\npost:.clusterfuzzlite/platformio-clusterfuzzlite-post.py")
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp bluez --silence-errors)
STATIC_LIBS=$(pkg-config --libs --static libulfius openssl libgpiod yaml-cpp jsoncpp bluez --silence-errors)
export PLATFORMIO_EXTRA_SCRIPTS
export STATIC_LIBS
export PLATFORMIO_WORKSPACE_DIR="$WORK/pio/$SANITIZER"
+1
View File
@@ -16,6 +16,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
libssl-dev \
libulfius-dev \
libyaml-cpp-dev \
libjsoncpp-dev \
pipx \
pkg-config \
python3 \
+1 -1
View File
@@ -13,7 +13,7 @@ runs:
shell: bash
run: |
sudo apt-get -y update --fix-missing
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev lsb-release
- name: Setup Python
uses: actions/setup-python@v6
+1 -1
View File
@@ -11,4 +11,4 @@ runs:
- name: Install libs needed for native build
shell: bash
run: |
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
sudo apt-get install -y libbluetooth-dev libgpiod-dev libyaml-cpp-dev libjsoncpp-dev openssl libssl-dev libulfius-dev liborcania-dev libusb-1.0-0-dev libi2c-dev libuv1-dev
+23 -8
View File
@@ -609,20 +609,35 @@ Most workflows can be triggered manually via `workflow_dispatch` for testing.
### Native unit tests (C++)
Unit tests in `test/` directory with 12 test suites:
Unit tests in `test/` directory with 17 test suites:
- `test_crypto/` - Cryptography
- `test_mqtt/` - MQTT integration
- `test_radio/` - Radio interface
- `test_mesh_module/` - Module framework
- `test_meshpacket_serializer/` - Packet serialization
- `test_transmit_history/` - Retransmission tracking
- `test_admin_radio/` - LoRa region/config validation and AdminModule dispatch
- `test_atak/` - ATAK integration
- `test_crypto/` - Cryptography
- `test_default/` - Default configuration
- `test_http_content_handler/` - HTTP handling
- `test_mac_from_string/` - MAC address parsing
- `test_mesh_module/` - Module framework
- `test_meshpacket_serializer/` - Packet serialization
- `test_mqtt/` - MQTT integration
- `test_packet_history/` - Packet history tracking
- `test_position_precision/` - Position precision helpers
- `test_radio/` - Radio interface
- `test_serial/` - Serial communication
- `test_traffic_management/` - Traffic management
- `test_transmit_history/` - Retransmission tracking
- `test_type_conversions/` - NodeDB v25 type conversion (bitfield round-trips, NodeInfoLite)
- `test_utf8/` - UTF-8 utilities
Run with: `pio test -e native`
Run command (preferred — avoids pipe-buffering and the Ubuntu externally-managed-environment error):
```bash
~/.platformio/penv/bin/python -m platformio test -e native -f test_your_suite > /tmp/test_out.txt 2>&1
grep -E 'error:|PASS|FAIL|succeeded|failed' /tmp/test_out.txt
tail -15 /tmp/test_out.txt
```
Do **not** use `pio test … | tail -N` — it discards build errors and shows stale cached results. Do **not** use `pio test … | grep` — line-buffering makes the terminal appear hung until the process exits. Redirect to a file first, then grep.
Simulation testing: `bin/test-simulator.sh`
@@ -0,0 +1,62 @@
name: Post Firmware Size Comment
on:
workflow_run:
workflows: [CI]
types: [completed]
permissions:
pull-requests: write
actions: read
jobs:
post-size-comment:
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion != 'cancelled' &&
github.repository == 'meshtastic/firmware'
continue-on-error: true
runs-on: ubuntu-latest
steps:
- name: Download size report
id: download
uses: actions/download-artifact@v8
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: size-report
path: ./
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const marker = '<!-- firmware-size-report -->';
const body = fs.readFileSync('./size-report.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('./pr-number.txt', 'utf8').trim(), 10);
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
+106 -26
View File
@@ -245,47 +245,126 @@ jobs:
path: ./*.elf
retention-days: 30
shame:
firmware-size-report:
if: github.repository == 'meshtastic/firmware'
continue-on-error: true
permissions:
contents: read
actions: read
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v6
if: github.event_name == 'pull_request'
with:
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
fetch-depth: 0
- name: Download the current manifests
- name: Download current manifests
uses: actions/download-artifact@v8
with:
path: ./manifests-new/
path: ./manifests/
pattern: manifest-*
merge-multiple: true
- name: Upload combined manifests for later commit and global stats crunching.
- name: Collect current firmware sizes
run: python3 bin/collect_sizes.py ./manifests/ ./current-sizes.json
- name: Upload size report artifact
uses: actions/upload-artifact@v7
id: upload-manifest
with:
name: manifests-${{ github.sha }}
name: firmware-sizes-${{ github.sha }}
overwrite: true
path: manifests-new/*.mt.json
- name: Find the merge base
path: ./current-sizes.json
retention-days: 90
- name: Download baseline sizes from develop
if: github.event_name == 'pull_request'
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
continue-on-error: true
id: baseline-develop
env:
base: ${{ github.base_ref }}
head: ${{ github.sha }}
# Currently broken (for-loop through EVERY artifact -- rate limiting)
# - name: Download the old manifests
# if: github.event_name == 'pull_request'
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
# env:
# GH_TOKEN: ${{ github.token }}
# merge_base: ${{ env.MERGE_BASE }}
# repo: ${{ github.repository }}
# - name: Do scan and post comment
# if: github.event_name == 'pull_request'
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
GH_TOKEN: ${{ github.token }}
run: |
RUN_ID=$(gh run list -R "${{ github.repository }}" \
--workflow CI --branch develop --status success \
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
if [ -n "$ARTIFACT_NAME" ]; then
gh run download "$RUN_ID" -R "${{ github.repository }}" \
--name "$ARTIFACT_NAME" --dir ./baseline-develop/
cp "./baseline-develop/${ARTIFACT_NAME}/current-sizes.json" ./develop-sizes.json
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Download baseline sizes from master
if: github.event_name == 'pull_request'
continue-on-error: true
id: baseline-master
env:
GH_TOKEN: ${{ github.token }}
run: |
RUN_ID=$(gh run list -R "${{ github.repository }}" \
--workflow CI --branch master --status success \
--limit 1 --json databaseId --jq '.[0].databaseId // empty')
if [ -n "$RUN_ID" ]; then
ARTIFACT_NAME=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts" \
--jq '.artifacts[] | select(.name | startswith("firmware-sizes-")) | .name' | head -1)
if [ -n "$ARTIFACT_NAME" ]; then
gh run download "$RUN_ID" -R "${{ github.repository }}" \
--name "$ARTIFACT_NAME" --dir ./baseline-master/
cp "./baseline-master/${ARTIFACT_NAME}/current-sizes.json" ./master-sizes.json
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Generate size comparison report
if: github.event_name == 'pull_request'
id: report
run: |
ARGS="./current-sizes.json"
if [ -f ./develop-sizes.json ]; then
ARGS="$ARGS --baseline develop:./develop-sizes.json"
fi
if [ -f ./master-sizes.json ]; then
ARGS="$ARGS --baseline master:./master-sizes.json"
fi
REPORT=$(python3 bin/size_report.py $ARGS)
if [ -z "$REPORT" ]; then
echo "has_report=false" >> "$GITHUB_OUTPUT"
else
echo "has_report=true" >> "$GITHUB_OUTPUT"
{
echo '<!-- firmware-size-report -->'
echo '# Firmware Size Report'
echo ''
echo "$REPORT"
echo ''
echo '---'
echo "*Updated for ${{ github.sha }}*"
} > ./size-report.md
cat ./size-report.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Save PR number
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
run: echo "${{ github.event.pull_request.number }}" > ./pr-number.txt
- name: Upload size report
if: github.event_name == 'pull_request' && steps.report.outputs.has_report == 'true'
uses: actions/upload-artifact@v7
with:
name: size-report
path: |
./size-report.md
./pr-number.txt
retention-days: 5
release-artifacts:
permissions: # Needed for 'gh release upload'.
@@ -333,6 +412,7 @@ jobs:
prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }}
target_commitish: ${{ github.sha }}
body: ${{ steps.release_notes.outputs.notes }}
- name: Download source deb
+9 -4
View File
@@ -16,13 +16,18 @@ jobs:
submodules: true
- name: Update submodule
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }}
if: ${{ github.ref_name == 'master' || github.ref_name == 'develop' }}
working-directory: protobufs
env:
# Use the branch that triggered the workflow as the protobuf branch.
GIT_BRANCH: ${{ github.ref_name }}
run: |
git submodule update --remote protobufs
git fetch --prune origin $GIT_BRANCH
git checkout origin/$GIT_BRANCH
- name: Download nanopb
run: |
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.9.1-linux-x86.tar.gz
wget https://github.com/nanopb/nanopb/releases/download/nanopb-0.4.9.1/nanopb-0.4.9.1-linux-x86.tar.gz
tar xvzf nanopb-0.4.9.1-linux-x86.tar.gz
mv nanopb-0.4.9.1-linux-x86 nanopb-0.4.9
@@ -33,7 +38,7 @@ jobs:
- name: Create pull request
uses: peter-evans/create-pull-request@v8
with:
branch: create-pull-request/update-protobufs
branch: create-pull-request/update-protobufs-${{ github.ref_name }}
labels: submodules
title: Update protobufs and classes
commit-message: Update protobufs
+13 -13
View File
@@ -10,18 +10,18 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
## Quick command reference
| Action | Command |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
| Run firmware unit tests (native) | `pio test -e native` |
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
| Format before commit | `trunk fmt` |
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
| Action | Command |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
| Run firmware unit tests (native) | `~/.platformio/penv/bin/python -m platformio test -e native > /tmp/test_out.txt 2>&1` then `grep -E 'error:\|PASS\|FAIL\|succeeded\|failed' /tmp/test_out.txt` (redirect first — piping causes line-buffering) |
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
| Format before commit | `trunk fmt` |
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
## MCP server (device + test automation)
@@ -108,7 +108,7 @@ Sequence these; don't parallelize on the same port.
| `src/modules/` | Feature modules; `Telemetry/Sensor/` has 50+ I2C sensor drivers |
| `variants/` | 200+ hardware variant definitions (`variant.h` + `platformio.ini` per board) |
| `protobufs/` | `.proto` definitions; regenerate with `bin/regen-protos.sh` |
| `test/` | Firmware unit tests (12 suites; `pio test -e native`) |
| `test/` | Firmware unit tests (17 suites; `pio test -e native`) |
| `mcp-server/` | Python MCP server + pytest hardware integration tests |
| `mcp-server/tests/` | Tiered pytest suite: `unit/`, `mesh/`, `telemetry/`, `monitor/`, `recovery/`, `ui/`, `fleet/`, `admin/`, `provisioning/` |
| `.claude/commands/` | Claude Code slash command bodies |
+2 -2
View File
@@ -14,7 +14,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
python3-pip python3-grpc-tools \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
@@ -53,7 +53,7 @@ ENV TZ=Etc/UTC
USER root
RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libjsoncpp26 libi2c0 libuv1t64 libusb-1.0-0-dev \
liborcania2.3 libulfius2.7t64 libssl3t64 \
libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
+1 -1
View File
@@ -7,7 +7,7 @@ ENV PIP_ROOT_USER_ACTION=ignore
# hadolint ignore=DL3008
RUN apt-get update && apt-get install --no-install-recommends -y \
g++ git ca-certificates pkg-config \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libgpiod-dev libyaml-cpp-dev libjsoncpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
+2 -2
View File
@@ -16,7 +16,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
py3-pip py3-grpcio-tools \
libgpiod-dev yaml-cpp-dev bluez-dev \
libgpiod-dev yaml-cpp-dev jsoncpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
&& rm -rf /var/cache/apk/* \
@@ -48,7 +48,7 @@ LABEL org.opencontainers.image.title="Meshtastic" \
USER root
RUN apk --no-cache add \
shadow libstdc++ libbsd libgpiod yaml-cpp libusb \
shadow libstdc++ libbsd libgpiod yaml-cpp jsoncpp libusb \
i2c-tools libuv libx11 libinput libxkbcommon sdl2 \
&& rm -rf /var/cache/apk/* \
&& mkdir -p /var/lib/meshtasticd \
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Collect firmware binary sizes from manifest (.mt.json) files into a single report."""
import json
import os
import sys
def collect_sizes(manifest_dir):
"""Scan manifest_dir for .mt.json files and return {board: size_bytes} dict."""
sizes = {}
for fname in sorted(os.listdir(manifest_dir)):
if not fname.endswith(".mt.json"):
continue
path = os.path.join(manifest_dir, fname)
with open(path) as f:
data = json.load(f)
board = data.get("platformioTarget", fname.replace(".mt.json", ""))
# Find the main firmware .bin size (largest .bin, excluding OTA/littlefs/bleota)
bin_size = None
for entry in data.get("files", []):
name = entry.get("name", "")
if name.startswith("firmware-") and name.endswith(".bin"):
bin_size = entry["bytes"]
break
# Fallback: any .bin that isn't ota/littlefs/bleota
if bin_size is None:
for entry in data.get("files", []):
name = entry.get("name", "")
if name.endswith(".bin") and not any(
x in name for x in ["littlefs", "bleota", "ota"]
):
bin_size = entry["bytes"]
break
if bin_size is not None:
sizes[board] = bin_size
return sizes
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <manifest_dir> <output.json>", file=sys.stderr)
sys.exit(1)
manifest_dir = sys.argv[1]
output_path = sys.argv[2]
sizes = collect_sizes(manifest_dir)
with open(output_path, "w") as f:
json.dump(sizes, f, indent=2, sort_keys=True)
print(f"Collected sizes for {len(sizes)} targets -> {output_path}")
+5 -1
View File
@@ -5,7 +5,9 @@ Meta:
- raspberry-pi
Lora:
### RAK13300 in Slot 2 pins
### RAK13300 in Slot 2
Module: sx1262
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
@@ -13,5 +15,7 @@ Lora:
Enable_Pins:
- 26
- 23
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.1
# CS: 7
+6 -2
View File
@@ -5,14 +5,18 @@ Meta:
- raspberry-pi
Lora:
### RAK13302 in Slot 2 pins
### RAK13302 in Slot 2
Module: sx1262
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
# Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.1
# CS: 7
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
-95
View File
@@ -1,95 +0,0 @@
import sys
import os
import json
from github import Github
def parseFile(path):
with open(path, "r") as f:
data = json.loads(f)
for file in data["files"]:
if file["name"].endswith(".bin"):
return file["name"], file["bytes"]
if len(sys.argv) != 4:
print(f"expected usage: {sys.argv[0]} <PR number> <path to old-manifests> <path to new-manifests>")
sys.exit(1)
pr_number = int(sys.argv[1])
token = os.getenv("GITHUB_TOKEN")
if not token:
raise EnvironmentError("GITHUB_TOKEN not found in environment.")
repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
if not repo_name:
raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
oldFiles = sys.argv[2]
old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
newFiles = sys.argv[3]
new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
startMarkdown = "# Target Size Changes\n\n"
markdown = ""
newlyIntroduced = new - old
if len(newlyIntroduced) > 0:
markdown += "## Newly Introduced Targets\n\n"
# create a table
markdown += "| File | Size |\n"
markdown += "| ---- | ---- |\n"
for f in newlyIntroduced:
name, size = parseFile(f)
markdown += f"| `{name}` | {size}b |\n"
# do not log removed targets
# PRs only run a small subset of builds, so removed targets are not meaningful
# since they are very likely to just be not ran in PR CI
both = old & new
degradations = []
improvements = []
for f in both:
oldName, oldSize = parseFile(f)
_, newSize = parseFile(f)
if oldSize != newSize:
if newSize < oldSize:
improvements.append((oldName, oldSize, newSize))
else:
degradations.append((oldName, oldSize, newSize))
if len(degradations) > 0:
markdown += "\n## Degradation\n\n"
# create a table
markdown += "| File | Difference | Old Size | New Size |\n"
markdown += "| ---- | ---------- | -------- | -------- |\n"
for oldName, oldSize, newSize in degradations:
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
if len(improvements) > 0:
markdown += "\n## Improvement\n\n"
# create a table
markdown += "| File | Difference | Old Size | New Size |\n"
markdown += "| ---- | ---------- | -------- | -------- |\n"
for oldName, oldSize, newSize in improvements:
markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
if len(markdown) == 0:
markdown = "No changes in target sizes detected."
g = Github(token)
repo = g.get_repo(repo_name)
pr = repo.get_pull(pr_number)
existing_comment = None
for comment in pr.get_issue_comments():
if comment.body.startswith(startMarkdown):
existing_comment = comment
break
final_markdown = startMarkdown + markdown
if existing_comment:
existing_comment.edit(body=final_markdown)
else:
pr.create_issue_comment(body=final_markdown)
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Compare firmware size reports and generate a markdown summary.
Usage:
size_report.py <new_sizes.json> [--baseline <label>:<old_sizes.json>]...
Examples:
# Compare PR against develop and master baselines
size_report.py pr.json --baseline develop:develop.json --baseline master:master.json
# Single baseline comparison
size_report.py pr.json --baseline develop:develop.json
# No baselines — shows sizes with blank delta columns
size_report.py pr.json
"""
import argparse
import json
import sys
def load_sizes(path):
with open(path) as f:
return json.load(f)
def format_delta(n):
"""Format byte delta with sign and human-friendly suffix."""
sign = "+" if n > 0 else ""
if abs(n) >= 1024:
return f"{sign}{n:,} ({sign}{n / 1024:.1f} KB)"
return f"{sign}{n:,}"
def generate_markdown(new_sizes, baselines, top_n=5):
"""Generate a single table with current size and delta columns per baseline.
baselines: list of (label, old_sizes_dict), may be empty
"""
labels = [label for label, _ in baselines]
# Build rows: (board, current_size, [(delta, abs_delta) per baseline])
rows = []
for board in sorted(new_sizes):
current = new_sizes[board]
deltas = []
for _, old_sizes in baselines:
old = old_sizes.get(board)
if old is not None:
d = current - old
deltas.append((d, abs(d)))
else:
deltas.append((None, 0))
# Sort key: max abs delta across baselines (biggest changes first)
max_abs = max((ad for _, ad in deltas), default=0)
rows.append((board, current, deltas, max_abs))
rows.sort(key=lambda r: r[3], reverse=True)
# Summary line
sections = []
summary_parts = [f"{len(new_sizes)} targets"]
for i, (label, old_sizes) in enumerate(baselines):
increases = sum(
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] > 0
)
decreases = sum(
1 for _, _, deltas, _ in rows if deltas[i][0] is not None and deltas[i][0] < 0
)
net = sum(
deltas[i][0] for _, _, deltas, _ in rows if deltas[i][0] is not None
)
parts = []
if increases:
parts.append(f"{increases} increased")
if decreases:
parts.append(f"{decreases} decreased")
if parts:
parts.append(f"net {format_delta(net)}")
summary_parts.append(f"vs `{label}`: {', '.join(parts)}")
else:
summary_parts.append(f"vs `{label}`: no changes")
if not baselines:
summary_parts.append("no baseline available yet")
sections.append(f"**{' | '.join(summary_parts)}**\n")
# Table header
header = "| Target | Size |"
separator = "|--------|-----:|"
for label in labels:
header += f" vs `{label}` |"
separator += "----------:|"
sections.append(header)
sections.append(separator)
def format_row(board, current, deltas):
row = f"| `{board}` | {current:,} |"
for d, _ in deltas:
if d is None:
row += " |"
elif d == 0:
row += " 0 |"
else:
icon = "📈" if d > 0 else "📉"
row += f" {icon} {format_delta(d)} |"
return row
# Top N rows always visible
top = rows[:top_n]
for board, current, deltas, _ in top:
sections.append(format_row(board, current, deltas))
# Remaining rows in expandable section
rest = rows[top_n:]
if rest:
sections.append("")
sections.append(
f"<details><summary>Show {len(rest)} more target(s)</summary>\n"
)
sections.append(header)
sections.append(separator)
for board, current, deltas, _ in rest:
sections.append(format_row(board, current, deltas))
sections.append("\n</details>")
sections.append("")
return "\n".join(sections)
def main():
parser = argparse.ArgumentParser(description="Compare firmware size reports")
parser.add_argument("new_sizes", help="Path to new sizes JSON")
parser.add_argument(
"--baseline",
action="append",
default=[],
metavar="LABEL:PATH",
help="Baseline to compare against (e.g. develop:develop.json)",
)
parser.add_argument(
"--top",
type=int,
default=5,
help="Number of top changes to show before collapsing (default: 5)",
)
args = parser.parse_args()
new_sizes = load_sizes(args.new_sizes)
# Silence output when no targets were built — repo maintainer choice
if not new_sizes:
return
baselines = []
for b in args.baseline:
if ":" not in b:
print(f"Error: baseline must be LABEL:PATH, got '{b}'", file=sys.stderr)
sys.exit(1)
label, path = b.split(":", 1)
baselines.append((label, load_sizes(path)))
md = generate_markdown(new_sizes, baselines, top_n=args.top)
print(md)
if __name__ == "__main__":
main()
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Tests for bin/collect_sizes.py and bin/size_report.py."""
import json
import os
import subprocess
import sys
import tempfile
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "bin")
def make_manifest(target, firmware_bytes, extra_files=None):
"""Create a minimal .mt.json manifest dict."""
files = [{"name": f"firmware-{target}-2.6.0.bin", "bytes": firmware_bytes}]
if extra_files:
files.extend(extra_files)
return {
"platformioTarget": target,
"version": "2.6.0.test",
"files": files,
}
def write_manifests(tmpdir, manifests):
"""Write manifest dicts as .mt.json files into tmpdir."""
for target, data in manifests.items():
path = os.path.join(tmpdir, f"firmware-{target}.mt.json")
with open(path, "w") as f:
json.dump(data, f)
def run_script(script, args):
"""Run a Python script and return (returncode, stdout, stderr)."""
result = subprocess.run(
[sys.executable, os.path.join(SCRIPTS_DIR, script)] + args,
capture_output=True,
text=True,
)
return result.returncode, result.stdout, result.stderr
def test_collect_sizes_basic():
"""collect_sizes picks up firmware-*.bin entries from manifests."""
with tempfile.TemporaryDirectory() as tmpdir:
outfile = os.path.join(tmpdir, "sizes.json")
manifests = {
"heltec-v3": make_manifest("heltec-v3", 1048576),
"rak4631": make_manifest("rak4631", 524288),
"tbeam": make_manifest("tbeam", 786432),
}
write_manifests(tmpdir, manifests)
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
assert rc == 0, f"collect_sizes failed: {stderr}"
assert "3 targets" in stdout
with open(outfile) as f:
sizes = json.load(f)
assert sizes == {"heltec-v3": 1048576, "rak4631": 524288, "tbeam": 786432}
def test_collect_sizes_fallback_bin():
"""collect_sizes falls back to non-firmware-prefixed .bin if no firmware-*.bin."""
with tempfile.TemporaryDirectory() as tmpdir:
outfile = os.path.join(tmpdir, "sizes.json")
# Manifest with only a generic .bin (no firmware- prefix)
data = {
"platformioTarget": "custom-board",
"files": [
{"name": "littlefs-custom-board.bin", "bytes": 100000},
{"name": "custom-board.bin", "bytes": 500000},
],
}
path = os.path.join(tmpdir, "firmware-custom-board.mt.json")
with open(path, "w") as f:
json.dump(data, f)
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
assert rc == 0, f"collect_sizes failed: {stderr}"
with open(outfile) as f:
sizes = json.load(f)
assert sizes == {"custom-board": 500000}
def test_collect_sizes_skips_ota_littlefs():
"""collect_sizes ignores ota/littlefs/bleota .bin files in fallback."""
with tempfile.TemporaryDirectory() as tmpdir:
outfile = os.path.join(tmpdir, "sizes.json")
data = {
"platformioTarget": "board-x",
"files": [
{"name": "littlefs-board-x.bin", "bytes": 100000},
{"name": "bleota-board-x.bin", "bytes": 50000},
{"name": "mt-board-x-ota.bin", "bytes": 60000},
],
}
path = os.path.join(tmpdir, "firmware-board-x.mt.json")
with open(path, "w") as f:
json.dump(data, f)
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
assert rc == 0
with open(outfile) as f:
sizes = json.load(f)
# No valid firmware .bin found, board should be absent
assert sizes == {}
def test_collect_sizes_ignores_non_mt_json():
"""collect_sizes skips non .mt.json files."""
with tempfile.TemporaryDirectory() as tmpdir:
outfile = os.path.join(tmpdir, "sizes.json")
# Write a valid manifest
manifests = {"rak4631": make_manifest("rak4631", 500000)}
write_manifests(tmpdir, manifests)
# Write a decoy file
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
f.write("not a manifest")
rc, stdout, stderr = run_script("collect_sizes.py", [tmpdir, outfile])
assert rc == 0
with open(outfile) as f:
sizes = json.load(f)
assert list(sizes.keys()) == ["rak4631"]
def test_size_report_no_baseline():
"""size_report with no baselines shows sizes only."""
with tempfile.TemporaryDirectory() as tmpdir:
sizes_file = os.path.join(tmpdir, "new.json")
with open(sizes_file, "w") as f:
json.dump({"heltec-v3": 1000000, "rak4631": 500000}, f)
rc, stdout, stderr = run_script("size_report.py", [sizes_file])
assert rc == 0, f"size_report failed: {stderr}"
assert "2 targets" in stdout
assert "no baseline available yet" in stdout
assert "`heltec-v3`" in stdout
assert "`rak4631`" in stdout
def test_size_report_with_baseline():
"""size_report shows deltas against a baseline."""
with tempfile.TemporaryDirectory() as tmpdir:
new_file = os.path.join(tmpdir, "new.json")
old_file = os.path.join(tmpdir, "old.json")
with open(new_file, "w") as f:
json.dump({"heltec-v3": 1050000, "rak4631": 500000, "tbeam": 800000}, f)
with open(old_file, "w") as f:
json.dump({"heltec-v3": 1000000, "rak4631": 500000, "tbeam": 810000}, f)
rc, stdout, stderr = run_script(
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
)
assert rc == 0, f"size_report failed: {stderr}"
assert "3 targets" in stdout
assert "1 increased" in stdout
assert "1 decreased" in stdout
# heltec-v3 grew by 50000
assert "📈" in stdout
# tbeam shrank by 10000
assert "📉" in stdout
# rak4631 unchanged
assert "vs `develop`" in stdout
def test_size_report_multiple_baselines():
"""size_report handles multiple baselines."""
with tempfile.TemporaryDirectory() as tmpdir:
new_file = os.path.join(tmpdir, "new.json")
dev_file = os.path.join(tmpdir, "develop.json")
master_file = os.path.join(tmpdir, "master.json")
with open(new_file, "w") as f:
json.dump({"board-a": 100000}, f)
with open(dev_file, "w") as f:
json.dump({"board-a": 95000}, f)
with open(master_file, "w") as f:
json.dump({"board-a": 90000}, f)
rc, stdout, stderr = run_script(
"size_report.py",
[new_file, "--baseline", f"develop:{dev_file}", "--baseline", f"master:{master_file}"],
)
assert rc == 0, f"size_report failed: {stderr}"
assert "vs `develop`" in stdout
assert "vs `master`" in stdout
def test_size_report_new_target_no_baseline_entry():
"""size_report handles targets not present in baseline (new boards)."""
with tempfile.TemporaryDirectory() as tmpdir:
new_file = os.path.join(tmpdir, "new.json")
old_file = os.path.join(tmpdir, "old.json")
with open(new_file, "w") as f:
json.dump({"new-board": 300000, "existing": 500000}, f)
with open(old_file, "w") as f:
json.dump({"existing": 500000}, f)
rc, stdout, stderr = run_script(
"size_report.py", [new_file, "--baseline", f"develop:{old_file}"]
)
assert rc == 0, f"size_report failed: {stderr}"
assert "`new-board`" in stdout
assert "no changes" in stdout # only existing is compared, delta=0
def test_size_report_all_unchanged():
"""size_report shows 'no changes' when all sizes match."""
with tempfile.TemporaryDirectory() as tmpdir:
sizes_file = os.path.join(tmpdir, "sizes.json")
with open(sizes_file, "w") as f:
json.dump({"board-a": 100000, "board-b": 200000}, f)
rc, stdout, stderr = run_script(
"size_report.py", [sizes_file, "--baseline", f"develop:{sizes_file}"]
)
assert rc == 0, f"size_report failed: {stderr}"
assert "no changes" in stdout
def test_collect_sizes_bad_args():
"""collect_sizes exits with error on wrong arg count."""
rc, stdout, stderr = run_script("collect_sizes.py", [])
assert rc == 1
assert "Usage" in stderr
def test_size_report_bad_baseline_format():
"""size_report exits with error on malformed --baseline."""
with tempfile.TemporaryDirectory() as tmpdir:
sizes_file = os.path.join(tmpdir, "sizes.json")
with open(sizes_file, "w") as f:
json.dump({"x": 1}, f)
rc, stdout, stderr = run_script(
"size_report.py", [sizes_file, "--baseline", "no-colon-here"]
)
assert rc == 1
assert "LABEL:PATH" in stderr
if __name__ == "__main__":
tests = [v for k, v in globals().items() if k.startswith("test_")]
passed = 0
failed = 0
for test in tests:
try:
test()
print(f" PASS: {test.__name__}")
passed += 1
except AssertionError as e:
print(f" FAIL: {test.__name__}: {e}")
failed += 1
except Exception as e:
print(f" ERROR: {test.__name__}: {type(e).__name__}: {e}")
failed += 1
print(f"\n{passed} passed, {failed} failed out of {passed + failed}")
sys.exit(1 if failed else 0)
+54
View File
@@ -0,0 +1,54 @@
{
"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"],
["0x2886", "0x1667"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_node_t1",
"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": "Heltec Mesh Node T1",
"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://heltec.org",
"vendor": "Heltec"
}
+1
View File
@@ -14,6 +14,7 @@ Build-Depends: debhelper-compat (= 13),
g++,
pkg-config,
libyaml-cpp-dev,
libjsoncpp-dev,
libgpiod-dev,
libbluetooth-dev,
libusb-1.0-0-dev,
+6 -2
View File
@@ -191,10 +191,12 @@ echo
# PASS/FAIL — every hardware test would SKIP with "role not present". We
# narrow to tests/unit explicitly so the summary reads as "no hardware,
# unit suite only" instead of "big skip count looks suspicious".
# Keep terminal output condensed (`-q -r fE`) so skip-heavy runs do not print
# each skipped test in full; skip counts still appear in pytest's summary.
if [[ -z $DETECTED && $# -eq 0 ]]; then
echo "[pre-flight] no supported devices detected; running unit tier only."
echo
exec "$VENV_PY" -m pytest tests/unit -v --report-log=tests/reportlog.jsonl
exec "$VENV_PY" -m pytest tests/unit -q -r fE --report-log=tests/reportlog.jsonl
fi
# Default pytest args when the user passed none. Power users can invoke
@@ -210,11 +212,13 @@ fi
# skipping half the hardware tests with "not baked with session profile"
# errors. Power users who know their hardware is current and want to shave
# those seconds can pass `--assume-baked` explicitly.
# Defaults also use condensed reporting (`-q -r fE`) to avoid listing every
# skipped test verbatim while still surfacing failures/errors and summary data.
if [[ $# -eq 0 ]]; then
set -- tests/ \
--html=tests/report.html --self-contained-html \
--junitxml=tests/junit.xml \
-v --tb=short
-q -r fE --tb=short
fi
# UI tier requires opencv-python-headless (and ideally easyocr). If it's
+1
View File
@@ -34,6 +34,7 @@ BuildRequires: python3dist(grpcio-tools)
BuildRequires: git-core
BuildRequires: gcc-c++
BuildRequires: pkgconfig(yaml-cpp)
BuildRequires: pkgconfig(jsoncpp)
BuildRequires: pkgconfig(libgpiod)
BuildRequires: pkgconfig(bluez)
BuildRequires: pkgconfig(libusb-1.0)
+4 -6
View File
@@ -186,12 +186,16 @@ lib_deps =
https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip
# renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary
https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip
# renovate: datasource=github-tags depName=TDK InvenSense ICM42670P packageName=tdk-invn-oss/motion.arduino.ICM42670P
https://github.com/tdk-invn-oss/motion.arduino.ICM42670P/archive/refs/tags/1.0.8.zip
# renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390
https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip
# renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075
https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip
# renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150
https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip
# renovate: datasource=github-tags depName=SparkFun MMC5983MA Magnetometer packageName=sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library
https://github.com/sparkfun/SparkFun_MMC5983MA_Magnetometer_Arduino_Library/archive/refs/tags/v1.1.4.zip
# renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561
https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip
# renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE
@@ -204,16 +208,10 @@ lib_deps =
https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip
# renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X
https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip
# renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3
https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip
# renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X
https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip
# renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31
https://github.com/adafruit/Adafruit_SHT31/archive/refs/tags/2.2.2.zip
# renovate: datasource=github-tags depName=Adafruit VEML7700 packageName=adafruit/Adafruit_VEML7700
https://github.com/adafruit/Adafruit_VEML7700/archive/refs/tags/2.1.6.zip
# renovate: datasource=github-tags depName=Adafruit SHT4x packageName=adafruit/Adafruit_SHT4X
https://github.com/adafruit/Adafruit_SHT4X/archive/refs/tags/1.0.5.zip
# renovate: datasource=github-tags depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library
https://github.com/sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library/archive/refs/tags/v1.0.6.zip
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
+3 -1
View File
@@ -152,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN
#define defaultBLEPin 123456
#if HAS_ETHERNET && defined(USE_CH390D)
#if HAS_ETHERNET && defined(USE_ARDUINO_ETHERNET)
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5500 auto-detect
#elif HAS_ETHERNET && defined(USE_CH390D)
#include <ESP32_CH390.h>
#elif HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h>
+28 -9
View File
@@ -1,4 +1,5 @@
#include "DisplayFormatters.h"
#include "MeshRadio.h"
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
bool usePreset)
@@ -11,33 +12,51 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
}
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
case PRESET(SHORT_TURBO):
return useShortName ? "ShortT" : "ShortTurbo";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case PRESET(SHORT_SLOW):
return useShortName ? "ShortS" : "ShortSlow";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case PRESET(SHORT_FAST):
return useShortName ? "ShortF" : "ShortFast";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case PRESET(MEDIUM_SLOW):
return useShortName ? "MedS" : "MediumSlow";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
case PRESET(MEDIUM_FAST):
return useShortName ? "MedF" : "MediumFast";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case PRESET(LONG_SLOW):
return useShortName ? "LongS" : "LongSlow";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
case PRESET(LONG_FAST):
return useShortName ? "LongF" : "LongFast";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
case PRESET(LONG_TURBO):
return useShortName ? "LongT" : "LongTurbo";
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case PRESET(LONG_MODERATE):
return useShortName ? "LongM" : "LongMod";
break;
case PRESET(LITE_FAST):
return useShortName ? "LiteF" : "LiteFast";
break;
case PRESET(LITE_SLOW):
return useShortName ? "LiteS" : "LiteSlow";
break;
case PRESET(NARROW_FAST):
return useShortName ? "NarF" : "NarrowFast";
break;
case PRESET(NARROW_SLOW):
return useShortName ? "NarS" : "NarrowSlow";
break;
case PRESET(TINY_FAST):
return useShortName ? "TinyF" : "TinyFast";
break;
case PRESET(TINY_SLOW):
return useShortName ? "TinyS" : "TinySlow";
break;
default:
return useShortName ? "Custom" : "Invalid";
break;
+1 -1
View File
@@ -1,5 +1,5 @@
#include "configuration.h"
#if HAS_SCREEN
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if HAS_SCREEN
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
#if defined(HELTEC_MESH_SOLAR)
+11 -1
View File
@@ -1,6 +1,16 @@
// TODO refactor this out with better radio configuration system
#ifdef USE_RF95
#ifndef RF95_RESET
#define RF95_RESET LORA_RESET
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
#endif
#ifndef RF95_IRQ
#define RF95_IRQ LORA_DIO0 // on SX1262 version this is a no connect DIO0
#endif
#ifndef RF95_DIO1
#define RF95_DIO1 LORA_DIO1 // Note: not really used for RF95, but used for pure SX127x
#endif
#endif
+1 -1
View File
@@ -51,7 +51,7 @@ size_t RedirectablePrint::write(uint8_t c)
size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_list arg)
{
va_list copy;
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
#if ARCH_PORTDUINO
static char printBuf[512];
#else
static char printBuf[160];
+4 -3
View File
@@ -133,11 +133,12 @@ bool AirTime::isTxAllowedChannelUtil(bool polite)
bool AirTime::isTxAllowedAirUtil()
{
if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) {
if (utilizationTXPercent() < myRegion->dutyCycle * polite_duty_cycle_percent / 100) {
float effectiveDutyCycle = getEffectiveDutyCycle();
if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) {
if (utilizationTXPercent() < effectiveDutyCycle * polite_duty_cycle_percent / 100) {
return true;
} else {
LOG_WARN("TX air util. >%f%%. Skip send", myRegion->dutyCycle * polite_duty_cycle_percent / 100);
LOG_WARN("TX air util. >%f%%. Skip send", effectiveDutyCycle * polite_duty_cycle_percent / 100);
return false;
}
}
+8
View File
@@ -31,6 +31,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif
#if __has_include("SensorRtcHelper.hpp")
#include "SensorRtcHelper.hpp"
// SensorLib defines isBitSet as a macro; undefine it here to avoid conflicts
// with the SparkFun MMC5983MA library, which has a class method of the same name.
#ifdef isBitSet
#undef isBitSet
#endif
#endif
/* Offer chance for variant-specific defines */
@@ -243,6 +248,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define QMI8658_ADDR 0x6B
#define QMC5883L_ADDR 0x0D
#define HMC5883L_ADDR 0x1E
#define MMC5983MA_ADDR 0x30
#define SHTC3_ADDR 0x70
#define LPS22HB_ADDR 0x5C
#define LPS22HB_ADDR_ALT 0x5D
@@ -292,6 +298,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define DA217_ADDR 0x26
#define BMI270_ADDR 0x68
#define BMI270_ADDR_ALT 0x69
#define ICM42607P_ADDR 0x68
#define ICM42607P_ADDR_ALT 0x69
// -----------------------------------------------------------------------------
// LED
+9 -2
View File
@@ -37,8 +37,15 @@ 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, BMI270, ICM42607P};
return firstOfOrNONE(11, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
{
ScanI2C::DeviceType types[] = {MMC5983MA};
return firstOfOrNONE(1, types);
}
ScanI2C::FoundDevice ScanI2C::firstAQI() const
+4
View File
@@ -41,6 +41,7 @@ class ScanI2C
QMI8658,
QMC5883L,
HMC5883L,
MMC5983MA,
PMSA003I,
QMA6100P,
MPU6050,
@@ -65,6 +66,7 @@ class ScanI2C
FT6336U,
STK8BAXX,
ICM20948,
ICM42607P,
SCD4X,
MAX30102,
TPS65233,
@@ -149,6 +151,8 @@ class ScanI2C
FoundDevice firstAccelerometer() const;
FoundDevice firstMagnetometer() const;
FoundDevice firstAQI() const;
FoundDevice firstRGBLED() const;
+47 -15
View File
@@ -179,8 +179,22 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address)
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
static uint8_t crcSHT2X(const uint8_t *data, uint8_t len)
{
uint8_t crc = 0;
for (uint8_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; bit++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1;
}
}
return crc;
}
bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
{
uint8_t serialA[8] = {0};
uint8_t serialB[6] = {0};
i2cBus->beginTransmission(address);
i2cBus->write(0xFA);
@@ -189,12 +203,13 @@ bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
if (i2cBus->endTransmission() != 0)
return false;
if (i2cBus->requestFrom(address, (uint8_t)8) != 8)
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialA)) != sizeof(serialA))
return false;
// Just flush the data
while (i2cBus->available() < 8) {
i2cBus->read();
for (uint8_t i = 0; i < sizeof(serialA); i++) {
if (!i2cBus->available())
return false;
serialA[i] = i2cBus->read();
}
i2cBus->beginTransmission(address);
@@ -204,16 +219,18 @@ bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address)
if (i2cBus->endTransmission() != 0)
return false;
if (i2cBus->requestFrom(address, (uint8_t)6) != 6)
if (i2cBus->requestFrom(address, (uint8_t)sizeof(serialB)) != sizeof(serialB))
return false;
// Just flush the data
while (i2cBus->available() < 6) {
i2cBus->read();
for (uint8_t i = 0; i < sizeof(serialB); i++) {
if (!i2cBus->available())
return false;
serialB[i] = i2cBus->read();
}
// Assume we detect the SHT21 if something came back from the request
return true;
return crcSHT2X(&serialA[0], 1) == serialA[1] && crcSHT2X(&serialA[2], 1) == serialA[3] &&
crcSHT2X(&serialA[4], 1) == serialA[5] && crcSHT2X(&serialA[6], 1) == serialA[7] &&
crcSHT2X(&serialB[0], 2) == serialB[2] && crcSHT2X(&serialB[3], 2) == serialB[5];
}
#endif
@@ -343,9 +360,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
#ifdef HAS_NCP5623
SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address);
#endif
#ifdef HAS_LP5562
SCAN_SIMPLE_CASE(LP5562_ADDR, LP5562, "LP5562", (uint8_t)addr.address);
#endif
case XPOWERS_AXP192_AXP2101_ADDRESS:
// Do we have the axp2101/192 or the TCA8418
@@ -583,6 +597,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#else
SCAN_SIMPLE_CASE(PMSA003I_ADDR, PMSA003I, "PMSA003I", (uint8_t)addr.address)
#endif
case MMC5983MA_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x2F), 1);
if (registerValue == 0x30) {
type = MMC5983MA;
logFoundDevice("MMC5983MA", (uint8_t)addr.address);
#ifdef HAS_LP5562
} else {
type = LP5562;
logFoundDevice("LP5562", (uint8_t)addr.address);
#endif
}
break;
case BMA423_ADDR: // this can also be LIS3DH_ADDR_ALT
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 2);
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
@@ -737,8 +763,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
}
break;
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, ICM42607P_ADDR_ALT, and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR, and ICM42607P_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948
type = ICM20948;
@@ -758,6 +784,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x75), 1); // WHO_AM_I
if (registerValue == 0x60) {
type = ICM42607P;
logFoundDevice("ICM-42607-P", (uint8_t)addr.address);
break;
}
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR
String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address);
+3 -5
View File
@@ -1304,7 +1304,7 @@ GnssModel_t GPS::probe(int serialSpeed)
{"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}
{"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);
@@ -1331,10 +1331,8 @@ GnssModel_t GPS::probe(int serialSpeed)
case 1: {
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580},
{"UM600", "UM600", GNSS_MODEL_UC6580},
{"CM121", "CM121", GNSS_MODEL_CM121},
{"CC1167Q", "CC1167Q", GNSS_MODEL_CM121}};
std::vector<ChipInfo> unicore = {
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}};
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
currentDelay = 20;
currentStep = 2;
+169
View File
@@ -50,6 +50,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h"
#include "SPILock.h"
#include "error.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h"
@@ -655,6 +656,10 @@ void Screen::setup()
brightness = uiconfig.screen_brightness;
}
// Restore which frames the user has hidden (persisted across reboots).
// Must happen before the first setFrames().
loadFrameVisibility();
// Detect OLED subtype (if supported by board variant)
#ifdef AutoOLEDWire_h
if (isAUTOOled)
@@ -1416,6 +1421,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
if (frameName == "chirpy") {
hiddenFrames.chirpy = !hiddenFrames.chirpy;
}
// Save the new visibility state so it survives a reboot.
saveFrameVisibility();
}
bool Screen::isFrameHidden(const std::string &frameName) const
@@ -1454,6 +1462,167 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false;
}
// ---------------------------------------------------------------------------
// Frame visibility persistence
//
// The set of hideable frames varies by build (USE_EINK, HAS_GPS, ...), so we
// serialize to a fixed bitmask where each frame name owns a permanent bit
// position. Bits for frames that don't exist in the current build are simply
// left untouched, which keeps the saved file portable across firmware variants.
// ---------------------------------------------------------------------------
namespace
{
static const char *frameVisibilityFileName = "/prefs/framevis";
constexpr uint32_t FRAMEVIS_MAGIC = 0x53495646; // "FVIS" little-endian
constexpr uint8_t FRAMEVIS_VERSION = 1;
// Permanent bit assignments. Never renumber these; only append new ones.
enum FrameVisBit : uint8_t {
FVBIT_TEXT_MESSAGE = 0,
FVBIT_WAYPOINT = 1,
FVBIT_WIFI = 2,
FVBIT_SYSTEM = 3,
FVBIT_HOME = 4,
FVBIT_CLOCK = 5,
FVBIT_NODELIST_NODES = 6,
FVBIT_NODELIST_LOCATION = 7,
FVBIT_NODELIST_LASTHEARD = 8,
FVBIT_NODELIST_HOPSIGNAL = 9,
FVBIT_NODELIST_DISTANCE = 10,
FVBIT_NODELIST_BEARINGS = 11,
FVBIT_GPS = 12,
FVBIT_LORA = 13,
FVBIT_SHOW_FAVORITES = 14,
FVBIT_CHIRPY = 15,
};
struct __attribute__((packed)) FrameVisFile {
uint32_t magic;
uint8_t version;
uint32_t mask;
};
inline void setBit(uint32_t &mask, uint8_t bit, bool value)
{
if (value)
mask |= (1UL << bit);
else
mask &= ~(1UL << bit);
}
inline bool getBit(uint32_t mask, uint8_t bit)
{
return (mask & (1UL << bit)) != 0;
}
} // namespace
uint32_t Screen::packHiddenFrames() const
{
uint32_t mask = 0;
setBit(mask, FVBIT_TEXT_MESSAGE, hiddenFrames.textMessage);
setBit(mask, FVBIT_WAYPOINT, hiddenFrames.waypoint);
setBit(mask, FVBIT_WIFI, hiddenFrames.wifi);
setBit(mask, FVBIT_SYSTEM, hiddenFrames.system);
setBit(mask, FVBIT_HOME, hiddenFrames.home);
setBit(mask, FVBIT_CLOCK, hiddenFrames.clock);
#ifndef USE_EINK
setBit(mask, FVBIT_NODELIST_NODES, hiddenFrames.nodelist_nodes);
setBit(mask, FVBIT_NODELIST_LOCATION, hiddenFrames.nodelist_location);
#endif
#ifdef USE_EINK
setBit(mask, FVBIT_NODELIST_LASTHEARD, hiddenFrames.nodelist_lastheard);
setBit(mask, FVBIT_NODELIST_HOPSIGNAL, hiddenFrames.nodelist_hopsignal);
setBit(mask, FVBIT_NODELIST_DISTANCE, hiddenFrames.nodelist_distance);
#endif
#if HAS_GPS
#ifdef USE_EINK
setBit(mask, FVBIT_NODELIST_BEARINGS, hiddenFrames.nodelist_bearings);
#endif
setBit(mask, FVBIT_GPS, hiddenFrames.gps);
#endif
setBit(mask, FVBIT_LORA, hiddenFrames.lora);
setBit(mask, FVBIT_SHOW_FAVORITES, hiddenFrames.show_favorites);
setBit(mask, FVBIT_CHIRPY, hiddenFrames.chirpy);
return mask;
}
void Screen::applyHiddenFramesMask(uint32_t mask)
{
hiddenFrames.textMessage = getBit(mask, FVBIT_TEXT_MESSAGE);
hiddenFrames.waypoint = getBit(mask, FVBIT_WAYPOINT);
hiddenFrames.wifi = getBit(mask, FVBIT_WIFI);
hiddenFrames.system = getBit(mask, FVBIT_SYSTEM);
hiddenFrames.home = getBit(mask, FVBIT_HOME);
hiddenFrames.clock = getBit(mask, FVBIT_CLOCK);
#ifndef USE_EINK
hiddenFrames.nodelist_nodes = getBit(mask, FVBIT_NODELIST_NODES);
hiddenFrames.nodelist_location = getBit(mask, FVBIT_NODELIST_LOCATION);
#endif
#ifdef USE_EINK
hiddenFrames.nodelist_lastheard = getBit(mask, FVBIT_NODELIST_LASTHEARD);
hiddenFrames.nodelist_hopsignal = getBit(mask, FVBIT_NODELIST_HOPSIGNAL);
hiddenFrames.nodelist_distance = getBit(mask, FVBIT_NODELIST_DISTANCE);
#endif
#if HAS_GPS
#ifdef USE_EINK
hiddenFrames.nodelist_bearings = getBit(mask, FVBIT_NODELIST_BEARINGS);
#endif
hiddenFrames.gps = getBit(mask, FVBIT_GPS);
#endif
hiddenFrames.lora = getBit(mask, FVBIT_LORA);
hiddenFrames.show_favorites = getBit(mask, FVBIT_SHOW_FAVORITES);
hiddenFrames.chirpy = getBit(mask, FVBIT_CHIRPY);
}
void Screen::loadFrameVisibility()
{
#ifdef FSCom
spiLock->lock();
auto file = FSCom.open(frameVisibilityFileName, FILE_O_READ);
if (file) {
FrameVisFile data{};
bool ok = file.read((uint8_t *)&data, sizeof(data)) == sizeof(data) && data.magic == FRAMEVIS_MAGIC &&
data.version == FRAMEVIS_VERSION;
file.close();
spiLock->unlock();
if (ok) {
applyHiddenFramesMask(data.mask);
LOG_INFO("Loaded frame visibility (mask 0x%08x)", data.mask);
} else {
LOG_WARN("Frame visibility file invalid, keeping defaults");
}
return;
}
spiLock->unlock();
LOG_DEBUG("No saved frame visibility, using defaults");
#endif
}
void Screen::saveFrameVisibility()
{
#ifdef FSCom
spiLock->lock();
FSCom.mkdir("/prefs");
if (FSCom.exists(frameVisibilityFileName))
FSCom.remove(frameVisibilityFileName);
auto file = FSCom.open(frameVisibilityFileName, FILE_O_WRITE);
if (file) {
FrameVisFile data{};
data.magic = FRAMEVIS_MAGIC;
data.version = FRAMEVIS_VERSION;
data.mask = packHiddenFrames();
file.write((uint8_t *)&data, sizeof(data));
file.flush();
file.close();
LOG_INFO("Saved frame visibility (mask 0x%08x)", data.mask);
} else {
LOG_WARN("Failed to open %s for writing", frameVisibilityFileName);
}
spiLock->unlock();
#endif
}
void Screen::handleStartFirmwareUpdateScreen()
{
LOG_DEBUG("Show firmware screen");
+11 -1
View File
@@ -12,7 +12,7 @@
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics
{
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, hex_picker, text_input };
struct BannerOverlayOptions {
const char *message;
@@ -623,6 +623,11 @@ class Screen : public concurrency::OSThread
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
// Persist / restore which frames are hidden, across reboots.
// Stored as a single uint32 bitmask in /prefs (see Screen.cpp for the format).
void loadFrameVisibility();
void saveFrameVisibility();
#ifdef USE_EINK
/// Draw an image to remain on E-Ink display after screen off
void setScreensaverFrames(FrameCallback einkScreensaver = NULL);
@@ -738,6 +743,11 @@ class Screen : public concurrency::OSThread
bool chirpy = true;
} hiddenFrames;
// Convert hiddenFrames to a uint32 bitmask. Bit positions are fixed per
// frame name (see Screen.cpp).
uint32_t packHiddenFrames() const;
void applyHiddenFramesMask(uint32_t mask);
/// Try to start drawing ASAP
void setFastFramerate();
+12 -8
View File
@@ -1458,7 +1458,8 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
tft->wakeup();
tft->powerSaveOff();
#endif
@@ -1469,7 +1470,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172);
@@ -1485,7 +1486,8 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
tft->sleep();
tft->powerSaveOn();
#endif
@@ -1496,7 +1498,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(false); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(0);
#endif
@@ -1511,7 +1513,7 @@ void TFTDisplay::sendCommand(uint8_t com)
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
// todo
#elif !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(_brightness);
@@ -1531,7 +1533,8 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
return tft->touch() != nullptr;
#else
return false;
@@ -1550,7 +1553,8 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
return tft->getTouch(x, y);
#else
return false;
@@ -1567,7 +1571,7 @@ bool TFTDisplay::connect()
{
concurrency::LockGuard g(spiLock);
LOG_INFO("Do TFT init");
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
tft = new TFT_eSPI;
#elif defined(HACKADAY_COMMUNICATOR)
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
+6 -8
View File
@@ -142,8 +142,9 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
if (keyboardStartY < 0)
keyboardStartY = 0;
} else {
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
cellH = KEY_HEIGHT;
// Default (non-wide, non-64px) e.g. SH1107 128x128:
// cellH = FONT_HEIGHT_SMALL - 2 so rows are tighter while still hosting the font
cellH = std::max((int)KEY_HEIGHT, FONT_HEIGHT_SMALL - 2);
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
@@ -446,11 +447,8 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
if (textX < x)
textX = x; // guard
} else {
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
textX = x + (width - textWidth + 1) / 2;
} else {
textX = x + (width - textWidth) / 2;
}
// Use ceil rounding for all screens (consistent with 128x64 behavior for numbers)
textX = x + (width - textWidth + 1) / 2;
}
int contentTop = y;
int contentH = height;
@@ -746,4 +744,4 @@ bool VirtualKeyboard::isTimedOut() const
}
} // namespace graphics
#endif
#endif
+6 -2
View File
@@ -183,9 +183,13 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
if (!scaleInitialized) {
#ifdef DISPLAY_FORCE_SMALL_FONTS
float screenwidth_target_ratio = 0.70f; // Target 70% of display width (adjustable)
#else
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
#endif
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 =
+70 -35
View File
@@ -2,6 +2,7 @@
#if HAS_SCREEN
#include "ClockRenderer.h"
#include "Default.h"
#include "DisplayFormatters.h"
#include "GPS.h"
#include "MenuHandler.h"
#include "MeshRadio.h"
@@ -180,6 +181,8 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
{"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US},
{"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433},
{"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868},
{"EU_866", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_866},
{"EU_868_NARROW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_N_868},
{"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN},
{"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP},
{"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ},
@@ -203,6 +206,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
{"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863},
{"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865},
{"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902},
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
{"ITU2_125CM (220-225)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM},
};
constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]);
@@ -244,7 +252,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
#endif
config.lora.tx_enabled = true;
initRegion();
if (myRegion->dutyCycle < 100) {
if (getEffectiveDutyCycle() < 100) {
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
}
@@ -378,42 +386,64 @@ void menuHandler::FrequencySlotPicker()
screen->showOverlayBanner(bannerOptions);
}
// Maximum presets any region can have + 1 for Back
static constexpr int MAX_PRESET_OPTIONS = 16;
static BannerOverlayOptions buildRegionPresetBanner()
{
// Static storage reused each call — safe because the banner is shown immediately after.
static const char *optionsArray[MAX_PRESET_OPTIONS];
static int optionsEnumArray[MAX_PRESET_OPTIONS];
static char presetLabelBuf[MAX_PRESET_OPTIONS][12]; // scratch space for name copies
int count = 0;
optionsArray[count] = "Back";
optionsEnumArray[count++] = -1;
if (myRegion && myRegion->profile) {
const meshtastic_Config_LoRaConfig_ModemPreset *presets = myRegion->getAvailablePresets();
size_t numPresets = myRegion->getNumPresets();
for (size_t i = 0; i < numPresets && count < MAX_PRESET_OPTIONS; ++i) {
const char *name = DisplayFormatters::getModemPresetDisplayName(presets[i], false, true);
strncpy(presetLabelBuf[count], name, sizeof(presetLabelBuf[count]) - 1);
presetLabelBuf[count][sizeof(presetLabelBuf[count]) - 1] = '\0';
optionsArray[count] = presetLabelBuf[count];
optionsEnumArray[count++] = static_cast<int>(presets[i]);
}
}
int initialSelection = 0;
for (int i = 1; i < count; ++i) {
if (optionsEnumArray[i] == static_cast<int>(config.lora.modem_preset)) {
initialSelection = i;
break;
}
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Radio Preset";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = static_cast<uint8_t>(count);
bannerOptions.InitialSelected = initialSelection;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == -1) {
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
config.lora.use_preset = true;
config.lora.modem_preset = static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(selected);
config.lora.channel_num = 0; // Reset to default channel for the preset
config.lora.override_frequency = 0; // Clear any custom frequency
service->reloadConfig(SEGMENT_CONFIG);
};
return bannerOptions;
}
void menuHandler::radioPresetPicker()
{
static const RadioPresetOption presetOptions[] = {
{"Back", OptionsAction::Back},
{"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO},
{"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE},
{"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST},
{"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW},
{"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST},
{"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW},
{"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST},
{"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO},
};
constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]);
static std::array<const char *, presetCount> presetLabels{};
auto bannerOptions =
createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
menuHandler::menuQueue = menuHandler::LoraMenu;
screen->runNow();
return;
}
if (!option.hasValue) {
return;
}
config.lora.modem_preset = option.value;
config.lora.channel_num = 0; // Reset to default channel for the preset
config.lora.override_frequency = 0; // Clear any custom frequency
service->reloadConfig(SEGMENT_CONFIG);
});
screen->showOverlayBanner(bannerOptions);
screen->showOverlayBanner(buildRegionPresetBanner());
}
void menuHandler::twelveHourPicker()
@@ -1307,6 +1337,11 @@ void menuHandler::positionBaseMenu()
if (accelerometerThread) {
accelerometerThread->calibrate(30);
}
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
if (magnetometerThread) {
magnetometerThread->calibrate(30);
}
#endif
break;
case PositionAction::GPSSmartPosition:
+111
View File
@@ -66,6 +66,15 @@ uint32_t pow_of_10(uint32_t n)
return ret;
}
uint64_t pow_of_16(uint32_t n)
{
uint64_t ret = 1;
for (uint32_t i = 0; i < n; i++) {
ret *= 16ULL;
}
return ret;
}
char graphics::NotificationRenderer::alertBannerLines[MAX_LINES + 1][64] = {};
uint8_t graphics::NotificationRenderer::alertBannerLineCount = 0;
graphics::NotificationRenderer::BannerFont graphics::NotificationRenderer::alertBannerLineFonts[MAX_LINES + 1] = {};
@@ -259,6 +268,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
case notificationTypeEnum::number_picker:
drawNumberPicker(display, state);
break;
case notificationTypeEnum::hex_picker:
drawHexPicker(display, state);
break;
}
}
@@ -345,6 +357,105 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
drawNotificationBox(display, state, linePointers, totalLines, 0);
}
void NotificationRenderer::drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
{
const char *lineStarts[MAX_LINES + 1] = {0};
uint16_t lineCount = 0;
// Parse lines
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
lineStarts[lineCount] = alertBannerMessage;
// Find lines
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
if (lineStarts[lineCount + 1][0] == '\n')
lineStarts[lineCount + 1] += 1;
lineCount++;
}
// modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_16(numDigits - curSelected))) / (pow_of_16(numDigits - curSelected - 1));
// Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
if (this_digit == 15) {
currentNumber -= 15 * (pow_of_16(numDigits - curSelected - 1));
} else {
currentNumber += (pow_of_16(numDigits - curSelected - 1));
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
if (this_digit == 0) {
currentNumber += 15 * (pow_of_16(numDigits - curSelected - 1));
} else {
currentNumber -= (pow_of_16(numDigits - curSelected - 1));
}
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
currentNumber -= this_digit * (pow_of_16(numDigits - curSelected - 1));
currentNumber += (inEvent.kbchar - 48) * (pow_of_16(numDigits - curSelected - 1));
curSelected++;
}
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
curSelected--;
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
alertBannerUntil != 0) {
resetBanner();
return;
}
if (curSelected == static_cast<int8_t>(numDigits)) {
alertBannerCallback(currentNumber);
resetBanner();
return;
}
inEvent.inputEvent = INPUT_BROKER_NONE;
if (alertBannerMessage[0] == '\0')
return;
uint16_t totalLines = lineCount + 2;
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
// copy the linestarts to display to the linePointers holder
for (uint16_t i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
std::string digits = " ";
std::string arrowPointer = " ";
for (uint16_t i = 0; i < numDigits; i++) {
// Modulo minus modulo to return just the current number
uint8_t digitValue = (currentNumber % (pow_of_16(numDigits - i))) / (pow_of_16(numDigits - i - 1));
if (digitValue < 10) {
digits += std::to_string(digitValue) + " ";
} else if (digitValue == 10) {
digits += "A ";
} else if (digitValue == 11) {
digits += "B ";
} else if (digitValue == 12) {
digits += "C ";
} else if (digitValue == 13) {
digits += "D ";
} else if (digitValue == 14) {
digits += "E ";
} else if (digitValue == 15) {
digits += "F ";
}
if (curSelected == i) {
arrowPointer += "^ ";
} else {
arrowPointer += "_ ";
}
}
linePointers[lineCount++] = digits.c_str();
linePointers[lineCount++] = arrowPointer.c_str();
drawNotificationBox(display, state, linePointers, totalLines, 0);
}
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
{
static uint32_t selectedNodenum = 0;
+1
View File
@@ -42,6 +42,7 @@ class NotificationRenderer
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawHexPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
+11 -8
View File
@@ -2,6 +2,7 @@
#if HAS_SCREEN
#include "CompassRenderer.h"
#include "GPSStatus.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "NodeListRenderer.h"
@@ -78,10 +79,12 @@ static inline void transformNeedlePoint(float localX, float localY, float sinHea
outY = static_cast<int16_t>(y);
}
#if GRAPHICS_TFT_COLORING_ENABLED
static float getCompassRingAngleOffset(float heading)
{
return (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -heading : 0.0f;
}
#endif
static inline StandardCompassNeedlePoints computeStandardCompassNeedlePoints(int16_t compassX, int16_t compassY,
uint16_t compassDiam, float headingRadian,
@@ -816,16 +819,16 @@ void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *stat
// Helper to get SNR limit based on modem preset
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
case PRESET(LONG_SLOW):
case PRESET(LONG_MODERATE):
case PRESET(LONG_FAST):
return -6.0f;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
case PRESET(MEDIUM_SLOW):
case PRESET(MEDIUM_FAST):
return -5.5f;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
case PRESET(SHORT_SLOW):
case PRESET(SHORT_FAST):
case PRESET(SHORT_TURBO):
return -4.5f;
default:
return -6.0f;
+1 -33
View File
@@ -6,15 +6,12 @@
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
The InkHUD safe-area buffer (928×508) is copied into the centre of the physical
960×540 FastEPD buffer so content clears the panel's inactive edge border.
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
*/
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
// #define EINK_EDGE_LINES
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#ifdef T5_S3_EPAPER_PRO
@@ -212,35 +209,6 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
memcpy(dstRow, srcRow, srcRowBytes);
}
#ifdef EINK_EDGE_LINES
// Draw a 1px black box at the exact boundary of the safe area within the
// physical buffer. If the margins are correct, all 4 lines should be
// fully visible and right at the edge of the usable display area.
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
const uint32_t safeX = H_OFFSET_BYTES * 8;
const uint32_t safeY = V_OFFSET_TOP;
const uint32_t safeW = DISPLAY_WIDTH;
const uint32_t safeH = DISPLAY_HEIGHT;
// Top edge: horizontal line at safeY
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY);
// Bottom edge: horizontal line at safeY + safeH - 1
for (uint32_t col = safeX; col < safeX + safeW; col++)
setPixelBlack(col, safeY + safeH - 1);
// Left edge: vertical line at safeX
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX, row);
// Right edge: vertical line at safeX + safeW - 1
for (uint32_t row = safeY; row < safeY + safeH; row++)
setPixelBlack(safeX + safeW - 1, row);
#endif
if (type == FULL) {
epaper->fullUpdate(CLEAR_SLOW, false);
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
+9 -9
View File
@@ -18,9 +18,9 @@
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
Changing these constants shifts content inward from each physical edge:
H_OFFSET_BYTES = 1 8px left margin, 8px right margin (960 8 8 = 944)
V_OFFSET_TOP = 9 9px top margin (asymmetric: top bottom)
V_OFFSET_BOTTOM = 8 8px bottom margin (540 9 8 = 523)
H_OFFSET_BYTES = 2 16px left margin, 16px right margin (960 16 16 = 928)
V_OFFSET_TOP = 16 16px top margin
V_OFFSET_BOTTOM = 16 16px bottom margin (540 16 16 = 508)
*/
@@ -61,13 +61,13 @@ class ED047TC1 : public EInk
//
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 H_OFFSET_BYTES×8 right_margin (8+8 = 16px)
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 V_OFFSET_TOP V_OFFSET_BOTTOM (9+8 = 17px)
static constexpr uint16_t DISPLAY_WIDTH = 928; // 960 H_OFFSET_BYTES×8 right_margin (16+16 = 32px)
static constexpr uint16_t DISPLAY_HEIGHT = 508; // 540 V_OFFSET_TOP V_OFFSET_BOTTOM (16+16 = 32px)
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
// visual BOTTOM: 9608944=8px physical right margin
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
static constexpr uint8_t H_OFFSET_BYTES = 2; // visual TOP : 16px physical left margin
// visual BOTTOM: 96016928=16px physical right margin
static constexpr uint8_t V_OFFSET_TOP = 16; // visual RIGHT : 16px physical top margin
static constexpr uint8_t V_OFFSET_BOTTOM = 16; // visual LEFT : 16px physical bottom margin
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
@@ -64,6 +64,12 @@ enum MenuAction {
SET_REGION_KZ_863,
SET_REGION_NP_865,
SET_REGION_BR_902,
SET_REGION_EU_866,
SET_REGION_NARROW_868,
SET_REGION_ITU1_2M,
SET_REGION_ITU2_2M,
SET_REGION_ITU3_2M,
SET_REGION_ITU2_125CM,
// Device Roles
SET_ROLE_CLIENT,
SET_ROLE_CLIENT_MUTE,
@@ -78,6 +84,13 @@ enum MenuAction {
SET_PRESET_SHORT_SLOW,
SET_PRESET_SHORT_FAST,
SET_PRESET_SHORT_TURBO,
SET_PRESET_LITE_SLOW,
SET_PRESET_LITE_FAST,
SET_PRESET_NARROW_SLOW,
SET_PRESET_NARROW_FAST,
SET_PRESET_TINY_SLOW,
SET_PRESET_TINY_FAST,
SET_PRESET_FROM_REGION, // Dynamic: preset chosen from region-available list
// Timezones
SET_TZ_US_HAWAII,
SET_TZ_US_ALASKA,
@@ -4,6 +4,7 @@
#include "DisplayFormatters.h"
#include "GPS.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "RTC.h"
#include "Router.h"
@@ -257,6 +258,11 @@ int32_t InkHUD::MenuApplet::runOnce()
return OSThread::disable();
}
// Storage for the dynamically-built region preset list — populated in showPage(NODE_CONFIG_PRESET)
static constexpr uint8_t MAX_REGION_PRESETS = 16;
static meshtastic_Config_LoRaConfig_ModemPreset regionPresets[MAX_REGION_PRESETS];
static uint8_t regionPresetCount = 0;
static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
{
if (config.lora.region == region)
@@ -276,7 +282,7 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region)
initRegion();
if (myRegion && myRegion->dutyCycle < 100) {
if (myRegion && getEffectiveDutyCycle() < 100) {
config.lora.ignore_mqtt = true;
}
@@ -770,6 +776,30 @@ void InkHUD::MenuApplet::execute(MenuItem item)
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_BR_902);
break;
case SET_REGION_EU_866:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
break;
case SET_REGION_NARROW_868:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868);
break;
case SET_REGION_ITU1_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M);
break;
case SET_REGION_ITU2_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M);
break;
case SET_REGION_ITU3_2M:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M);
break;
case SET_REGION_ITU2_125CM:
applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM);
break;
// Roles
case SET_ROLE_CLIENT:
applyDeviceRole(meshtastic_Config_DeviceConfig_Role_CLIENT);
@@ -789,37 +819,62 @@ void InkHUD::MenuApplet::execute(MenuItem item)
// Presets
case SET_PRESET_LONG_SLOW:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW);
applyLoRaPreset(PRESET(LONG_SLOW));
break;
case SET_PRESET_LONG_MODERATE:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE);
applyLoRaPreset(PRESET(LONG_MODERATE));
break;
case SET_PRESET_LONG_FAST:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
applyLoRaPreset(PRESET(LONG_FAST));
break;
case SET_PRESET_MEDIUM_SLOW:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW);
applyLoRaPreset(PRESET(MEDIUM_SLOW));
break;
case SET_PRESET_MEDIUM_FAST:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST);
applyLoRaPreset(PRESET(MEDIUM_FAST));
break;
case SET_PRESET_SHORT_SLOW:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW);
applyLoRaPreset(PRESET(SHORT_SLOW));
break;
case SET_PRESET_SHORT_FAST:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST);
applyLoRaPreset(PRESET(SHORT_FAST));
break;
case SET_PRESET_SHORT_TURBO:
applyLoRaPreset(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO);
applyLoRaPreset(PRESET(SHORT_TURBO));
break;
case SET_PRESET_NARROW_SLOW:
applyLoRaPreset(PRESET(NARROW_SLOW));
break;
case SET_PRESET_NARROW_FAST:
applyLoRaPreset(PRESET(NARROW_FAST));
break;
case SET_PRESET_TINY_SLOW:
applyLoRaPreset(PRESET(TINY_SLOW));
break;
case SET_PRESET_TINY_FAST:
applyLoRaPreset(PRESET(TINY_FAST));
break;
case SET_PRESET_FROM_REGION: {
// cursor - 1 because index 0 is "Back"
const uint8_t index = cursor - 1;
if (index < regionPresetCount) {
applyLoRaPreset(regionPresets[index]);
}
break;
}
// Timezones
case SET_TZ_US_HAWAII:
applyTimezone("HST10");
@@ -1421,6 +1476,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("US", MenuAction::SET_REGION_US, MenuPage::EXIT));
items.push_back(MenuItem("EU 868", MenuAction::SET_REGION_EU_868, MenuPage::EXIT));
items.push_back(MenuItem("EU 433", MenuAction::SET_REGION_EU_433, MenuPage::EXIT));
items.push_back(MenuItem("EU 866", MenuAction::SET_REGION_EU_866, MenuPage::EXIT));
items.push_back(MenuItem("EU 868 Narrow", MenuAction::SET_REGION_NARROW_868, MenuPage::EXIT));
items.push_back(MenuItem("CN", MenuAction::SET_REGION_CN, MenuPage::EXIT));
items.push_back(MenuItem("JP", MenuAction::SET_REGION_JP, MenuPage::EXIT));
items.push_back(MenuItem("ANZ", MenuAction::SET_REGION_ANZ, MenuPage::EXIT));
@@ -1444,19 +1501,27 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(MenuItem("KZ 863", MenuAction::SET_REGION_KZ_863, MenuPage::EXIT));
items.push_back(MenuItem("NP 865", MenuAction::SET_REGION_NP_865, MenuPage::EXIT));
items.push_back(MenuItem("BR 902", MenuAction::SET_REGION_BR_902, MenuPage::EXIT));
items.push_back(MenuItem("ITU1_2M (144-146)", MenuAction::SET_REGION_ITU1_2M, MenuPage::EXIT));
items.push_back(MenuItem("ITU2_2M (144-148)", MenuAction::SET_REGION_ITU2_2M, MenuPage::EXIT));
items.push_back(MenuItem("ITU3_2M (144-148)", MenuAction::SET_REGION_ITU3_2M, MenuPage::EXIT));
items.push_back(MenuItem("ITU2_125CM (220-225)", MenuAction::SET_REGION_ITU2_125CM, MenuPage::EXIT));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
case NODE_CONFIG_PRESET: {
previousPage = MenuPage::NODE_CONFIG_LORA;
items.push_back(MenuItem("Back", previousPage));
items.push_back(MenuItem("Long Moderate", MenuAction::SET_PRESET_LONG_MODERATE, MenuPage::EXIT));
items.push_back(MenuItem("Long Fast", MenuAction::SET_PRESET_LONG_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Medium Slow", MenuAction::SET_PRESET_MEDIUM_SLOW, MenuPage::EXIT));
items.push_back(MenuItem("Medium Fast", MenuAction::SET_PRESET_MEDIUM_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Short Slow", MenuAction::SET_PRESET_SHORT_SLOW, MenuPage::EXIT));
items.push_back(MenuItem("Short Fast", MenuAction::SET_PRESET_SHORT_FAST, MenuPage::EXIT));
items.push_back(MenuItem("Short Turbo", MenuAction::SET_PRESET_SHORT_TURBO, MenuPage::EXIT));
regionPresetCount = 0;
if (myRegion && myRegion->profile) {
const meshtastic_Config_LoRaConfig_ModemPreset *presets = myRegion->getAvailablePresets();
size_t numPresets = myRegion->getNumPresets();
for (size_t i = 0; i < numPresets && regionPresetCount < MAX_REGION_PRESETS; ++i) {
regionPresets[regionPresetCount++] = presets[i];
const char *name = DisplayFormatters::getModemPresetDisplayName(presets[i], false, true);
nodeConfigLabels.emplace_back(name);
items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_PRESET_FROM_REGION, MenuPage::EXIT));
}
}
items.push_back(MenuItem("Exit", MenuPage::EXIT));
break;
}
@@ -3,6 +3,7 @@
#include "./NotificationApplet.h"
#include "./Notification.h"
#include "MessageStore.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "meshUtils.h"
@@ -231,7 +232,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
const MessageStore::Message *message =
const StoredMessage *message =
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
// Find info about the sender
@@ -261,7 +262,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila
text += hexifyNodeNum(message->sender);
text += ": ";
text += message->text;
text += MessageStore::getText(*message);
}
}
@@ -2,6 +2,8 @@
#include "./AllMessageApplet.h"
#include "MessageStore.h"
using namespace NicheGraphics;
void InkHUD::AllMessageApplet::onActivate()
@@ -37,7 +39,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
void InkHUD::AllMessageApplet::onRender(bool full)
{
// Find newest message, regardless of whether DM or broadcast
MessageStore::Message *message;
StoredMessage *message;
if (latestMessage->wasBroadcast)
message = &latestMessage->broadcast;
else
@@ -96,7 +98,7 @@ void InkHUD::AllMessageApplet::onRender(bool full)
// ===================
// Parse any non-ascii chars in the message
std::string text = parse(message->text);
std::string text = parse(std::string(MessageStore::getText(*message)));
// Extra gap below the header
int16_t textTop = headerDivY + padDivH;
@@ -2,6 +2,8 @@
#include "./DMApplet.h"
#include "MessageStore.h"
using namespace NicheGraphics;
void InkHUD::DMApplet::onActivate()
@@ -92,7 +94,7 @@ void InkHUD::DMApplet::onRender(bool full)
// ===================
// Parse any non-ascii chars in the message
std::string text = parse(latestMessage->dm.text);
std::string text = parse(std::string(MessageStore::getText(latestMessage->dm)));
// Extra gap below the header
int16_t textTop = headerDivY + padDivH;
@@ -7,19 +7,9 @@
using namespace NicheGraphics;
// Hard limits on how much message data to write to flash
// Avoid filling the storage if something goes wrong
// Normal usage should be well below this size
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
{
// Create the message store
// Will shortly attempt to load messages from RAM, if applet is active
// Label (filename in flash) is set from channel index
store = new MessageStore("ch" + to_string(channelIndex));
}
void InkHUD::ThreadedMessageApplet::onRender(bool full)
@@ -61,17 +51,24 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
const uint16_t msgW = (msgR - msgL) + 1;
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
uint8_t i = 0; // Index of stored message
// Loop over messages
// - until no messages left, or
// - until no part of message fits on screen
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
// Iterate the global store newest-first, showing only broadcast messages on our channel
const auto &allMessages = messageStore.getLiveMessages();
int msgIdx = (int)allMessages.size() - 1;
while (msgB >= (0 - fontSmall.lineHeight()) && msgIdx >= 0) {
const StoredMessage &m = allMessages.at(msgIdx);
// Skip messages that don't belong to this channel or are DMs
if (m.type != MessageType::BROADCAST || m.channelIndex != channelIndex) {
msgIdx--;
continue;
}
// Grab data for message
const MessageStore::Message &m = store->messages.at(i);
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
bool outgoing = (m.sender == myNodeInfo.my_node_num);
std::string bodyText = parse(std::string(MessageStore::getText(m))); // Parse any non-ascii chars
// Cache bottom Y of message text
// - Used when drawing vertical line alongside
@@ -152,18 +149,13 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full)
// Move cursor up: padding before next message
msgB -= fontSmall.lineHeight() * 0.5;
i++;
msgIdx--;
} // End of loop: drawing each message
// Fade effect:
// Area immediately below the divider. Overdraw with sparse white lines.
// Make text appear to pass behind the header
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
// Those messages have been pushed off the screen-top by newer ones
while (i < store->messages.size())
store->messages.pop_back();
}
// Code which runs when the applet begins running
@@ -198,16 +190,8 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me
if (mp.to != NODENUM_BROADCAST)
return ProcessMessage::CONTINUE;
// Extract info into our slimmed-down "StoredMessage" type
MessageStore::Message newMessage;
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
newMessage.sender = mp.from;
newMessage.channelIndex = mp.channel;
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
// Store newest message at front
// These records are used when rendering, and also stored in flash at shutdown
store->messages.push_front(newMessage);
// Store in the global messageStore — this handles sender, timestamp, channel, text, and ack status
messageStore.addFromPacket(mp);
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
if (getFrom(&mp) != nodeDB->getNodeNum())
@@ -232,37 +216,25 @@ bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
return true;
}
// Save several recent messages to flash
// Stores the contents of ThreadedMessageApplet::messages
// Just enough messages to fill the display
// Messages are packed "back-to-back", to minimize blocks of flash used
// Save messages to flash via the global messageStore.
// The global store holds messages for all channels; no per-channel file is needed.
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->saveToFlash();
messageStore.saveToFlash();
}
// Load recent messages to flash
// Fills ThreadedMessageApplet::messages with previous messages
// Just enough messages have been stored to cover the display
// Messages are loaded once by InkHUD::begin() before applets start.
// Nothing to do here at per-applet activation time.
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->loadFromFlash();
// No-op: messageStore.loadFromFlash() is called in InkHUD::begin()
}
// Code to run when device is shutting down
// This is in addition to any onDeactivate() code, which will also run
// Todo: implement before a reboot also
void InkHUD::ThreadedMessageApplet::onShutdown()
{
// Save our current set of messages to flash, provided the applet isn't disabled
if (isActive())
saveMessagesToFlash();
// messageStore.saveToFlash() is called centrally by Events::beforeDeepSleep / beforeReboot
}
#endif
#endif
@@ -20,8 +20,8 @@ Suggest a max of two channel, to minimize fs usage?
#include "configuration.h"
#include "MessageStore.h"
#include "graphics/niche/InkHUD/Applet.h"
#include "graphics/niche/InkHUD/MessageStore.h"
#include "modules/TextMessageModule.h"
@@ -49,7 +49,6 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
void saveMessagesToFlash();
void loadMessagesFromFlash();
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
uint8_t channelIndex = 0;
};
+22 -25
View File
@@ -2,6 +2,7 @@
#include "./Events.h"
#include "MessageStore.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "buzz.h"
@@ -514,6 +515,7 @@ int InkHUD::Events::beforeReboot(void *unused)
inkhud->persistence->saveLatestMessage();
} else {
NicheGraphics::clearFlashData();
messageStore.clearAllMessages(); // also wipe the shared message store
}
// Note: no forceUpdate call here
@@ -532,32 +534,27 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
if (getFrom(packet) == nodeDB->getNodeNum())
return 0;
// Determine whether the message is broadcast or a DM
// Store this info to prevent confusion after a reboot
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set
inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to);
bool isBroadcastMsg = isBroadcast(packet->to);
inkhud->persistence->latestMessage.wasBroadcast = isBroadcastMsg;
// Pick the appropriate variable to store the message in
MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast
? &inkhud->persistence->latestMessage.broadcast
: &inkhud->persistence->latestMessage.dm;
// Store nodenum of the sender
// Applets can use this to fetch user data from nodedb, if they want
storedMessage->sender = packet->from;
// Store the time (epoch seconds) when message received
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
// Store the channel
// - (potentially) used to determine whether notification shows
// - (potentially) used to determine which applet to focus
storedMessage->channelIndex = packet->channel;
// Store the text
// Need to specify manually how many bytes, because source not null-terminated
storedMessage->text =
std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
if (!isBroadcastMsg) {
// DMs never pass through ThreadedMessageApplet, so add them to the global store here
// so they survive reboots. Derive the latestMessage cache entry from the stored result.
inkhud->persistence->latestMessage.dm = messageStore.addFromPacket(*packet);
} else {
// Broadcasts are added to the global store by ThreadedMessageApplet::handleReceived().
// Here we only update the latestMessage cache used by AllMessageApplet / NotificationApplet.
StoredMessage &sm = inkhud->persistence->latestMessage.broadcast;
sm.sender = packet->from;
sm.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true);
sm.channelIndex = packet->channel;
const char *payload = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
size_t storedLen = packet->decoded.payload.size;
if (storedLen >= MAX_MESSAGE_SIZE)
storedLen = MAX_MESSAGE_SIZE - 1;
sm.textOffset = MessageStore::storeText(payload, storedLen);
sm.textLength = static_cast<uint16_t>(storedLen);
}
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
}
+95
View File
@@ -9,6 +9,10 @@
#include "./SystemApplet.h"
#include "./Tile.h"
#include "./WindowManager.h"
#include "FSCommon.h"
#include "MessageStore.h"
#include "SPILock.h"
#include "concurrency/LockGuard.h"
using namespace NicheGraphics;
@@ -60,11 +64,102 @@ void InkHUD::InkHUD::notifyApplyingChanges()
}
}
// One-time migration from the old per-channel InkHUD message files (/NicheGraphics/ch*.msgs)
// to the firmware-wide MessageStore format (/Messages_default.msgs).
// Only runs when the new store loaded empty, meaning this is the first boot after the format change.
// Old files are deleted once migrated.
static void migrateOldInkHUDMessages()
{
#ifdef FSCom
bool migrated = false;
constexpr uint8_t MAX_CHANNELS = 8;
constexpr uint32_t OLD_MAX_MSG_SIZE = 250;
for (uint8_t ch = 0; ch < MAX_CHANNELS; ch++) {
std::string path = "/NicheGraphics/ch";
path += std::to_string(ch);
path += ".msgs";
spiLock->lock();
bool exists = FSCom.exists(path.c_str());
spiLock->unlock();
if (!exists)
continue;
concurrency::LockGuard guard(spiLock);
auto f = FSCom.open(path.c_str(), FILE_O_READ);
if (!f || f.size() == 0) {
if (f)
f.close();
FSCom.remove(path.c_str());
continue;
}
uint8_t count = 0;
f.readBytes(reinterpret_cast<char *>(&count), 1);
std::vector<StoredMessage> channelMsgs;
for (uint8_t i = 0; i < count; i++) {
StoredMessage sm;
f.readBytes(reinterpret_cast<char *>(&sm.timestamp), sizeof(sm.timestamp));
f.readBytes(reinterpret_cast<char *>(&sm.sender), sizeof(sm.sender));
f.readBytes(reinterpret_cast<char *>(&sm.channelIndex), sizeof(sm.channelIndex));
char textBuf[OLD_MAX_MSG_SIZE + 1] = {};
uint32_t textLen = 0;
char c;
while (textLen < OLD_MAX_MSG_SIZE) {
if (f.readBytes(&c, 1) != 1)
break;
if (c == '\0')
break;
textBuf[textLen++] = c;
}
sm.dest = NODENUM_BROADCAST;
sm.type = MessageType::BROADCAST;
sm.isBootRelative = false;
sm.ackStatus = AckStatus::ACKED;
size_t storedLen = (textLen >= MAX_MESSAGE_SIZE) ? MAX_MESSAGE_SIZE - 1 : textLen;
sm.textOffset = MessageStore::storeText(textBuf, storedLen);
sm.textLength = static_cast<uint16_t>(storedLen);
channelMsgs.push_back(sm);
}
// Old format stored newest-first (push_front); insert oldest-first for correct chronological order
for (int i = static_cast<int>(channelMsgs.size()) - 1; i >= 0; i--)
messageStore.addLiveMessage(channelMsgs[i]);
if (!channelMsgs.empty())
migrated = true;
f.close();
FSCom.remove(path.c_str());
LOG_INFO("Migrated %u messages from %s", static_cast<uint32_t>(count), path.c_str());
}
// Delete the old latest.msgs; the latestMessage cache will be re-derived from migrated channel messages
spiLock->lock();
if (FSCom.exists("/NicheGraphics/latest.msgs"))
FSCom.remove("/NicheGraphics/latest.msgs");
spiLock->unlock();
if (migrated) {
LOG_INFO("InkHUD message migration complete, saving to new format");
messageStore.saveToFlash();
}
#endif
}
// Start InkHUD!
// Call this only after you have configured InkHUD
void InkHUD::InkHUD::begin()
{
persistence->loadSettings();
messageStore.loadFromFlash(); // Load persisted messages before deriving latestMessage cache
if (messageStore.getLiveMessages().empty())
migrateOldInkHUDMessages(); // First boot after format change: import old per-channel files
persistence->loadLatestMessage();
windowManager->begin();
-156
View File
@@ -1,156 +0,0 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./MessageStore.h"
#include "SafeFile.h"
using namespace NicheGraphics;
// Hard limits on how much message data to write to flash
// Avoid filling the storage if something goes wrong
// Normal usage should be well below this size
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::MessageStore::MessageStore(const std::string &label)
{
filename = "";
filename += "/NicheGraphics";
filename += "/";
filename += label;
filename += ".msgs";
}
// Write the contents of the MessageStore::messages object to flash
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally
void InkHUD::MessageStore::saveToFlash()
{
assert(!filename.empty());
#ifdef FSCom
// Make the directory, if doesn't already exist
// This is the same directory accessed by NicheGraphics::FlashData
spiLock->lock();
FSCom.mkdir("/NicheGraphics");
spiLock->unlock();
// Open or create the file
// No "full atomic": don't save then rename
auto f = SafeFile(filename.c_str(), false);
LOG_INFO("Saving messages in %s", filename.c_str());
// Take firmware's SPI Lock while writing
spiLock->lock();
// 1st byte: how many messages will be written to store
f.write(messages.size());
// For each message
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
Message &m = messages.at(i);
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()),
min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text
f.write('\0'); // Append null term
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i),
min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
}
// Release firmware's SPI lock, because SafeFile::close needs it
spiLock->unlock();
bool writeSucceeded = f.close();
if (!writeSucceeded) {
LOG_ERROR("Can't write data!");
}
#else
LOG_ERROR("ERROR: Filesystem not implemented\n");
#endif
}
// Attempt to load the previous contents of the MessageStore:message deque from flash.
// Filename is controlled by the "label" parameter
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
void InkHUD::MessageStore::loadFromFlash()
{
// Hopefully redundant. Initial intention is to only load / save once per boot.
messages.clear();
#ifdef FSCom
// Take the firmware's SPI Lock, in case filesystem is on SD card
concurrency::LockGuard guard(spiLock);
// Check that the file *does* actually exist
if (!FSCom.exists(filename.c_str())) {
LOG_WARN("'%s' not found. Using default values", filename.c_str());
return;
}
// Check that the file *does* actually exist
if (!FSCom.exists(filename.c_str())) {
LOG_INFO("'%s' not found.", filename.c_str());
return;
}
// Open the file
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
if (f.size() == 0) {
LOG_INFO("%s is empty", filename.c_str());
f.close();
return;
}
// If opened, start reading
if (f) {
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
// First byte: how many messages are in the flash store
uint8_t flashMessageCount = 0;
f.readBytes(reinterpret_cast<char *>(&flashMessageCount), 1);
LOG_DEBUG("Messages available: %u", static_cast<uint32_t>(flashMessageCount));
// For each message
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
Message m;
// Read meta data (fixed width)
f.readBytes(reinterpret_cast<char *>(&m.timestamp), sizeof(m.timestamp));
f.readBytes(reinterpret_cast<char *>(&m.sender), sizeof(m.sender));
f.readBytes(reinterpret_cast<char *>(&m.channelIndex), sizeof(m.channelIndex));
// Read characters until we find a null term
char c;
while (m.text.size() < MAX_MESSAGE_SIZE) {
f.readBytes(&c, 1);
if (c != '\0')
m.text += c;
else
break;
}
// Store in RAM
messages.push_back(m);
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast<uint32_t>(i), m.timestamp, m.sender,
m.text.c_str());
}
f.close();
} else {
LOG_ERROR("Could not open / read %s", filename.c_str());
}
#else
LOG_ERROR("Filesystem not implemented");
state = LoadFileState::NO_FILESYSTEM;
#endif
return;
}
#endif
-47
View File
@@ -1,47 +0,0 @@
#ifdef MESHTASTIC_INCLUDE_INKHUD
/*
We hold a few recent messages, for features like the threaded message applet.
This class contains a struct for storing those messages,
and methods for serializing them to flash.
*/
#pragma once
#include "configuration.h"
#include <deque>
#include "mesh/MeshTypes.h"
namespace NicheGraphics::InkHUD
{
class MessageStore
{
public:
// A stored message
struct Message {
uint32_t timestamp; // Epoch seconds
NodeNum sender = 0;
uint8_t channelIndex;
std::string text;
};
MessageStore() = delete;
explicit MessageStore(const std::string &label); // Label determines filename in flash
void saveToFlash();
void loadFromFlash();
std::deque<Message> messages; // Interact with this object!
private:
std::string filename;
};
} // namespace NicheGraphics::InkHUD
#endif
+16 -21
View File
@@ -17,23 +17,23 @@ void InkHUD::Persistence::loadSettings()
LOG_WARN("Settings version changed. Using defaults");
}
// Load settings and latestMessage data
// Rebuild the latestMessage cache from the global messageStore.
// Called after messageStore.loadFromFlash() so that the most recent broadcast and DM
// are immediately available to applets (DMApplet, AllMessageApplet, NotificationApplet).
void InkHUD::Persistence::loadLatestMessage()
{
// Load previous "latestMessages" data from flash
MessageStore store("latest");
store.loadFromFlash();
// Place into latestMessage struct, for convenient access
// Number of strings loaded determines whether last message was broadcast or dm
if (store.messages.size() == 1) {
latestMessage.dm = store.messages.at(0);
latestMessage.wasBroadcast = false;
} else if (store.messages.size() == 2) {
latestMessage.dm = store.messages.at(0);
latestMessage.broadcast = store.messages.at(1);
latestMessage.wasBroadcast = true;
int lastBroadcastPos = -1, lastDMPos = -1, pos = 0;
for (const StoredMessage &m : messageStore.getLiveMessages()) {
if (m.type == MessageType::BROADCAST) {
latestMessage.broadcast = m;
lastBroadcastPos = pos;
} else if (m.type == MessageType::DM_TO_US) {
latestMessage.dm = m;
lastDMPos = pos;
}
pos++;
}
latestMessage.wasBroadcast = (lastBroadcastPos > lastDMPos);
}
// Save the InkHUD settings to flash
@@ -42,15 +42,10 @@ void InkHUD::Persistence::saveSettings()
FlashData<Settings>::save(&settings, "settings");
}
// Save latestMessage data to flash
// Persist all messages via the global messageStore.
void InkHUD::Persistence::saveLatestMessage()
{
// Number of strings saved determines whether last message was broadcast or dm
MessageStore store("latest");
store.messages.push_back(latestMessage.dm);
if (latestMessage.wasBroadcast)
store.messages.push_back(latestMessage.broadcast);
store.saveToFlash();
messageStore.saveToFlash();
}
/*
+6 -6
View File
@@ -15,7 +15,7 @@ The save / load mechanism is a shared NicheGraphics feature.
#include "configuration.h"
#include "./InkHUD.h"
#include "graphics/niche/InkHUD/MessageStore.h"
#include "MessageStore.h"
#include "graphics/niche/Utils/FlashData.h"
namespace NicheGraphics::InkHUD
@@ -120,12 +120,12 @@ class Persistence
};
// Most recently received text message
// Value is updated by InkHUD::WindowManager, as a courtesy to applets
// InkHUD keeps its own latest-message cache for applets.
// Value is updated by InkHUD::Events, as a courtesy to applets.
// Populated at boot from the global messageStore, then updated live on receive.
struct LatestMessage {
MessageStore::Message broadcast; // Most recent message received broadcast
MessageStore::Message dm; // Most recent received DM
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
StoredMessage broadcast; // Most recent broadcast message received
StoredMessage dm; // Most recent DM received
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
};
void loadSettings();
+20 -11
View File
@@ -422,9 +422,11 @@ Stores InkHUD data in flash
- settings
- most recent text message received (both for broadcast and DM)
In rare cases, applets may store their own specific data separately (e.g. `ThreadedMessageApplet`)
Message history (used by `ThreadedMessageApplet`) is stored by the firmware-wide `MessageStore` (`src/MessageStore.h`), not by `Persistence` directly.
Data saved only on shutdown / reboot. Not saved if power is removed unexpectedly.
Settings are saved only on shutdown / reboot. Not saved if power is removed unexpectedly.
Message history is saved periodically (every 2 hours by default), as well as on shutdown / reboot.
---
@@ -466,18 +468,21 @@ Collected here, so various user applets don't all have to store their own copy o
We keep this separate latest-message cache for this purpose, because:
- it is cleared by an outgoing text message
- we want to store both a recent broadcast and a recent DM
- we want to expose both the most recent broadcast and most recent DM independently
- applets like `DMApplet` and `NotificationApplet` need quick access without scanning the full message history
#### How messages reach the store
Broadcasts and DMs take different paths into `messageStore`:
- **Broadcasts**`ThreadedMessageApplet::handleReceived()` calls `messageStore.addFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
- **DMs**`ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.addFromPacket()` directly and stores the result in `latestMessage.dm`.
#### Saving / Loading
_A bit of a hack.._
Stored to flash using `InkHUD::MessageStore`, which is really intended for storing a thread of messages (see `ThreadedMessageApplet`). Used because it stores strings more efficiently than `FlashData.h`.
The `LatestMessage` cache is not persisted to its own file. On boot, `InkHUD::begin()` calls `messageStore.loadFromFlash()` first, then `Persistence::loadLatestMessage()` rebuilds the cache by scanning the loaded messages for the most recent broadcast and DM.
The hack is:
- If most recent message was a DM, we only store the DM.
- If most recent message was a broadcast, we store both a DM and a broadcast. The DM may be 0-length string.
Text is stored in the firmware-wide shared text pool. Use `MessageStore::getText(msg)` to retrieve it from a `StoredMessage`.
---
@@ -582,13 +587,17 @@ Handles events which impact the InkHUD system generally (e.g. shutdown, button p
Applets themselves do also listen separately for various events, but for the purpose of gathering information which they would like to display.
#### Text Messages
`Events::onReceiveTextMessage()` is the central handler for all incoming text messages. It updates the `LatestMessage` cache and, for DMs, also adds the message to `messageStore` (since `ThreadedMessageApplet` only handles broadcasts). See `Persistence::LatestMessage` for details on how the two message types are stored.
#### Buttons
Button input is sometimes handled by a system applet. `InkHUD::Events` determines whether the button should be handled by a specific system applet, or should instead trigger a default behavior
#### Factory Reset
The Events class handles the admin messages(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory. We do this because some applets (e.g. ThreadedMessageApplet) save their own data to flash, so if we erased earlier, that data would get re-written during reboot.
The Events class handles the admin message(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory (`/NicheGraphics/`) and also call `messageStore.clearAllMessages()` to wipe the firmware-wide message store (`/Messages_default.msgs`). Both are cleared during reboot rather than earlier, to avoid data being re-written by applets still running before shutdown.
---
+43 -1
View File
@@ -136,6 +136,10 @@ void printPartitionTable()
#include "motion/AccelerometerThread.h"
AccelerometerThread *accelerometerThread = nullptr;
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
#include "motion/MagnetometerThread.h"
MagnetometerThread *magnetometerThread = nullptr;
#endif
#ifdef HAS_I2S
#include "AudioThread.h"
@@ -156,6 +160,11 @@ extern void tftSetup(void);
UdpMulticastHandler *udpHandler = nullptr;
#endif
#ifdef ARCH_PORTDUINO
#include "mesh/udp/UdpUnicastConnector.h"
UdpUnicastConnector *udpUnicastConnector = nullptr;
#endif
#if defined(TCXO_OPTIONAL)
float tcxoVoltage = SX126X_DIO3_TCXO_VOLTAGE; // if TCXO is optional, put this here so it can be changed further down.
#endif
@@ -206,6 +215,8 @@ bool osk_found = false;
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Accelerometer (if found)
ScanI2C::DeviceAddress accelerometer_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the Magnetometer (if found)
ScanI2C::DeviceAddress magnetometer_found = ScanI2C::ADDRESS_NONE;
// The I2C address of the RGB LED (if found)
ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ScanI2C::ADDRESS_NONE);
/// The I2C address of our Air Quality Indicator (if found)
@@ -431,6 +442,11 @@ void setup()
digitalWrite(VEXT_ENABLE, VEXT_ON_VALUE); // turn on the display power
#endif
#if defined(PIN_SENSOR_EN)
pinMode(PIN_SENSOR_EN, OUTPUT);
digitalWrite(PIN_SENSOR_EN, PIN_SENSOR_EN_ACTIVE); // turn on sensor power
#endif
#if defined(BIAS_T_ENABLE)
pinMode(BIAS_T_ENABLE, OUTPUT);
digitalWrite(BIAS_T_ENABLE, BIAS_T_VALUE); // turn on 5V for GPS Antenna
@@ -671,6 +687,11 @@ void setup()
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
LOG_DEBUG("acc_info = %i", acc_info.type);
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
auto mag_info = i2cScanner->firstMagnetometer();
magnetometer_found = mag_info.type != ScanI2C::DeviceType::NONE ? mag_info.address : magnetometer_found;
LOG_DEBUG("mag_info = %i", mag_info.type);
#endif
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA260, meshtastic_TelemetrySensorType_INA260);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::INA226, meshtastic_TelemetrySensorType_INA226);
@@ -683,6 +704,8 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMI8658, meshtastic_TelemetrySensorType_QMI8658);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::QMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::HMC5883L, meshtastic_TelemetrySensorType_QMC5883L);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MMC5983MA, meshtastic_TelemetrySensorType_MMC5983MA);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM42607P, meshtastic_TelemetrySensorType_ICM42607P);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102);
@@ -766,6 +789,11 @@ void setup()
accelerometerThread = new AccelerometerThread(acc_info.type);
}
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_MAGNETOMETER
if (mag_info.type != ScanI2C::DeviceType::NONE) {
magnetometerThread = new MagnetometerThread(mag_info.type);
}
#endif
#if defined(HAS_NEOPIXEL) || defined(UNPHONE) || defined(RGBLED_RED)
ambientLightingThread = new AmbientLightingThread(ScanI2C::DeviceType::NONE);
@@ -909,6 +937,20 @@ void setup()
}
#endif
#endif
#ifdef ARCH_PORTDUINO
// Native unicast-UDP connector to the meshswitch routing daemon, enabled by setting a peer
// address in config.yaml (General -> UDPUnicastPeer). Independent of UDP multicast.
if (!portduino_config.udp_unicast_peer.empty()) {
LOG_DEBUG("Start UDP unicast connector");
udpUnicastConnector = new UdpUnicastConnector();
if (!udpUnicastConnector->start(portduino_config.udp_unicast_peer)) {
delete udpUnicastConnector;
udpUnicastConnector = nullptr;
}
}
#endif
service = new MeshService();
service->init();
@@ -1220,7 +1262,7 @@ void loop()
}
#endif
#endif
#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE
#if (HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)) && ENABLE_MESSAGE_PERSISTENCE
messageStoreAutosaveTick();
#endif
long delayMsec = mainController.runOrDelay();
+10
View File
@@ -39,6 +39,7 @@ extern bool kb_found;
extern bool osk_found;
extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::DeviceAddress magnetometer_found;
extern ScanI2C::FoundDevice rgb_found;
extern ScanI2C::DeviceAddress aqi_found;
@@ -66,6 +67,11 @@ extern AudioThread *audioThread;
extern UdpMulticastHandler *udpHandler;
#endif
#ifdef ARCH_PORTDUINO
#include "mesh/udp/UdpUnicastConnector.h"
extern UdpUnicastConnector *udpUnicastConnector;
#endif
// Global Screen singleton.
extern graphics::Screen *screen;
@@ -73,6 +79,10 @@ extern graphics::Screen *screen;
#include "motion/AccelerometerThread.h"
extern AccelerometerThread *accelerometerThread;
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_MAGNETOMETER
#include "motion/MagnetometerThread.h"
extern MagnetometerThread *magnetometerThread;
#endif
extern bool isVibrating;
+1 -1
View File
@@ -98,13 +98,13 @@ class Channels
int16_t getHash(ChannelIndex i) { return hashes[i]; }
private:
/** Given a channel index, change to use the crypto key specified by that index
*
* @eturn the (0 to 255) hash for that channel - if no suitable channel could be found, return -1
*/
int16_t setCrypto(ChannelIndex chIndex);
private:
/** Return the channel index for the specified channel hash, or -1 for not found */
int8_t getIndexByHash(ChannelHash channelHash);
+20
View File
@@ -60,6 +60,26 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
return base * coef;
}
uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes,
TrafficType type)
{
uint32_t baseMs = getConfiguredOrDefaultMsScaled(configured, defaultValue, numOnlineNodes);
if (!myRegion || !myRegion->profile)
return baseMs;
int8_t throttle =
(type == TrafficType::POSITION) ? myRegion->profile->positionThrottle : myRegion->profile->telemetryThrottle;
// throttle <= 0 means unset; 1 is the neutral multiplier — skip the multiply for performance
if (throttle <= 1)
return baseMs;
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
uint64_t result = static_cast<uint64_t>(baseMs) * static_cast<uint64_t>(throttle);
return result >= static_cast<uint64_t>(MAX_MS) ? MAX_MS : static_cast<uint32_t>(result);
}
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
{
// If zero, intervals should be coalesced later by getConfiguredOrDefault... methods
+10
View File
@@ -31,10 +31,18 @@
#define min_neighbor_info_broadcast_secs 4 * 60 * 60
#define default_map_publish_interval_secs 60 * 60
enum class TrafficType { POSITION, TELEMETRY };
// Traffic management defaults
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions
// Hop scaling defaults
#define default_hop_scaling_min_target_nodes 40 // walk threshold: first hop reaching this cumulative count
#define default_hop_scaling_max_target_nodes 80 // generous extension ceiling (2 × min)
#define default_hop_scaling_min_target_nodes_floor 5 // minimum allowed min_target_nodes
#define default_hop_scaling_max_target_nodes_ceiling 512 // maximum allowed max_target_nodes
#ifdef USERPREFS_RINGTONE_NAG_SECS
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
#else
@@ -64,6 +72,8 @@ class Default
// 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 uint32_t getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes,
TrafficType type);
static uint8_t getConfiguredOrDefaultHopLimit(uint8_t configured);
static uint32_t getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue);
+6
View File
@@ -368,4 +368,10 @@ template <typename T> bool LR11x0Interface<T>::sleep()
return true;
}
template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false, true);
return (int16_t)round(rssi);
}
#endif
+2
View File
@@ -37,6 +37,8 @@ template <class T> class LR11x0Interface : public RadioLibInterface
*/
T lora;
int16_t getCurrentRSSI() override;
/**
* Glue functions called from ISR land
*/
+6 -22
View File
@@ -136,17 +136,6 @@ template <typename T> bool LR20x0Interface<T>::init()
if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED)
return false;
// Some basic info about the module's explicit firmware version - no other info available
// Currently requires radiolib godmode
#if RADIOLIB_GODMODE
uint8_t fwMajor = 0;
uint8_t fwMinor = 0;
int versionRes = lora.getVersion(&fwMajor, &fwMinor);
if (versionRes == RADIOLIB_ERR_NONE)
LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor);
#endif
LOG_INFO("Frequency set to %f", getFreq());
LOG_INFO("Bandwidth set to %f", bw);
LOG_INFO("Power output set to %d", power);
@@ -154,17 +143,6 @@ template <typename T> bool LR20x0Interface<T>::init()
if (res == RADIOLIB_ERR_NONE)
res = lora.setCRC(2);
// Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024)
// Currently requires radiolib godmode
#if RADIOLIB_GODMODE
if (res == RADIOLIB_ERR_NONE) {
uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions
res = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes);
if (res != RADIOLIB_ERR_NONE)
LOG_WARN("LR2021 setRegMode failed: %d", res);
}
#endif
#ifdef LR2021_DIO_AS_RF_SWITCH
bool dioAsRfSwitch = true;
#elif defined(ARCH_PORTDUINO)
@@ -396,4 +374,10 @@ template <typename T> bool LR20x0Interface<T>::sleep()
return true;
}
template <typename T> int16_t LR20x0Interface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false, true);
return (int16_t)round(rssi);
}
#endif
+2
View File
@@ -37,6 +37,8 @@ template <class T> class LR20x0Interface : public RadioLibInterface
*/
T lora;
int16_t getCurrentRSSI() override;
/**
* Glue functions called from ISR land
*/
+81 -16
View File
@@ -2,11 +2,58 @@
#include "LoRaFEMInterface.h"
#if defined(ARCH_ESP32)
#include <driver/gpio.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#endif
LoRaFEMInterface loraFEMInterface;
static void enableFEMPower()
{
bool wasOff = digitalRead(LORA_PA_POWER) != HIGH;
digitalWrite(LORA_PA_POWER, HIGH);
if (wasOff) {
delay(5); // This is an arbitrary 5ms for FEM rail power-up.
}
}
#if defined(ARCH_ESP32)
static void releasePinHold(int pin)
{
if (pin < 0) {
return;
}
gpio_num_t gpio = (gpio_num_t)pin;
#if SOC_RTCIO_HOLD_SUPPORTED
if (rtc_gpio_is_valid_gpio(gpio)) {
rtc_gpio_hold_dis(gpio);
return;
}
#endif
if (GPIO_IS_VALID_OUTPUT_GPIO(gpio)) {
gpio_hold_dis(gpio);
}
}
static void releaseSleepHolds()
{
releasePinHold(LORA_PA_POWER);
#ifdef HELTEC_V4
releasePinHold(LORA_KCT8103L_PA_CSD);
releasePinHold(LORA_KCT8103L_PA_CTX);
#elif defined(USE_GC1109_PA)
releasePinHold(LORA_GC1109_PA_EN);
releasePinHold(LORA_GC1109_PA_TX_EN);
#elif defined(USE_KCT8103L_PA)
releasePinHold(LORA_KCT8103L_PA_CSD);
releasePinHold(LORA_KCT8103L_PA_CTX);
#endif
}
#endif
void LoRaFEMInterface::init(void)
{
setLnaCanControl(false); // Default is uncontrollable
@@ -21,6 +68,7 @@ void LoRaFEMInterface::init(void)
if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) {
// FEM is KCT8103L
fem_type = KCT8103L_PA;
LOG_INFO("Detected KCT8103L LoRa FEM");
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX);
pinMode(LORA_KCT8103L_PA_CSD, OUTPUT);
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
@@ -30,6 +78,7 @@ void LoRaFEMInterface::init(void)
} else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) {
// FEM is GC1109
fem_type = GC1109_PA;
LOG_INFO("Detected GC1109 LoRa FEM");
// LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held.
// rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN);
pinMode(LORA_GC1109_PA_EN, OUTPUT);
@@ -41,6 +90,7 @@ void LoRaFEMInterface::init(void)
}
#elif defined(USE_GC1109_PA)
fem_type = GC1109_PA;
LOG_INFO("Using GC1109 LoRa FEM");
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
#if defined(ARCH_ESP32)
@@ -55,6 +105,7 @@ void LoRaFEMInterface::init(void)
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
#elif defined(USE_KCT8103L_PA)
fem_type = KCT8103L_PA;
LOG_INFO("Using KCT8103L LoRa FEM");
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
#if defined(ARCH_ESP32)
@@ -73,6 +124,10 @@ void LoRaFEMInterface::init(void)
void LoRaFEMInterface::setSleepModeEnable(void)
{
#if defined(ARCH_ESP32)
releaseSleepHolds();
#endif
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
/*
@@ -84,6 +139,7 @@ void LoRaFEMInterface::setSleepModeEnable(void)
} else if (fem_type == KCT8103L_PA) {
// shutdown the PA
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
digitalWrite(LORA_PA_POWER, LOW);
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_GC1109_PA_EN, LOW);
@@ -91,16 +147,22 @@ void LoRaFEMInterface::setSleepModeEnable(void)
#elif defined(USE_KCT8103L_PA)
// shutdown the PA
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
digitalWrite(LORA_PA_POWER, LOW);
#endif
}
void LoRaFEMInterface::setTxModeEnable(void)
{
#if defined(ARCH_ESP32)
releaseSleepHolds();
#endif
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
} else if (fem_type == KCT8103L_PA) {
enableFEMPower();
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
@@ -108,6 +170,7 @@ void LoRaFEMInterface::setTxModeEnable(void)
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
#elif defined(USE_KCT8103L_PA)
enableFEMPower();
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
#endif
@@ -115,11 +178,16 @@ void LoRaFEMInterface::setTxModeEnable(void)
void LoRaFEMInterface::setRxModeEnable(void)
{
#if defined(ARCH_ESP32)
releaseSleepHolds();
#endif
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
} else if (fem_type == KCT8103L_PA) {
enableFEMPower();
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
@@ -131,6 +199,7 @@ void LoRaFEMInterface::setRxModeEnable(void)
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
#elif defined(USE_KCT8103L_PA)
enableFEMPower();
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
@@ -142,12 +211,14 @@ void LoRaFEMInterface::setRxModeEnable(void)
void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
{
#if defined(ARCH_ESP32)
releaseSleepHolds();
#endif
#ifdef HELTEC_V4
// Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake.
// Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown),
// then latch with RTC hold so the state survives deep sleep.
digitalWrite(LORA_PA_POWER, HIGH);
// Keep FEM rail powered during deep sleep so LoRa RX wake can work (GC1109 keeps LNA active; KCT8103L uses RX bypass).
// Set PA_POWER HIGH (overrides SX126xInterface::sleep() shutdown), then latch with RTC hold so the state survives deep sleep.
enableFEMPower();
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH);
@@ -156,15 +227,11 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
} else if (fem_type == KCT8103L_PA) {
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); // RX bypass while MCU sleeps
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_PA_POWER, HIGH);
enableFEMPower();
digitalWrite(LORA_GC1109_PA_EN, HIGH);
#if defined(ARCH_ESP32)
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
@@ -172,13 +239,11 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN);
#endif
#elif defined(USE_KCT8103L_PA)
enableFEMPower();
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); // RX bypass while MCU sleeps
#if defined(ARCH_ESP32)
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
#endif
@@ -227,4 +292,4 @@ int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower)
return loraOutputPower;
}
#endif
#endif
+83 -15
View File
@@ -13,9 +13,16 @@
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
#define PRESET(name) meshtastic_Config_LoRaConfig_ModemPreset_##name
// Override slot magic numbers for RegionInfo.overrideSlot
#define OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH 0 // Use hash of primary channel name
#define OVERRIDE_SLOT_PRESET_HASH -1 // Use hash of preset name instead
// Positive values (1-32767) are explicit slot numbers
// Region profile: bundles the preset list with regulatory parameters shared across regions
struct RegionProfile {
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated
float spacing; // gaps between radio channels
float padding; // padding at each side of the "operating channel"
bool audioPermitted;
@@ -23,15 +30,22 @@ struct RegionProfile {
int8_t textThrottle; // throttle for text - future expansion
int8_t positionThrottle; // throttle for location data - future expansion
int8_t telemetryThrottle; // throttle for telemetry - future expansion
uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place
};
/**
* Get the effective duty cycle for the current region based on device role.
* For EU_866, returns 10% for fixed devices (ROUTER, ROUTER_LATE) and 2.5% for mobile devices.
* For other regions, returns the standard duty cycle.
*/
extern float getEffectiveDutyCycle();
extern const RegionProfile PROFILE_STD;
extern const RegionProfile PROFILE_EU868;
extern const RegionProfile PROFILE_UNDEF;
// extern const RegionProfile PROFILE_LITE;
// extern const RegionProfile PROFILE_NARROW;
// extern const RegionProfile PROFILE_HAM;
extern const RegionProfile PROFILE_LITE;
extern const RegionProfile PROFILE_NARROW;
extern const RegionProfile PROFILE_HAM_20KHZ;
extern const RegionProfile PROFILE_HAM_100KHZ;
// Map from old region names to new region enums
struct RegionInfo {
@@ -43,10 +57,13 @@ struct RegionInfo {
bool freqSwitching;
bool wideLora;
const RegionProfile *profile;
const char *name; // EU433 etc
meshtastic_Config_LoRaConfig_ModemPreset defaultPreset;
int16_t overrideSlot; // per-region override slot for frequency selection
// Magic values: 0 = use channel name hash, -1 = use preset name hash, >0 = explicit slot
const char *name; // EU433 etc
// Preset accessors (delegate through profile)
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; }
meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return defaultPreset; }
const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; }
size_t getNumPresets() const
{
@@ -61,6 +78,7 @@ extern const RegionInfo regions[];
extern const RegionInfo *myRegion;
extern void initRegion();
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);
// Valid LoRa spread factor range and defaults
constexpr uint8_t LORA_SF_MIN = 5;
@@ -107,8 +125,18 @@ static inline float clampBandwidthKHz(float bwKHz)
static inline float bwCodeToKHz(uint16_t bwCode)
{
if (bwCode == 8)
return 7.8f;
if (bwCode == 10)
return 10.4f;
if (bwCode == 16)
return 15.6f;
if (bwCode == 21)
return 20.8f;
if (bwCode == 31)
return 31.25f;
if (bwCode == 42)
return 41.7f;
if (bwCode == 62)
return 62.5f;
if (bwCode == 200)
@@ -124,8 +152,18 @@ static inline float bwCodeToKHz(uint16_t bwCode)
static inline uint16_t bwKHzToCode(float bwKHz)
{
if (bwKHz > 7.7f && bwKHz < 7.9f)
return 8;
if (bwKHz > 10.3f && bwKHz < 10.5f)
return 10;
if (bwKHz > 15.5f && bwKHz < 15.7f)
return 16;
if (bwKHz > 20.7f && bwKHz < 20.9f)
return 21;
if (bwKHz > 31.24f && bwKHz < 31.26f)
return 31;
if (bwKHz > 41.6f && bwKHz < 41.8f)
return 42;
if (bwKHz > 62.49f && bwKHz < 62.51f)
return 62;
if (bwKHz > 203.12f && bwKHz < 203.13f)
@@ -143,46 +181,76 @@ static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset
uint8_t &cr)
{
switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
case PRESET(SHORT_TURBO):
bwKHz = wideLora ? 1625.0f : 500.0f;
cr = 5;
sf = 7;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST:
case PRESET(SHORT_FAST):
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 7;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW:
case PRESET(SHORT_SLOW):
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 8;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST:
case PRESET(MEDIUM_FAST):
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 9;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW:
case PRESET(MEDIUM_SLOW):
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
sf = 10;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
case PRESET(LONG_TURBO):
bwKHz = wideLora ? 1625.0f : 500.0f;
cr = 8;
sf = 11;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
case PRESET(LONG_MODERATE):
bwKHz = wideLora ? 406.25f : 125.0f;
cr = 8;
sf = 11;
break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW:
case PRESET(LONG_SLOW):
bwKHz = wideLora ? 406.25f : 125.0f;
cr = 8;
sf = 12;
break;
case PRESET(LITE_FAST):
bwKHz = 125;
cr = 5;
sf = 9;
break;
case PRESET(LITE_SLOW):
bwKHz = 125;
cr = 5;
sf = 10;
break;
case PRESET(NARROW_FAST):
bwKHz = 62.5f;
cr = 6;
sf = 7;
break;
case PRESET(NARROW_SLOW):
bwKHz = 62.5f;
cr = 6;
sf = 8;
break;
case PRESET(TINY_FAST):
bwKHz = 15.6f;
cr = 5;
sf = 7;
break;
case PRESET(TINY_SLOW):
bwKHz = 15.6f;
cr = 6;
sf = 8;
break;
default: // LONG_FAST (or illegal)
bwKHz = wideLora ? 812.5f : 250.0f;
cr = 5;
+20 -1
View File
@@ -25,6 +25,9 @@
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include "meshUtils.h"
#include "modules/NeighborInfoModule.h"
#if HAS_VARIABLE_HOPS
#include "modules/HopScalingModule.h"
#endif
#include "xmodem.h"
#include <ErriezCRC32.h>
#include <algorithm>
@@ -927,7 +930,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(ELECROW_ThinkNode_M9)) && \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
@@ -1986,6 +1989,14 @@ bool NodeDB::saveDeviceStateToDisk()
bool NodeDB::saveNodeDatabaseToDisk()
{
// Don't persist the node DB until this device has a PKI keypair
// TODO: revisit when https://github.com/meshtastic/firmware/pull/10478 lands
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
if (owner.public_key.size != 32 && !owner.is_licensed) {
LOG_DEBUG("Skip NodeDB without key");
return true;
}
#endif
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
// and all writes will fail anyway. Device should be sleeping at this point anyway.
@@ -2538,6 +2549,14 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
mp.via_mqtt); // Store if we received this packet via MQTT
#if HAS_VARIABLE_HOPS
// Only sample packets that arrived over LoRa.
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && hopScalingModule) {
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
hopScalingModule->samplePacketForHistogram(mp.from, hopCount);
}
#endif
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
const int8_t hopsAway = getHopsAway(mp);
if (hopsAway >= 0) {
+9 -7
View File
@@ -4,17 +4,19 @@
#include <Arduino.h>
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
{
const meshtastic_Channel &channel = channels.getByIndex(channelIndex);
if (channel.settings.has_module_settings) {
return channel.settings.module_settings.position_precision;
} else if (channel.role == meshtastic_Channel_Role_PRIMARY) {
return 32;
} else {
return 0;
}
// No module settings: fail closed. A PRIMARY channel used to default to 32
// here, leaking an exact position on a sharing-disabled channel. See #10509.
return 0;
}
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
{
return getPositionPrecisionForChannel(channels.getByIndex(channelIndex));
}
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
+2
View File
@@ -1,8 +1,10 @@
#pragma once
#include "meshtastic/channel.pb.h"
#include "meshtastic/mesh.pb.h"
#include <stdint.h>
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
+6
View File
@@ -341,4 +341,10 @@ bool RF95Interface::sleep()
return true;
}
int16_t RF95Interface::getCurrentRSSI()
{
float rssi = lora->getRSSI(false);
return (int16_t)round(rssi);
}
#endif
+2
View File
@@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface
*/
virtual void disableInterrupt() override;
int16_t getCurrentRSSI() override;
/**
* Enable a particular ISR callback glue function
*/
+159 -60
View File
@@ -35,31 +35,39 @@
#endif
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = {
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END};
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST), PRESET(SHORT_SLOW),
PRESET(SHORT_FAST), PRESET(LONG_MODERATE), PRESET(SHORT_TURBO), PRESET(LONG_TURBO), MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = {
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW,
meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END};
PRESET(LONG_FAST), PRESET(LONG_SLOW), PRESET(MEDIUM_SLOW), PRESET(MEDIUM_FAST),
PRESET(SHORT_SLOW), PRESET(SHORT_FAST), PRESET(LONG_MODERATE), MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST,
MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {PRESET(LONG_FAST), MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_LITE[] = {PRESET(LITE_FAST), PRESET(LITE_SLOW), MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_NARROW[] = {PRESET(NARROW_FAST), PRESET(NARROW_SLOW),
MODEM_PRESET_END};
static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_TINY[] = {PRESET(TINY_FAST), PRESET(TINY_SLOW), MODEM_PRESET_END};
// Region profiles: bundle preset list + regulatory parameters shared across regions
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0};
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0};
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0};
// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle
const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 1, 1};
const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 1, 1};
const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 1, 1};
const RegionProfile PROFILE_LITE = {PRESETS_LITE, 0.4, 0.0375f, false, false, 0, 10, 10};
const RegionProfile PROFILE_NARROW = {PRESETS_NARROW, 0, 0.0104f, true, false, 0, 1, 1};
// Ham '20kHz' profile. 15.6kHz bandwidth coerced to 20kHz via padding.
const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true, 0, 2, 2};
// Ham '100kHz' profile. 62.5kHz bandwidth coerced to 100kHz via padding.
const RegionProfile PROFILE_HAM_100KHZ = {PRESETS_NARROW, 0, 0.01875f, false, true, 0, 1, 1};
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \
#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \
override_slot) \
{ \
meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \
wide_lora, &profile_ptr, #name \
wide_lora, &profile_ptr, default_preset, override_slot, #name \
}
const RegionInfo regions[] = {
@@ -67,7 +75,7 @@ const RegionInfo regions[] = {
https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
*/
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
@@ -75,7 +83,7 @@ const RegionInfo regions[] = {
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
FIXME: https://github.com/meshtastic/firmware/issues/3371
*/
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD),
RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/
@@ -90,33 +98,33 @@ const RegionInfo regions[] = {
AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.)
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf
*/
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868),
RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868, PRESET(LONG_FAST), 0),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD),
RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf
https://qiita.com/ammo0613/items/d952154f1195b64dc29f
*/
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD),
RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
Also used in Brazil.
*/
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD),
RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions
AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence
NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100
*/
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD),
RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf
@@ -124,13 +132,13 @@ const RegionInfo regions[] = {
Note:
- We do LBT, so 100% is allowed.
*/
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD),
RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0
https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters
*/
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD),
RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor.
@@ -138,40 +146,40 @@ const RegionInfo regions[] = {
https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF
https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283
*/
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD),
RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
*/
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD),
RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752
https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf
*/
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD),
RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
Thailand 920925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
*/
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD),
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
433,05-434,7 Mhz 10 mW
868,0-868,6 Mhz 25 mW
https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf
*/
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD),
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD),
RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Malaysia
433 - 435 MHz at 100mW, no restrictions.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD),
RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Malaysia
@@ -180,14 +188,14 @@ const RegionInfo regions[] = {
Frequency hopping is used for 919 - 923 MHz.
https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf
*/
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD),
RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Singapore
SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions.
https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf
*/
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD),
RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Philippines
@@ -197,9 +205,9 @@ const RegionInfo regions[] = {
https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135
*/
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD),
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD),
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD),
RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Kazakhstan
@@ -207,32 +215,85 @@ const RegionInfo regions[] = {
863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields
https://github.com/meshtastic/firmware/issues/7204
*/
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD),
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Nepal
865MHz to 868MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use,
specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf
*/
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD),
RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
Brazil
902 - 907.5 MHz , 1W power limit, no duty cycle restrictions
https://github.com/meshtastic/firmware/issues/3741
*/
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD),
RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
Power limit is the regulatory ceiling (1 W / 30 dBm) individual hardware will cap below this
via its own PA curve; the field here is just the legal upper bound.
Default slot: 26 (144.510 MHz)
https://www.iaru-r1.org/wp-content/uploads/2020/12/VHF-Bandplan.pdf
*/
RDEF(ITU1_2M, 144.0f, 146.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 26),
/*
ITU Region 2 (Americas) amateur 2m allocation: 144.000 - 148.000 MHz.
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
Default slot: 51 (145.010 MHz)
https://www.arrl.org/band-plan
*/
RDEF(ITU2_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 51),
/*
ITU Region 3 (Asia/Pacific) amateur 2m allocation: 144.000 - 148.000 MHz.
Typical admin rules allow well above 30 dBm for licensed operators.
Default slot: 33 (144.650 MHz)
https://www.iaru.org/wp-content/uploads/2020/01/R3-004-IARU-Region-3-Bandplan-rev.2.pdf
https://www.wia.org.au/members/bandplans/data/documents/WIA%20Australian%20Band%20Plan%202026.pdf
*/
RDEF(ITU3_2M, 144.0f, 148.0f, 100, 30, false, false, PROFILE_HAM_20KHZ, PRESET(TINY_FAST), 33),
/*
ITU Region 2 (Americas) amateur 1.25m '125cm' allocation: 220.000 - 225.000 MHz.
Typical admin rules (e.g. US FCC Part 97) allow well above 30 dBm for licensed operators.
Note: Some countries do not allocate 220-222 MHz (e.g. USA, Canada). Check local law!
Default slot: 37 (223.650 MHz)
https://www.arrl.org/band-plan
*/
RDEF(ITU2_125CM, 220.0f, 225.0f, 100, 30, false, false, PROFILE_HAM_100KHZ, PRESET(NARROW_SLOW), 37),
/*
2.4 GHZ WLAN Band equivalent. Only for SX128x chips.
*/
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD),
RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD, PRESET(LONG_FAST), 0),
/*
EU 866MHz band (Band no. 46b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
Gives 4 channels at 865.7/866.3/866.9/867.5 MHz, 400 kHz gap plus 37.5 kHz padding between channels, 27 dBm,
duty cycle 2.5% (mobile) or 10% (fixed) https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02006D0771(01)-20250123
*/
RDEF(EU_866, 865.6f, 867.6f, 2.5, 27, false, false, PROFILE_LITE, PRESET(LITE_FAST), 0),
/*
EU 868MHz band: 3 channels at 869.410/869.4625/869.577 MHz
Channel centres at 869.442/869.525/869.608 MHz,
10.4 kHz padding on channels, 27 dBm, duty cycle 10%
*/
RDEF(EU_N_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_NARROW, PRESET(NARROW_SLOW), 1),
/*
This needs to be last. Same as US.
*/
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF)
RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF, PRESET(LONG_FAST), 0),
};
@@ -546,6 +607,23 @@ const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code)
return r;
}
/**
* Get duty cycle for current region. EU_866: 10% for routers, 2.5% for mobile.
*/
float getEffectiveDutyCycle()
{
if (myRegion->code == meshtastic_Config_LoRaConfig_RegionCode_EU_866) {
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
return 10.0f;
} else {
return 2.5f;
}
}
// For all other regions, return the standard duty cycle
return myRegion->dutyCycle;
}
uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received)
{
uint32_t pl = 0;
@@ -801,6 +879,16 @@ bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &lo
{
const RegionInfo *newRegion = getRegion(loraConfig.region);
// Reject unrecognized region codes (getRegion returns UNSET sentinel for unknown codes)
if (newRegion->code != loraConfig.region) {
char err_string[160];
snprintf(err_string, sizeof(err_string), "Region code %d is not recognized", loraConfig.region);
LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
sendErrorNotification(err_string);
return false;
}
// If you are not licensed, you can't use ham regions.
if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) {
char err_string[160];
@@ -897,12 +985,15 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
if (loraConfig.override_frequency == 0) {
// Check if we use the default frequency slot
// overrideSlot: 0 = channel hash, -1 = preset hash, >0 = explicit slot
uses_default_frequency_slot =
(loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default
(newRegion->profile->overrideSlot != 0 &&
loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override
((newRegion->profile->overrideSlot == 0) &&
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override
(newRegion->overrideSlot > 0 &&
loraConfig.channel_num == newRegion->overrideSlot) || // user setting matches explicit override slot
((newRegion->overrideSlot == OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH) &&
((uint32_t)(loraConfig.channel_num - 1) == channelNameHashSlot)) || // user setting matches channel name hash
((newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) &&
((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset name hash
// check if user setting different to preset name
uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0);
@@ -917,10 +1008,13 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo
if (clamp) {
if (uses_custom_channel_name) { // clamp to channel name hash
loraConfig.channel_num =
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
} else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot
loraConfig.channel_num =
newRegion->profile->overrideSlot; // use the override slot specified by the region profile
channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
} else if (newRegion->overrideSlot > 0) { // clamp to explicit override slot
loraConfig.channel_num = newRegion->overrideSlot; // use the explicit override slot defined for this region
uses_default_frequency_slot = true;
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH && loraConfig.use_preset) {
// clamp to preset name hash
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
uses_default_frequency_slot = true;
} else if (loraConfig.use_preset) { // clamp to preset slot
loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1
@@ -1018,6 +1112,8 @@ void RadioInterface::applyModemConfig()
// Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one.
// Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to
// (numFreqSlots - 1).
const char *channelName = channels.getName(channels.getPrimaryIndex());
uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots;
uint32_t presetNameHashSlot =
hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots;
@@ -1034,11 +1130,13 @@ void RadioInterface::applyModemConfig()
// (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1).
// NB: channel_num is also know as frequency slot but it's too late to fix now.
if (uses_default_frequency_slot) {
// if there's an override slot, use that
if (newRegion->profile->overrideSlot != 0) {
channel_num = newRegion->profile->overrideSlot - 1;
// Handle three override slot cases: explicit slot (>0), preset hash (-1), or channel hash (0)
if (newRegion->overrideSlot > 0) {
channel_num = newRegion->overrideSlot - 1; // explicit override slot (1-based to 0-based)
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
channel_num = presetNameHashSlot; // use preset name hash
} else {
channel_num = presetNameHashSlot;
channel_num = channelNameHashSlot; // use channel name hash (default case)
}
} else { // use the manually defined one
channel_num = loraConfig.channel_num - 1;
@@ -1051,7 +1149,6 @@ void RadioInterface::applyModemConfig()
saveChannelNum(channel_num);
saveFreq(freq + loraConfig.frequency_offset);
const char *channelName = channels.getName(channels.getPrimaryIndex());
if (newRegion->wideLora) { // clamp if wide freq range
preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz
@@ -1068,9 +1165,11 @@ void RadioInterface::applyModemConfig()
channel_num, power);
LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd,
newRegion->freqEnd - newRegion->freqStart);
LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw);
if (newRegion->profile->overrideSlot != 0) {
LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot);
LOG_INFO("numFreqSlots: %u x %.3fkHz", numFreqSlots, bw);
if (newRegion->overrideSlot > 0) {
LOG_INFO("Using region explicit override slot: %d", newRegion->overrideSlot);
} else if (newRegion->overrideSlot == OVERRIDE_SLOT_PRESET_HASH) {
LOG_INFO("Using region preset name hash for slot selection");
}
LOG_INFO("channel_num: %d", channel_num + 1);
LOG_INFO("frequency: %f", getFreq());
+6
View File
@@ -283,6 +283,12 @@ class RadioInterface
*/
virtual void saveChannelNum(uint32_t savedChannelNum);
/**
* Get current RSSI reading from the radio.
* Returns 0 if not available.
*/
virtual int16_t getCurrentRSSI() { return 0; }
private:
/**
* Convert our modemConfig enum into wf, sf, etc...
+90 -5
View File
@@ -15,6 +15,7 @@
#include "PortduinoGlue.h"
#include "meshUtils.h"
#endif
void LockingArduinoHal::spiBeginTransaction()
{
spiLock->lock();
@@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction()
spiLock->unlock();
}
#if ARCH_PORTDUINO
void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
{
@@ -40,6 +42,12 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
: NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface)
{
instance = this;
// Initialize unused sample slots to a sane default; sample count controls averaging.
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT;
}
#if defined(ARCH_STM32WL) && defined(USE_SX1262)
module.setCb_digitalWrite(stm32wl_emulate_digitalWrite);
module.setCb_digitalRead(stm32wl_emulate_digitalRead);
@@ -246,6 +254,87 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
return txQueue.find(from, id);
}
void RadioLibInterface::updateNoiseFloor()
{
// Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly.
if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) {
return;
}
uint32_t now = millis();
if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) {
return;
}
lastNoiseFloorUpdate = now;
int16_t rssi = getCurrentRSSI();
if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) {
LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi);
return;
}
noiseFloorSamples[currentSampleIndex] = (int32_t)rssi;
currentSampleIndex++;
if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) {
currentSampleIndex = 0;
isNoiseFloorBufferFull = true;
}
currentNoiseFloor = getAverageNoiseFloorInternal();
LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi);
}
uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const
{
return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex;
}
int32_t RadioLibInterface::getAverageNoiseFloorInternal() const
{
uint8_t sampleCount = getNoiseFloorSampleCountInternal();
if (sampleCount == 0) {
return NOISE_FLOOR_DEFAULT;
}
int32_t sum = 0;
for (uint8_t i = 0; i < sampleCount; i++) {
sum += noiseFloorSamples[i];
}
return sum / sampleCount;
}
int32_t RadioLibInterface::getAverageNoiseFloor()
{
return getAverageNoiseFloorInternal();
}
int32_t RadioLibInterface::getNoiseFloor()
{
return currentNoiseFloor;
}
bool RadioLibInterface::hasNoiseFloorSamples()
{
return getNoiseFloorSampleCountInternal() > 0;
}
uint8_t RadioLibInterface::getNoiseFloorSampleCount()
{
return getNoiseFloorSampleCountInternal();
}
void RadioLibInterface::resetNoiseFloor()
{
currentSampleIndex = 0;
isNoiseFloorBufferFull = false;
currentNoiseFloor = NOISE_FLOOR_DEFAULT;
LOG_INFO("Noise floor reset - rolling window collection will restart");
}
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
{
if (!buffer || length == 0 || !iface) {
@@ -273,6 +362,7 @@ currently active.
*/
void RadioLibInterface::onNotify(uint32_t notification)
{
switch (notification) {
case ISR_TX:
handleTransmitInterrupt();
@@ -404,11 +494,6 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
return false;
}
/**
* Remove a packet that is eligible for replacement from the TX queue
*/
// void RadioLibInterface::removePending
void RadioLibInterface::handleTransmitInterrupt()
{
// This can be null if we forced the device to enter standby mode. In that case
+56
View File
@@ -99,6 +99,26 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
bool isReceiving = false;
protected:
// Noise floor tracking - rolling window of samples.
static const uint8_t NOISE_FLOOR_SAMPLES = 20;
static const int32_t NOISE_FLOOR_DEFAULT = -120;
static const int32_t NOISE_FLOOR_VALID_MIN = -127;
static const int32_t NOISE_FLOOR_INVALID = -128;
int32_t noiseFloorSamples[NOISE_FLOOR_SAMPLES];
uint8_t currentSampleIndex = 0;
bool isNoiseFloorBufferFull = false;
uint32_t lastNoiseFloorUpdate = 0;
static const uint32_t NOISE_FLOOR_UPDATE_INTERVAL_MS = 5000;
int32_t currentNoiseFloor = NOISE_FLOOR_DEFAULT;
/**
* Pure virtual hook for derived radio interfaces to provide instantaneous RSSI.
* Implementations should return dBm, or an invalid value that updateNoiseFloor()
* can reject.
*/
virtual int16_t getCurrentRSSI() = 0;
public:
/** Our ISR code currently needs this to find our active instance
*/
@@ -111,6 +131,17 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
instance = nullptr;
}
/**
* Get the current calculated noise floor in dBm
* Returns -120 dBm if not yet calibrated
*/
int32_t getNoiseFloor();
/**
* Calculate the average noise floor from collected samples
*/
int32_t getAverageNoiseFloor();
/**
* Glue functions called from ISR land
*/
@@ -179,6 +210,28 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
/**
* Update the noise floor measurement by sampling RSSI from a slow path.
* This should not be called from radio interrupt or TX/RX critical paths.
*/
void updateNoiseFloor();
/**
* Check if we have collected any noise floor samples
*/
bool hasNoiseFloorSamples();
/**
* Get the number of samples in the rolling window
*/
uint8_t getNoiseFloorSampleCount();
/**
* Reset the noise floor calibration
* Will automatically restart collection
*/
void resetNoiseFloor();
/**
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
* @return true if len bytes were produced, false otherwise.
@@ -186,6 +239,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
bool randomBytes(uint8_t *buffer, size_t length);
private:
uint8_t getNoiseFloorSampleCountInternal() const;
int32_t getAverageNoiseFloorInternal() const;
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
* doing the transmit */
void setTransmitDelay();
+95 -40
View File
@@ -15,6 +15,9 @@
#if HAS_TRAFFIC_MANAGEMENT
#include "modules/TrafficManagementModule.h"
#endif
#if HAS_VARIABLE_HOPS
#include "modules/HopScalingModule.h"
#endif
#if !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#endif
@@ -22,8 +25,6 @@
#if ARCH_PORTDUINO
#include "Throttle.h"
#include "platform/portduino/PortduinoGlue.h"
#endif
#if ENABLE_JSON_LOGGING || ARCH_PORTDUINO
#include "serialization/MeshPacketSerializer.h"
#endif
@@ -318,10 +319,11 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
} // should have already been handled by sendLocal
// Abort sending if we are violating the duty cycle
if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) {
float effectiveDutyCycle = getEffectiveDutyCycle();
if (!config.lora.override_duty_cycle && effectiveDutyCycle < 100) {
float hourlyTxPercent = airTime->utilizationTXPercent();
if (hourlyTxPercent > myRegion->dutyCycle) {
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle);
if (hourlyTxPercent > effectiveDutyCycle) {
uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, effectiveDutyCycle);
LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes);
@@ -356,6 +358,30 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
p->from = getFrom(p);
p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // set the relayer to us
#if HAS_VARIABLE_HOPS
// Apply HopScaling hop recommendation to routine outgoing broadcasts
if (isFromUs(p) && isBroadcast(p->to) && hopScalingModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
switch (p->decoded.portnum) {
case meshtastic_PortNum_POSITION_APP:
case meshtastic_PortNum_TELEMETRY_APP:
case meshtastic_PortNum_NODEINFO_APP:
case meshtastic_PortNum_NEIGHBORINFO_APP: {
uint8_t variableHopLimit = hopScalingModule->getLastRequiredHop();
// Never exceed user-configured hop_limit
if (variableHopLimit < p->hop_limit) {
LOG_DEBUG("[HOPSCALE] hop_limit %u -> %u for portnum %u", p->hop_limit, variableHopLimit, p->decoded.portnum);
p->hop_limit = variableHopLimit;
}
break;
}
default:
break;
}
}
#endif
// If we are the original transmitter, set the hop limit with which we start
if (isFromUs(p))
p->hop_start = p->hop_limit;
@@ -368,10 +394,14 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
}
fixPriority(p); // Before encryption, fix the priority if it's unset
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
LOG_ERROR("Dropping malformed position packet before send");
packetPool.release(p);
return meshtastic_Routing_Error_BAD_REQUEST;
// Position precision is an originator-only privacy policy. Relays keep
// p->from as the original sender, so do not rewrite their POSITION_APP payload.
if (isFromUs(p)) {
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
LOG_ERROR("Dropping malformed position packet before send");
packetPool.release(p);
return meshtastic_Routing_Error_BAD_REQUEST;
}
}
// If the packet is not yet encrypted, do so now
@@ -404,6 +434,13 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
}
#endif
#ifdef ARCH_PORTDUINO
// Mirror every outbound packet to the meshswitch routing daemon, if a unicast peer is configured.
if (udpUnicastConnector) {
udpUnicastConnector->onSend(p);
}
#endif
assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside)
return iface->send(p);
}
@@ -434,6 +471,34 @@ void Router::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Rout
// FIXME, update nodedb here for any packet that passes through us
}
bool attemptAESDecrypt(meshtastic_MeshPacket *p, size_t rawSize)
{
memcpy(bytes, p->encrypted.bytes, rawSize);
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_DEBUG("Invalid portnum (bad psk?)");
#if !(MESHTASTIC_EXCLUDE_PKI)
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
LOG_WARN("Rejecting legacy DM");
return false;
#endif
} else {
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
return true;
}
return false;
}
DecodeState perhapsDecode(meshtastic_MeshPacket *p)
{
concurrency::LockGuard g(cryptLock);
@@ -493,30 +558,26 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
// Try to use this hash/channel pair
if (channels.decryptForHash(chIndex, p->channel)) {
// we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a
// fresh copy for each decrypt attempt.
memcpy(bytes, p->encrypted.bytes, rawSize);
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
decrypted = attemptAESDecrypt(p, rawSize);
if (decrypted) {
LOG_DEBUG("Packet decrypted using channel %d", chIndex);
break;
}
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_DEBUG("Invalid portnum (bad psk?)");
#if !(MESHTASTIC_EXCLUDE_PKI)
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
LOG_WARN("Rejecting legacy DM");
return DecodeState::DECODE_FAILURE;
#endif
} else {
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
decrypted = true;
// We have a sticky problem here in a specific case.
// If we're routing packets between presets over UDP
// The channel hash may be bogus.
// So we probably need to try to decrypt with every channel
// even if the hash doesn't match, to see if any of them work.
// If we find one that works, we can just re-write the hash
} else if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP) {
// On Portduino, we allow UDP packets to bypass the channel hash check, since the hash may not be
// correct for packets coming over UDP from the meshswitch daemon.
// This allows us to support routing between presets over UDP even if the channel hashes don't match.
channels.setCrypto(chIndex);
decrypted = attemptAESDecrypt(p, rawSize);
if (decrypted) {
LOG_DEBUG("Packet decrypted using channel %d with UDP bypass", chIndex);
break;
}
}
@@ -550,9 +611,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
} */
printPacket("decoded message", p);
#if ENABLE_JSON_LOGGING
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
#elif ARCH_PORTDUINO
#if ARCH_PORTDUINO
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
} else if (portduino_config.JSONFilename != "") {
@@ -847,11 +906,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
{
#if ENABLE_JSON_LOGGING
// Even ignored packets get logged in the trace
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
#elif ARCH_PORTDUINO
#if ARCH_PORTDUINO
// Even ignored packets get logged in the trace
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
+8 -2
View File
@@ -52,8 +52,8 @@ template <typename T> bool SX126xInterface<T>::init()
#ifdef SX126X_POWER_EN // Perhaps add RADIOLIB_NC check, and beforehand define as such if it is undefined, but it is not commonly
// used and not part of the 'default' set of pin definitions.
digitalWrite(SX126X_POWER_EN, HIGH);
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
#endif
#if HAS_LORA_FEM
@@ -65,8 +65,8 @@ template <typename T> bool SX126xInterface<T>::init()
#endif
#ifdef RF95_FAN_EN
digitalWrite(RF95_FAN_EN, HIGH);
pinMode(RF95_FAN_EN, OUTPUT);
digitalWrite(RF95_FAN_EN, HIGH);
#endif
#if ARCH_PORTDUINO
@@ -265,6 +265,12 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
return true;
}
template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false);
return (int16_t)round(rssi);
}
template <typename T> void SX126xInterface<T>::disableInterrupt()
{
lora.clearDio1Action();
+2
View File
@@ -41,6 +41,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
*/
T lora;
int16_t getCurrentRSSI() override;
/**
* Glue functions called from ISR land
*/
+6
View File
@@ -325,4 +325,10 @@ template <typename T> bool SX128xInterface<T>::sleep()
return true;
}
template <typename T> int16_t SX128xInterface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false);
return (int16_t)round(rssi);
}
#endif
+2
View File
@@ -35,6 +35,8 @@ template <class T> class SX128xInterface : public RadioLibInterface
*/
T lora;
int16_t getCurrentRSSI() override;
/**
* Glue functions called from ISR land
*/
+4
View File
@@ -2,7 +2,11 @@
#include "ServerAPI.h"
#if !defined(USE_WS5500) && !defined(USE_CH390D)
#if defined(USE_ARDUINO_ETHERNET)
#include <Ethernet.h>
#else
#include <RAK13800_W5100S.h>
#endif
/**
* Provides both debug printing and, if the client starts sending protobufs to us, switches to send/receive protobufs
File diff suppressed because it is too large Load Diff
-347
View File
@@ -1,347 +0,0 @@
/*
* Copyright (C) 2020 Siara Logics (cc)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Arundale Ramanathan
*
* Port for Particle (particle.io) / Aruino - Jonathan Greenblatt
*
* This file describes each function of the Unishox2 API \n
* For finding out how this API can be used in your program, \n
* please see test_unishox2.c.
*/
#ifndef unishox2
#define unishox2
#define UNISHOX_VERSION "2.0" ///< Unicode spec version
/**
* Macro switch to enable/disable output buffer length parameter in low level api \n
* Disabled by default \n
* When this macro is defined, the all the API functions \n
* except the simple API functions accept an additional parameter olen \n
* that enables the developer to pass the size of the output buffer provided \n
* so that the api function may not write beyond that length. \n
* This can be disabled if the developer knows that the buffer provided is sufficient enough \n
* so no additional parameter is passed and the program is faster since additional check \n
* for output length is not performed at each step \n
* The simple api, i.e. unishox2_(de)compress_simple will always omit the buffer length
*/
#ifndef UNISHOX_API_WITH_OUTPUT_LEN
#define UNISHOX_API_WITH_OUTPUT_LEN 1
#endif
/// Upto 8 bits of initial magic bit sequence can be included. Bit count can be specified with UNISHOX_MAGIC_BIT_LEN
#ifndef UNISHOX_MAGIC_BITS
#define UNISHOX_MAGIC_BITS 0xFF
#endif
/// Desired length of Magic bits defined by UNISHOX_MAGIC_BITS
#ifdef UNISHOX_MAGIC_BIT_LEN
#if UNISHOX_MAGIC_BIT_LEN < 0 || 9 <= UNISHOX_MAGIC_BIT_LEN
#error "UNISHOX_MAGIC_BIT_LEN need between [0, 8)"
#endif
#else
#define UNISHOX_MAGIC_BIT_LEN 1
#endif
// enum {USX_ALPHA = 0, USX_SYM, USX_NUM, USX_DICT, USX_DELTA};
/// Default Horizontal codes. When composition of text is know beforehand, the other hcodes in this section can be used to achieve
/// more compression.
#define USX_HCODES_DFLT \
(const unsigned char[]) \
{ \
0x00, 0x40, 0x80, 0xC0, 0xE0 \
}
/// Length of each default hcode
#define USX_HCODE_LENS_DFLT \
(const unsigned char[]) \
{ \
2, 2, 2, 3, 3 \
}
/// Horizontal codes preset for English Alphabet content only
#define USX_HCODES_ALPHA_ONLY \
(const unsigned char[]) \
{ \
0x00, 0x00, 0x00, 0x00, 0x00 \
}
/// Length of each Alpha only hcode
#define USX_HCODE_LENS_ALPHA_ONLY \
(const unsigned char[]) \
{ \
0, 0, 0, 0, 0 \
}
/// Horizontal codes preset for Alpha Numeric content only
#define USX_HCODES_ALPHA_NUM_ONLY \
(const unsigned char[]) \
{ \
0x00, 0x00, 0x80, 0x00, 0x00 \
}
/// Length of each Alpha numeric hcode
#define USX_HCODE_LENS_ALPHA_NUM_ONLY \
(const unsigned char[]) \
{ \
1, 0, 1, 0, 0 \
}
/// Horizontal codes preset for Alpha Numeric and Symbol content only
#define USX_HCODES_ALPHA_NUM_SYM_ONLY \
(const unsigned char[]) \
{ \
0x00, 0x80, 0xC0, 0x00, 0x00 \
}
/// Length of each Alpha numeric and symbol hcodes
#define USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY \
(const unsigned char[]) \
{ \
1, 2, 2, 0, 0 \
}
/// Horizontal codes preset favouring Alphabet content
#define USX_HCODES_FAVOR_ALPHA \
(const unsigned char[]) \
{ \
0x00, 0x80, 0xA0, 0xC0, 0xE0 \
}
/// Length of each hcode favouring Alpha content
#define USX_HCODE_LENS_FAVOR_ALPHA \
(const unsigned char[]) \
{ \
1, 3, 3, 3, 3 \
}
/// Horizontal codes preset favouring repeating sequences
#define USX_HCODES_FAVOR_DICT \
(const unsigned char[]) \
{ \
0x00, 0x40, 0xC0, 0x80, 0xE0 \
}
/// Length of each hcode favouring repeating sequences
#define USX_HCODE_LENS_FAVOR_DICT \
(const unsigned char[]) \
{ \
2, 2, 3, 2, 3 \
}
/// Horizontal codes preset favouring symbols
#define USX_HCODES_FAVOR_SYM \
(const unsigned char[]) \
{ \
0x80, 0x00, 0xA0, 0xC0, 0xE0 \
}
/// Length of each hcode favouring symbols
#define USX_HCODE_LENS_FAVOR_SYM \
(const unsigned char[]) \
{ \
3, 1, 3, 3, 3 \
}
// #define USX_HCODES_FAVOR_UMLAUT {0x00, 0x40, 0xE0, 0xC0, 0x80}
// #define USX_HCODE_LENS_FAVOR_UMLAUT {2, 2, 3, 3, 2}
/// Horizontal codes preset favouring umlaut letters
#define USX_HCODES_FAVOR_UMLAUT \
(const unsigned char[]) \
{ \
0x80, 0xA0, 0xC0, 0xE0, 0x00 \
}
/// Length of each hcode favouring umlaut letters
#define USX_HCODE_LENS_FAVOR_UMLAUT \
(const unsigned char[]) \
{ \
3, 3, 3, 3, 1 \
}
/// Horizontal codes preset for no repeating sequences
#define USX_HCODES_NO_DICT \
(const unsigned char[]) \
{ \
0x00, 0x40, 0x80, 0x00, 0xC0 \
}
/// Length of each hcode for no repeating sequences
#define USX_HCODE_LENS_NO_DICT \
(const unsigned char[]) \
{ \
2, 2, 2, 0, 2 \
}
/// Horizontal codes preset for no Unicode characters
#define USX_HCODES_NO_UNI \
(const unsigned char[]) \
{ \
0x00, 0x40, 0x80, 0xC0, 0x00 \
}
/// Length of each hcode for no Unicode characters
#define USX_HCODE_LENS_NO_UNI \
(const unsigned char[]) \
{ \
2, 2, 2, 2, 0 \
}
extern const char *USX_FREQ_SEQ_DFLT[];
extern const char *USX_FREQ_SEQ_TXT[];
extern const char *USX_FREQ_SEQ_URL[];
extern const char *USX_FREQ_SEQ_JSON[];
extern const char *USX_FREQ_SEQ_HTML[];
extern const char *USX_FREQ_SEQ_XML[];
extern const char *USX_TEMPLATES[];
/// Default preset parameter set. When composition of text is know beforehand, the other parameter sets in this section can be
/// used to achieve more compression.
#define USX_PSET_DFLT USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set for English Alphabet only content
#define USX_PSET_ALPHA_ONLY USX_HCODES_ALPHA_ONLY, USX_HCODE_LENS_ALPHA_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES
/// Preset parameter set for Alpha numeric content
#define USX_PSET_ALPHA_NUM_ONLY USX_HCODES_ALPHA_NUM_ONLY, USX_HCODE_LENS_ALPHA_NUM_ONLY, USX_FREQ_SEQ_TXT, USX_TEMPLATES
/// Preset parameter set for Alpha numeric and symbol content
#define USX_PSET_ALPHA_NUM_SYM_ONLY \
USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set for Alpha numeric symbol content having predominantly text
#define USX_PSET_ALPHA_NUM_SYM_ONLY_TXT \
USX_HCODES_ALPHA_NUM_SYM_ONLY, USX_HCODE_LENS_ALPHA_NUM_SYM_ONLY, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set favouring Alphabet content
#define USX_PSET_FAVOR_ALPHA USX_HCODES_FAVOR_ALPHA, USX_HCODE_LENS_FAVOR_ALPHA, USX_FREQ_SEQ_TXT, USX_TEMPLATES
/// Preset parameter set favouring repeating sequences
#define USX_PSET_FAVOR_DICT USX_HCODES_FAVOR_DICT, USX_HCODE_LENS_FAVOR_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set favouring symbols
#define USX_PSET_FAVOR_SYM USX_HCODES_FAVOR_SYM, USX_HCODE_LENS_FAVOR_SYM, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set favouring unlaut letters
#define USX_PSET_FAVOR_UMLAUT USX_HCODES_FAVOR_UMLAUT, USX_HCODE_LENS_FAVOR_UMLAUT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set for when there are no repeating sequences
#define USX_PSET_NO_DICT USX_HCODES_NO_DICT, USX_HCODE_LENS_NO_DICT, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set for when there are no unicode symbols
#define USX_PSET_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_DFLT, USX_TEMPLATES
/// Preset parameter set for when there are no unicode symbols favouring text
#define USX_PSET_NO_UNI_FAVOR_TEXT USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_TXT, USX_TEMPLATES
/// Preset parameter set favouring URL content
#define USX_PSET_URL USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_URL, USX_TEMPLATES
/// Preset parameter set favouring JSON content
#define USX_PSET_JSON USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_JSON, USX_TEMPLATES
/// Preset parameter set favouring JSON content having no Unicode symbols
#define USX_PSET_JSON_NO_UNI USX_HCODES_NO_UNI, USX_HCODE_LENS_NO_UNI, USX_FREQ_SEQ_JSON, USX_TEMPLATES
/// Preset parameter set favouring XML content
#define USX_PSET_XML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_XML, USX_TEMPLATES
/// Preset parameter set favouring HTML content
#define USX_PSET_HTML USX_HCODES_DFLT, USX_HCODE_LENS_DFLT, USX_FREQ_SEQ_HTML, USX_TEMPLATES
/**
* This structure is used when a string array needs to be compressed.
* This is passed as a parameter to the unishox2_decompress_lines() function
*/
struct us_lnk_lst {
char *data;
struct us_lnk_lst *previous;
};
/**
* This macro is for internal use, but builds upon the macro UNISHOX_API_WITH_OUTPUT_LEN
* When the macro UNISHOX_API_WITH_OUTPUT_LEN is defined, the all the API functions
* except the simple API functions accept an additional parameter olen
* that enables the developer to pass the size of the output buffer provided
* so that the api function may not write beyond that length.
* This can be disabled if the developer knows that the buffer provided is sufficient enough
* so no additional parameter is passed and the program is faster since additional check
* for output length is not performed at each step
*/
#if defined(UNISHOX_API_WITH_OUTPUT_LEN) && UNISHOX_API_WITH_OUTPUT_LEN != 0
#define UNISHOX_API_OUT_AND_LEN(out, olen) out, olen
#else
#define UNISHOX_API_OUT_AND_LEN(out, olen) out
#endif
/**
* Simple API for compressing a string
* @param[in] in Input ASCII / UTF-8 string
* @param[in] len length in bytes
* @param[out] out output buffer - should be large enough to hold compressed output
*/
extern int unishox2_compress_simple(const char *in, int len, char *out);
/**
* Simple API for decompressing a string
* @param[in] in Input compressed bytes (output of unishox2_compress functions)
* @param[in] len length of 'in' in bytes
* @param[out] out output buffer for ASCII / UTF-8 string - should be large enough
*/
extern int unishox2_decompress_simple(const char *in, int len, char *out);
/**
* Comprehensive API for compressing a string
*
* Presets are available for the last four parameters so they can be passed as single parameter. \n
* See USX_PSET_* macros. Example call: \n
* unishox2_compress(in, len, out, olen, USX_PSET_ALPHA_ONLY);
*
* @param[in] in Input ASCII / UTF-8 string
* @param[in] len length in bytes
* @param[out] out output buffer - should be large enough to hold compressed output
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
*/
extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
const char *usx_templates[]);
/**
* Comprehensive API for de-compressing a string
*
* Presets are available for the last four parameters so they can be passed as single parameter. \n
* See USX_PSET_* macros. Example call: \n
* unishox2_decompress(in, len, out, olen, USX_PSET_ALPHA_ONLY);
*
* @param[in] in Input compressed bytes (output of unishox2_compress functions)
* @param[in] len length of 'in' in bytes
* @param[out] out output buffer - should be large enough to hold de-compressed output
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
*/
extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
const char *usx_templates[]);
/**
* More Comprehensive API for compressing array of strings
*
* See unishox2_compress() function for parameter definitions. \n
* This function takes an additional parameter, i.e. 'prev_lines' - the usx_lnk_lst structure \n
* See -g parameter in test_unishox2.c to find out how this can be used. \n
* This function is used when an array of strings need to be compressed \n
* and stored in a compressed array of bytes for use as a constant in other programs \n
* where each element of the array can be decompressed and used at runtime.
*/
extern int unishox2_compress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[],
const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines);
/**
* More Comprehensive API for de-compressing array of strings \n
* This function is not be used in conjuction with unishox2_compress_lines()
*
* See unishox2_decompress() function for parameter definitions. \n
* Typically an array is compressed using unishox2_compress_lines() and \n
* a header (.h) file is generated using the resultant compressed array. \n
* This header file can be used in another program with another decompress \n
* routine which takes this compressed array as parameter and index to be \n
* decompressed.
*/
extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[],
const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines);
#endif
+30
View File
@@ -6,7 +6,13 @@
#include "main.h"
#include "mesh/api/ethServerAPI.h"
#include "target_specific.h"
#ifdef USE_ARDUINO_ETHERNET
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5100/W5200/W5500
// Shorter DHCP timeout so LoRa startup isn't blocked when no DHCP server is present.
#define ETH_DHCP_TIMEOUT_MS 10000
#else
#include <RAK13800_W5100S.h>
#endif
#include <SPI.h>
#if HAS_NETWORKING && !defined(USE_WS5500) && !defined(USE_CH390D)
@@ -69,6 +75,13 @@ static int32_t reconnectETH()
delay(100);
#endif
#ifdef USE_ARDUINO_ETHERNET // Re-configure SPI0 for the W5500 module
SPI.setRX(ETH_SPI0_MISO);
SPI.setSCK(ETH_SPI0_SCK);
SPI.setTX(ETH_SPI0_MOSI);
SPI.begin();
Ethernet.init(PIN_ETHERNET_SS);
#else
#ifdef RAK11310
ETH_SPI_PORT.setSCK(PIN_SPI0_SCK);
ETH_SPI_PORT.setTX(PIN_SPI0_MOSI);
@@ -76,10 +89,15 @@ static int32_t reconnectETH()
ETH_SPI_PORT.begin();
#endif
Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS);
#endif
int status = 0;
if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) {
#ifdef ETH_DHCP_TIMEOUT_MS
status = Ethernet.begin(expectedMac, ETH_DHCP_TIMEOUT_MS);
#else
status = Ethernet.begin(expectedMac);
#endif
} else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) {
Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns,
config.network.ipv4_config.gateway, config.network.ipv4_config.subnet);
@@ -182,6 +200,13 @@ bool initEthernet()
digitalWrite(PIN_ETHERNET_RESET, HIGH); // Reset Time.
#endif
#ifdef USE_ARDUINO_ETHERNET // Configure SPI0 for the W5500 module
SPI.setRX(ETH_SPI0_MISO);
SPI.setSCK(ETH_SPI0_SCK);
SPI.setTX(ETH_SPI0_MOSI);
SPI.begin();
Ethernet.init(PIN_ETHERNET_SS);
#else
#ifdef RAK11310 // Initialize the SPI port
ETH_SPI_PORT.setSCK(PIN_SPI0_SCK);
ETH_SPI_PORT.setTX(PIN_SPI0_MOSI);
@@ -189,6 +214,7 @@ bool initEthernet()
ETH_SPI_PORT.begin();
#endif
Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS);
#endif
uint8_t mac[6];
@@ -201,7 +227,11 @@ bool initEthernet()
if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) {
LOG_INFO("Start Ethernet DHCP");
#ifdef ETH_DHCP_TIMEOUT_MS
status = Ethernet.begin(mac, ETH_DHCP_TIMEOUT_MS);
#else
status = Ethernet.begin(mac);
#endif
} else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) {
LOG_INFO("Start Ethernet Static");
Ethernet.begin(mac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, config.network.ipv4_config.gateway,
+28 -4
View File
@@ -198,6 +198,28 @@ typedef struct _meshtastic_LockdownAuth {
way to reset the session clock is a reboot, which costs a boot
from the on-flash, HMAC-bound counter. */
uint32_t max_session_seconds;
/* Disable lockdown mode. Requires a valid passphrase in the same
message (the device must prove the operator owns it before
reverting at-rest encryption). On success the firmware decrypts
every stored config / channel / nodedb file back to plaintext,
removes the wrapped DEK, unlock token, monotonic-counter, and
backoff files, and reboots out of lockdown.
This is the inverse of the provision/unlock path: it is how the
client app's "lockdown mode" toggle returns a device to normal
operation.
NOT reversed by this operation: APPROTECT. Once the debug port
lockout has been burned (on silicon where it is effective) it is
permanent disabling lockdown decrypts your data and removes the
access gates, but the SWD/JTAG port stays locked for the life of
the device (recoverable only via a full chip erase over a debug
probe, which destroys all data). Clients should make this
irreversibility clear at the moment lockdown is first enabled.
When true the passphrase field is still required; boots_remaining,
valid_until_epoch, max_session_seconds, and lock_now are ignored. */
bool disable;
} meshtastic_LockdownAuth;
/* Parameters for setting up Meshtastic for ameteur radio usage */
@@ -521,7 +543,7 @@ extern "C" {
#define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_default {_meshtastic_OTAMode_MIN, {0, {0}}}
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0}
#define meshtastic_LockdownAuth_init_default {{0, {0}}, 0, 0, 0, 0, 0}
#define meshtastic_HamParameters_init_default {"", 0, 0, ""}
#define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}}
#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0}
@@ -534,7 +556,7 @@ extern "C" {
#define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}}
#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0}
#define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}}
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0}
#define meshtastic_LockdownAuth_init_zero {{0, {0}}, 0, 0, 0, 0, 0}
#define meshtastic_HamParameters_init_zero {"", 0, 0, ""}
#define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}}
#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0}
@@ -557,6 +579,7 @@ extern "C" {
#define meshtastic_LockdownAuth_valid_until_epoch_tag 3
#define meshtastic_LockdownAuth_lock_now_tag 4
#define meshtastic_LockdownAuth_max_session_seconds_tag 5
#define meshtastic_LockdownAuth_disable_tag 6
#define meshtastic_HamParameters_call_sign_tag 1
#define meshtastic_HamParameters_tx_power_tag 2
#define meshtastic_HamParameters_frequency_tag 3
@@ -754,7 +777,8 @@ X(a, STATIC, SINGULAR, BYTES, passphrase, 1) \
X(a, STATIC, SINGULAR, UINT32, boots_remaining, 2) \
X(a, STATIC, SINGULAR, UINT32, valid_until_epoch, 3) \
X(a, STATIC, SINGULAR, BOOL, lock_now, 4) \
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5)
X(a, STATIC, SINGULAR, UINT32, max_session_seconds, 5) \
X(a, STATIC, SINGULAR, BOOL, disable, 6)
#define meshtastic_LockdownAuth_CALLBACK NULL
#define meshtastic_LockdownAuth_DEFAULT NULL
@@ -869,7 +893,7 @@ extern const pb_msgdesc_t meshtastic_SHTXX_config_msg;
#define meshtastic_AdminMessage_size 511
#define meshtastic_HamParameters_size 31
#define meshtastic_KeyVerificationAdmin_size 25
#define meshtastic_LockdownAuth_size 54
#define meshtastic_LockdownAuth_size 56
#define meshtastic_NodeRemoteHardwarePinsResponse_size 496
#define meshtastic_SCD30_config_size 27
#define meshtastic_SCD4X_config_size 29
+10 -1
View File
@@ -30,7 +30,7 @@ PB_BIND(meshtastic_AircraftTrack, meshtastic_AircraftTrack, AUTO)
PB_BIND(meshtastic_CotGeoPoint, meshtastic_CotGeoPoint, AUTO)
PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, 2)
PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, AUTO)
PB_BIND(meshtastic_Marker, meshtastic_Marker, AUTO)
@@ -63,6 +63,15 @@ PB_BIND(meshtastic_TAKEnvironment, meshtastic_TAKEnvironment, AUTO)
PB_BIND(meshtastic_SensorFov, meshtastic_SensorFov, AUTO)
PB_BIND(meshtastic_TakTalkMessage, meshtastic_TakTalkMessage, AUTO)
PB_BIND(meshtastic_TakTalkRoomData, meshtastic_TakTalkRoomData, AUTO)
PB_BIND(meshtastic_Marti, meshtastic_Marti, AUTO)
PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2)
+214 -31
View File
@@ -325,7 +325,15 @@ typedef enum _meshtastic_CotType {
meshtastic_CotType_CotType_b_a_o_c = 123,
/* t-s: Task / engage request. Structured payload carried via the new
TaskRequest typed variant. */
meshtastic_CotType_CotType_t_s = 124
meshtastic_CotType_CotType_t_s = 124,
/* m-t-t: TAKTALK voice/text chat message. Payload carried via the
TakTalkMessage typed variant (text, chatroom_id, lang, from_voice). */
meshtastic_CotType_CotType_m_t_t = 125,
/* y-: TAKTALK room/membership broadcast. Payload carried via the
TakTalkRoomData typed variant (sender_callsign, room_id, room_name,
participants). The CoT type literally has a trailing dash and no
second atom not a typo. */
meshtastic_CotType_CotType_y = 126
} meshtastic_CotType;
/* Geopoint and altitude source */
@@ -553,6 +561,18 @@ typedef struct _meshtastic_GeoChat {
/* Receipt kind discriminator. See ReceiptType doc. Default ReceiptType_None
means this is a regular chat message, not a receipt. */
meshtastic_GeoChat_ReceiptType receipt_type;
/* BCP-47-ish language tag or human-readable name (e.g. "en", "English")
that the originator's TAKTALK plugin recorded for the message. */
pb_callback_t lang;
/* TAKTALK chatroom UUID (e.g. "30b2755c-c547-44ef-a0cc-cdbd8a15616f") that
the receiver's TAKTALK plugin uses to thread the message under the
right room. Resolved to a friendly name via TakTalkRoomData broadcasts. */
pb_callback_t room_id;
/* TAKTALK voice profile pointer. Often empty in practice (the empty
marker `<voice_profile_id/>` still signals TAKTALK origination), so
receivers should treat empty-but-present as the equivalent of the
marker rather than a missing field. */
pb_callback_t voice_profile_id;
} meshtastic_GeoChat;
/* ATAK Group
@@ -715,12 +735,7 @@ typedef struct _meshtastic_DrawnShape {
uint32_t fill_argb;
/* Whether labels are rendered on this shape. */
bool labels_on;
/* Vertex list for polyline/polygon/rectangle shapes. Capped at 32 by
the nanopb pool; senders MUST truncate longer inputs and set
`truncated = true`. */
pb_size_t vertices_count;
meshtastic_CotGeoPoint vertices[32];
/* True if the sender truncated `vertices` to fit the pool. */
/* True if the sender truncated the vertex columns to fit the pool. */
bool truncated; /* --- Bullseye-only fields. All ignored unless kind == Kind_Bullseye. --- */
/* Bullseye distance in meters * 10 (e.g. 3285 = 328.5 m). 0 = unset. */
uint32_t bullseye_distance_dm;
@@ -734,6 +749,8 @@ typedef struct _meshtastic_DrawnShape {
uint8_t bullseye_flags;
/* Bullseye reference UID (anchor marker). Empty = anchor is self. */
char bullseye_uid_ref[48];
pb_callback_t vertex_lat_deltas;
pb_callback_t vertex_lon_deltas;
} meshtastic_DrawnShape;
/* Fixed point of interest: spot marker, waypoint, checkpoint, 2525 symbol,
@@ -1068,6 +1085,87 @@ typedef struct _meshtastic_SensorFov {
pb_callback_t model;
} meshtastic_SensorFov;
/* TAKTALK chat message payload (CoT type m-t-t).
TAKTALK is an ATAK plugin for voice + text team messaging. The voice
audio stream goes over UDP/RTP and is NOT carried by the mesh only
the text envelope (this message) is. `from_voice` marks messages sent
via push-to-talk speech-to-text so receivers can render a mic icon
next to the text.
Wire shape inside <event type="m-t-t">/<detail>:
<callsign>...</callsign> - mapped to TAKPacketV2.callsign
<lang>English</lang> - lang
<text>...</text> - text
<chatroom-id>1</chatroom-id> - chatroom_id
<voice/> - presence sets from_voice = true */
typedef struct _meshtastic_TakTalkMessage {
/* The text body of the TAKTALK message (speech-to-text transcript when
from_voice = true, typed message otherwise). */
pb_callback_t text;
/* TAKTALK chatroom identifier. May be a short id like "1" for the
default room or a UUID like "30b2755c-c547-44ef-a0cc-cdbd8a15616f"
for custom rooms (resolved by TakTalkRoomData broadcasts).
Empty = broadcast room. */
pb_callback_t chatroom_id;
/* BCP-47-ish language tag or human-readable name (e.g. "en", "English").
Empty = unspecified. */
pb_callback_t lang;
/* True when the source CoT carried a <voice/> marker, i.e. the message
originated as push-to-talk speech-to-text. Lets receivers show a mic
icon. Proto3 only encodes when true so empty payload cost is 0 bytes. */
bool from_voice;
} meshtastic_TakTalkMessage;
/* TAKTALK room/membership broadcast (CoT type y-).
Announces a TAKTALK chatroom's friendly name and roster so peers can
resolve room UUIDs (used in TakTalkMessage.chatroom_id and
GeoChat.room_id) to a display name and participant list. Not a chat
message itself these events are emitted by TAKTALK when rooms are
created or memberships change. */
typedef struct _meshtastic_TakTalkRoomData {
/* Callsign of the device broadcasting the room state (typically the
room owner / latest writer).
DEPRECATED in v0.3.2: always equals TAKPacketV2.callsign, so the wire
byte was redundant. Builders stop emitting this field in v0.3.2;
parsers still read it for one release so v0.3.1-encoded packets decode
cleanly. To be removed entirely in v0.4.x. */
pb_callback_t sender_callsign;
/* Room UUID, matches TakTalkMessage.chatroom_id / GeoChat.room_id on
messages routed into this room. */
pb_callback_t room_id;
/* Friendly display name for the room (e.g. "test", "Alpha Team"). */
pb_callback_t room_name;
/* Member callsigns. Wire-encoded as repeated strings; the underlying
CoT carries them as a single <chatroom-participants>A,B,C</> element
which parsers split / builders join on ','. */
pb_callback_t participants;
} meshtastic_TakTalkRoomData;
/* ATAK directed-routing recipient list (CoT <marti><dest callsign='X'/>…</marti>).
Present when an event is addressed to specific TAK users rather than the
broadcast group. TAKTALK gates voice TTS on this element matching the
receiver's callsign; directed b-t-f chats use it for the same purpose. A
missing <marti> means "broadcast to all peers", which is the default for
PLI, alerts, drawings, and most situational-awareness events.
Carried as repeated strings (not indexes into a per-packet table) because
the typical event has 1-2 destinations and table overhead would erase the
savings. Receivers that need the original XML element rebuild it from
dest_callsign on emit. */
typedef struct _meshtastic_Marti {
/* Recipient callsigns. Order is preserved end-to-end so receivers can show
primary-vs-cc distinction the same way ATAK does.
If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but
legal e.g. ATAK echoing back to its own room), the builder still emits
the element so loopback shapes round-trip cleanly. */
pb_callback_t dest_callsign;
} meshtastic_Marti;
typedef PB_BYTES_ARRAY_T(220) meshtastic_TAKPacketV2_raw_detail_t;
/* ATAK v2 packet with expanded CoT field support and zstd dictionary compression.
Sent on ATAK_PLUGIN_V2 port. The wire payload is:
@@ -1089,7 +1187,14 @@ typedef struct _meshtastic_TAKPacketV2 {
int32_t latitude_i;
/* Longitude, multiply by 1e-7 to get degrees in floating point */
int32_t longitude_i;
/* Altitude in meters (HAE) */
/* Altitude in meters (HAE). ATAK's "no altitude" sentinel is hae=9999999.0.
NOTE: an earlier v0.4.0 attempt made this `optional` to omit the 9999999
sentinel from the wire, but measurement showed it was net-negative: the
zstd dictionary already compresses the literal 9999999 to ~nothing, while
proto3 `optional` forces a genuine 0 m HAE (common on routes/drawings that
carry hae="0.0" or omit hae parsed as 0) to encode explicitly (+2 bytes),
which REGRESSED the worst-case route fixture. Kept as a plain field. */
int32_t altitude;
/* Speed in cm/s */
uint32_t speed;
@@ -1135,10 +1240,18 @@ typedef struct _meshtastic_TAKPacketV2 {
/* Sensor field-of-view cone (camera, FLIR, laser, etc.). From <sensor>. */
bool has_sensor_fov;
meshtastic_SensorFov sensor_fov;
/* Directed-routing recipient list (CoT <marti><dest callsign='X'/>…</marti>).
Empty / unset = broadcast to all peers (the default for situational-awareness
events). Populated for TAKTALK m-t-t, directed b-t-f DMs, and any other CoT
shape that ATAK addresses to specific recipients. TAKTALK gates voice TTS
playback on this element matching the receiver's callsign, so dropping it
silently breaks voice messaging end-to-end.
See Marti. */
bool has_marti;
meshtastic_Marti marti;
pb_size_t which_payload_variant;
union {
/* Position report (true = PLI, no extra fields beyond the common ones above) */
bool pli;
/* ATAK GeoChat message */
meshtastic_GeoChat chat;
/* Aircraft track data (ADS-B, military air) */
@@ -1163,6 +1276,14 @@ typedef struct _meshtastic_TAKPacketV2 {
meshtastic_EmergencyAlert emergency;
/* Task / engage request. See TaskRequest. */
meshtastic_TaskRequest task;
/* TAKTALK chat message (CoT type m-t-t). See TakTalkMessage.
Voice audio itself rides UDP/RTP outside the mesh; this carries the
text envelope plus a from_voice marker for receiver UX. */
meshtastic_TakTalkMessage taktalk;
/* TAKTALK room/membership broadcast (CoT type y-). See TakTalkRoomData.
Resolves room UUIDs (used in TakTalkMessage.chatroom_id and
GeoChat.room_id) to display name + roster on receivers. */
meshtastic_TakTalkRoomData taktalk_room;
} payload_variant;
} meshtastic_TAKPacketV2;
@@ -1185,8 +1306,8 @@ extern "C" {
#define _meshtastic_CotHow_ARRAYSIZE ((meshtastic_CotHow)(meshtastic_CotHow_CotHow_m_s+1))
#define _meshtastic_CotType_MIN meshtastic_CotType_CotType_Other
#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_t_s
#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_t_s+1))
#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_y
#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_y+1))
#define _meshtastic_GeoPointSource_MIN meshtastic_GeoPointSource_GeoPointSource_Unspecified
#define _meshtastic_GeoPointSource_MAX meshtastic_GeoPointSource_GeoPointSource_NETWORK
@@ -1282,6 +1403,9 @@ extern "C" {
#define meshtastic_SensorFov_type_ENUMTYPE meshtastic_SensorFov_SensorType
#define meshtastic_TAKPacketV2_cot_type_id_ENUMTYPE meshtastic_CotType
#define meshtastic_TAKPacketV2_how_ENUMTYPE meshtastic_CotHow
#define meshtastic_TAKPacketV2_team_ENUMTYPE meshtastic_Team
@@ -1292,14 +1416,14 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_TAKPacket_init_default {0, false, meshtastic_Contact_init_default, false, meshtastic_Group_init_default, false, meshtastic_Status_init_default, 0, {meshtastic_PLI_init_default}}
#define meshtastic_GeoChat_init_default {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN}
#define meshtastic_GeoChat_init_default {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Group_init_default {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN}
#define meshtastic_Status_init_default {0}
#define meshtastic_Contact_init_default {"", ""}
#define meshtastic_PLI_init_default {0, 0, 0, 0, 0}
#define meshtastic_AircraftTrack_init_default {"", "", "", "", 0, "", 0, 0, ""}
#define meshtastic_CotGeoPoint_init_default {0, 0}
#define meshtastic_DrawnShape_init_default {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default, meshtastic_CotGeoPoint_init_default}, 0, 0, 0, 0, ""}
#define meshtastic_DrawnShape_init_default {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Marker_init_default {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""}
#define meshtastic_RangeAndBearing_init_default {false, meshtastic_CotGeoPoint_init_default, "", 0, 0, _meshtastic_Team_MIN, 0, 0}
#define meshtastic_Route_init_default {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default, meshtastic_Route_Link_init_default}, 0}
@@ -1310,16 +1434,19 @@ extern "C" {
#define meshtastic_TaskRequest_init_default {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""}
#define meshtastic_TAKEnvironment_init_default {0, 0, 0}
#define meshtastic_SensorFov_init_default {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}}
#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_default, false, meshtastic_SensorFov_init_default, 0, {0}}
#define meshtastic_TakTalkMessage_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, 0}
#define meshtastic_TakTalkRoomData_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Marti_init_default {{{NULL}, NULL}}
#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_default, false, meshtastic_SensorFov_init_default, false, meshtastic_Marti_init_default, 0, {meshtastic_GeoChat_init_default}}
#define meshtastic_TAKPacket_init_zero {0, false, meshtastic_Contact_init_zero, false, meshtastic_Group_init_zero, false, meshtastic_Status_init_zero, 0, {meshtastic_PLI_init_zero}}
#define meshtastic_GeoChat_init_zero {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN}
#define meshtastic_GeoChat_init_zero {"", false, "", false, "", "", _meshtastic_GeoChat_ReceiptType_MIN, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Group_init_zero {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN}
#define meshtastic_Status_init_zero {0}
#define meshtastic_Contact_init_zero {"", ""}
#define meshtastic_PLI_init_zero {0, 0, 0, 0, 0}
#define meshtastic_AircraftTrack_init_zero {"", "", "", "", 0, "", 0, 0, ""}
#define meshtastic_CotGeoPoint_init_zero {0, 0}
#define meshtastic_DrawnShape_init_zero {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, {meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero, meshtastic_CotGeoPoint_init_zero}, 0, 0, 0, 0, ""}
#define meshtastic_DrawnShape_init_zero {_meshtastic_DrawnShape_Kind_MIN, _meshtastic_DrawnShape_StyleMode_MIN, 0, 0, 0, _meshtastic_Team_MIN, 0, 0, _meshtastic_Team_MIN, 0, 0, 0, 0, 0, 0, "", {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Marker_init_zero {_meshtastic_Marker_Kind_MIN, _meshtastic_Team_MIN, 0, 0, "", "", "", ""}
#define meshtastic_RangeAndBearing_init_zero {false, meshtastic_CotGeoPoint_init_zero, "", 0, 0, _meshtastic_Team_MIN, 0, 0}
#define meshtastic_Route_init_zero {_meshtastic_Route_Method_MIN, _meshtastic_Route_Direction_MIN, "", 0, 0, {meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero, meshtastic_Route_Link_init_zero}, 0}
@@ -1330,7 +1457,10 @@ extern "C" {
#define meshtastic_TaskRequest_init_zero {"", "", "", _meshtastic_TaskRequest_Priority_MIN, _meshtastic_TaskRequest_Status_MIN, ""}
#define meshtastic_TAKEnvironment_init_zero {0, 0, 0}
#define meshtastic_SensorFov_init_zero {_meshtastic_SensorFov_SensorType_MIN, 0, false, 0, 0, 0, 0, 0, {{NULL}, NULL}}
#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_zero, false, meshtastic_SensorFov_init_zero, 0, {0}}
#define meshtastic_TakTalkMessage_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, 0}
#define meshtastic_TakTalkRoomData_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}}
#define meshtastic_Marti_init_zero {{{NULL}, NULL}}
#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", {{NULL}, NULL}, false, meshtastic_TAKEnvironment_init_zero, false, meshtastic_SensorFov_init_zero, false, meshtastic_Marti_init_zero, 0, {meshtastic_GeoChat_init_zero}}
/* Field tags (for use in manual encoding/decoding) */
#define meshtastic_GeoChat_message_tag 1
@@ -1338,6 +1468,9 @@ extern "C" {
#define meshtastic_GeoChat_to_callsign_tag 3
#define meshtastic_GeoChat_receipt_for_uid_tag 4
#define meshtastic_GeoChat_receipt_type_tag 5
#define meshtastic_GeoChat_lang_tag 6
#define meshtastic_GeoChat_room_id_tag 7
#define meshtastic_GeoChat_voice_profile_id_tag 8
#define meshtastic_Group_role_tag 1
#define meshtastic_Group_team_tag 2
#define meshtastic_Status_battery_tag 1
@@ -1377,12 +1510,13 @@ extern "C" {
#define meshtastic_DrawnShape_fill_color_tag 9
#define meshtastic_DrawnShape_fill_argb_tag 10
#define meshtastic_DrawnShape_labels_on_tag 11
#define meshtastic_DrawnShape_vertices_tag 12
#define meshtastic_DrawnShape_truncated_tag 13
#define meshtastic_DrawnShape_bullseye_distance_dm_tag 14
#define meshtastic_DrawnShape_bullseye_bearing_ref_tag 15
#define meshtastic_DrawnShape_bullseye_flags_tag 16
#define meshtastic_DrawnShape_bullseye_uid_ref_tag 17
#define meshtastic_DrawnShape_vertex_lat_deltas_tag 18
#define meshtastic_DrawnShape_vertex_lon_deltas_tag 19
#define meshtastic_Marker_kind_tag 1
#define meshtastic_Marker_color_tag 2
#define meshtastic_Marker_color_argb_tag 3
@@ -1467,6 +1601,15 @@ extern "C" {
#define meshtastic_SensorFov_elevation_deg_tag 6
#define meshtastic_SensorFov_roll_deg_tag 7
#define meshtastic_SensorFov_model_tag 8
#define meshtastic_TakTalkMessage_text_tag 1
#define meshtastic_TakTalkMessage_chatroom_id_tag 2
#define meshtastic_TakTalkMessage_lang_tag 3
#define meshtastic_TakTalkMessage_from_voice_tag 4
#define meshtastic_TakTalkRoomData_sender_callsign_tag 1
#define meshtastic_TakTalkRoomData_room_id_tag 2
#define meshtastic_TakTalkRoomData_room_name_tag 3
#define meshtastic_TakTalkRoomData_participants_tag 4
#define meshtastic_Marti_dest_callsign_tag 1
#define meshtastic_TAKPacketV2_cot_type_id_tag 1
#define meshtastic_TAKPacketV2_how_tag 2
#define meshtastic_TAKPacketV2_callsign_tag 3
@@ -1493,7 +1636,7 @@ extern "C" {
#define meshtastic_TAKPacketV2_remarks_tag 24
#define meshtastic_TAKPacketV2_environment_tag 25
#define meshtastic_TAKPacketV2_sensor_fov_tag 26
#define meshtastic_TAKPacketV2_pli_tag 30
#define meshtastic_TAKPacketV2_marti_tag 29
#define meshtastic_TAKPacketV2_chat_tag 31
#define meshtastic_TAKPacketV2_aircraft_tag 32
#define meshtastic_TAKPacketV2_raw_detail_tag 33
@@ -1504,6 +1647,8 @@ extern "C" {
#define meshtastic_TAKPacketV2_casevac_tag 38
#define meshtastic_TAKPacketV2_emergency_tag 39
#define meshtastic_TAKPacketV2_task_tag 40
#define meshtastic_TAKPacketV2_taktalk_tag 41
#define meshtastic_TAKPacketV2_taktalk_room_tag 42
/* Struct field encoding specification for nanopb */
#define meshtastic_TAKPacket_FIELDLIST(X, a) \
@@ -1527,8 +1672,11 @@ X(a, STATIC, SINGULAR, STRING, message, 1) \
X(a, STATIC, OPTIONAL, STRING, to, 2) \
X(a, STATIC, OPTIONAL, STRING, to_callsign, 3) \
X(a, STATIC, SINGULAR, STRING, receipt_for_uid, 4) \
X(a, STATIC, SINGULAR, UENUM, receipt_type, 5)
#define meshtastic_GeoChat_CALLBACK NULL
X(a, STATIC, SINGULAR, UENUM, receipt_type, 5) \
X(a, CALLBACK, OPTIONAL, STRING, lang, 6) \
X(a, CALLBACK, OPTIONAL, STRING, room_id, 7) \
X(a, CALLBACK, OPTIONAL, STRING, voice_profile_id, 8)
#define meshtastic_GeoChat_CALLBACK pb_default_field_callback
#define meshtastic_GeoChat_DEFAULT NULL
#define meshtastic_Group_FIELDLIST(X, a) \
@@ -1588,15 +1736,15 @@ X(a, STATIC, SINGULAR, UINT32, stroke_weight_x10, 8) \
X(a, STATIC, SINGULAR, UENUM, fill_color, 9) \
X(a, STATIC, SINGULAR, FIXED32, fill_argb, 10) \
X(a, STATIC, SINGULAR, BOOL, labels_on, 11) \
X(a, STATIC, REPEATED, MESSAGE, vertices, 12) \
X(a, STATIC, SINGULAR, BOOL, truncated, 13) \
X(a, STATIC, SINGULAR, UINT32, bullseye_distance_dm, 14) \
X(a, STATIC, SINGULAR, UINT32, bullseye_bearing_ref, 15) \
X(a, STATIC, SINGULAR, UINT32, bullseye_flags, 16) \
X(a, STATIC, SINGULAR, STRING, bullseye_uid_ref, 17)
#define meshtastic_DrawnShape_CALLBACK NULL
X(a, STATIC, SINGULAR, STRING, bullseye_uid_ref, 17) \
X(a, CALLBACK, REPEATED, SINT32, vertex_lat_deltas, 18) \
X(a, CALLBACK, REPEATED, SINT32, vertex_lon_deltas, 19)
#define meshtastic_DrawnShape_CALLBACK pb_default_field_callback
#define meshtastic_DrawnShape_DEFAULT NULL
#define meshtastic_DrawnShape_vertices_MSGTYPE meshtastic_CotGeoPoint
#define meshtastic_Marker_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, kind, 1) \
@@ -1726,6 +1874,27 @@ X(a, CALLBACK, SINGULAR, STRING, model, 8)
#define meshtastic_SensorFov_CALLBACK pb_default_field_callback
#define meshtastic_SensorFov_DEFAULT NULL
#define meshtastic_TakTalkMessage_FIELDLIST(X, a) \
X(a, CALLBACK, SINGULAR, STRING, text, 1) \
X(a, CALLBACK, SINGULAR, STRING, chatroom_id, 2) \
X(a, CALLBACK, SINGULAR, STRING, lang, 3) \
X(a, STATIC, SINGULAR, BOOL, from_voice, 4)
#define meshtastic_TakTalkMessage_CALLBACK pb_default_field_callback
#define meshtastic_TakTalkMessage_DEFAULT NULL
#define meshtastic_TakTalkRoomData_FIELDLIST(X, a) \
X(a, CALLBACK, SINGULAR, STRING, sender_callsign, 1) \
X(a, CALLBACK, SINGULAR, STRING, room_id, 2) \
X(a, CALLBACK, SINGULAR, STRING, room_name, 3) \
X(a, CALLBACK, REPEATED, STRING, participants, 4)
#define meshtastic_TakTalkRoomData_CALLBACK pb_default_field_callback
#define meshtastic_TakTalkRoomData_DEFAULT NULL
#define meshtastic_Marti_FIELDLIST(X, a) \
X(a, CALLBACK, REPEATED, STRING, dest_callsign, 1)
#define meshtastic_Marti_CALLBACK pb_default_field_callback
#define meshtastic_Marti_DEFAULT NULL
#define meshtastic_TAKPacketV2_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UENUM, cot_type_id, 1) \
X(a, STATIC, SINGULAR, UENUM, how, 2) \
@@ -1753,7 +1922,7 @@ X(a, STATIC, SINGULAR, STRING, cot_type_str, 23) \
X(a, CALLBACK, SINGULAR, STRING, remarks, 24) \
X(a, STATIC, OPTIONAL, MESSAGE, environment, 25) \
X(a, STATIC, OPTIONAL, MESSAGE, sensor_fov, 26) \
X(a, STATIC, ONEOF, BOOL, (payload_variant,pli,payload_variant.pli), 30) \
X(a, STATIC, OPTIONAL, MESSAGE, marti, 29) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chat,payload_variant.chat), 31) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,aircraft,payload_variant.aircraft), 32) \
X(a, STATIC, ONEOF, BYTES, (payload_variant,raw_detail,payload_variant.raw_detail), 33) \
@@ -1763,11 +1932,14 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,rab,payload_variant.rab), 3
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,route,payload_variant.route), 37) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,casevac,payload_variant.casevac), 38) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,emergency,payload_variant.emergency), 39) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), 40)
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task), 40) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,taktalk,payload_variant.taktalk), 41) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,taktalk_room,payload_variant.taktalk_room), 42)
#define meshtastic_TAKPacketV2_CALLBACK pb_default_field_callback
#define meshtastic_TAKPacketV2_DEFAULT NULL
#define meshtastic_TAKPacketV2_environment_MSGTYPE meshtastic_TAKEnvironment
#define meshtastic_TAKPacketV2_sensor_fov_MSGTYPE meshtastic_SensorFov
#define meshtastic_TAKPacketV2_marti_MSGTYPE meshtastic_Marti
#define meshtastic_TAKPacketV2_payload_variant_chat_MSGTYPE meshtastic_GeoChat
#define meshtastic_TAKPacketV2_payload_variant_aircraft_MSGTYPE meshtastic_AircraftTrack
#define meshtastic_TAKPacketV2_payload_variant_shape_MSGTYPE meshtastic_DrawnShape
@@ -1777,6 +1949,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,task,payload_variant.task),
#define meshtastic_TAKPacketV2_payload_variant_casevac_MSGTYPE meshtastic_CasevacReport
#define meshtastic_TAKPacketV2_payload_variant_emergency_MSGTYPE meshtastic_EmergencyAlert
#define meshtastic_TAKPacketV2_payload_variant_task_MSGTYPE meshtastic_TaskRequest
#define meshtastic_TAKPacketV2_payload_variant_taktalk_MSGTYPE meshtastic_TakTalkMessage
#define meshtastic_TAKPacketV2_payload_variant_taktalk_room_MSGTYPE meshtastic_TakTalkRoomData
extern const pb_msgdesc_t meshtastic_TAKPacket_msg;
extern const pb_msgdesc_t meshtastic_GeoChat_msg;
@@ -1797,6 +1971,9 @@ extern const pb_msgdesc_t meshtastic_EmergencyAlert_msg;
extern const pb_msgdesc_t meshtastic_TaskRequest_msg;
extern const pb_msgdesc_t meshtastic_TAKEnvironment_msg;
extern const pb_msgdesc_t meshtastic_SensorFov_msg;
extern const pb_msgdesc_t meshtastic_TakTalkMessage_msg;
extern const pb_msgdesc_t meshtastic_TakTalkRoomData_msg;
extern const pb_msgdesc_t meshtastic_Marti_msg;
extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
@@ -1819,20 +1996,27 @@ extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg;
#define meshtastic_TaskRequest_fields &meshtastic_TaskRequest_msg
#define meshtastic_TAKEnvironment_fields &meshtastic_TAKEnvironment_msg
#define meshtastic_SensorFov_fields &meshtastic_SensorFov_msg
#define meshtastic_TakTalkMessage_fields &meshtastic_TakTalkMessage_msg
#define meshtastic_TakTalkRoomData_fields &meshtastic_TakTalkRoomData_msg
#define meshtastic_Marti_fields &meshtastic_Marti_msg
#define meshtastic_TAKPacketV2_fields &meshtastic_TAKPacketV2_msg
/* Maximum encoded size of messages (where known) */
/* meshtastic_TAKPacket_size depends on runtime parameters */
/* meshtastic_GeoChat_size depends on runtime parameters */
/* meshtastic_DrawnShape_size depends on runtime parameters */
/* meshtastic_CasevacReport_size depends on runtime parameters */
/* meshtastic_ZMistEntry_size depends on runtime parameters */
/* meshtastic_SensorFov_size depends on runtime parameters */
/* meshtastic_TakTalkMessage_size depends on runtime parameters */
/* meshtastic_TakTalkRoomData_size depends on runtime parameters */
/* meshtastic_Marti_size depends on runtime parameters */
/* meshtastic_TAKPacketV2_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_Route_size
#define meshtastic_AircraftTrack_size 134
#define meshtastic_Contact_size 242
#define meshtastic_CotGeoPoint_size 12
#define meshtastic_DrawnShape_size 553
#define meshtastic_EmergencyAlert_size 100
#define meshtastic_GeoChat_size 495
#define meshtastic_Group_size 4
#define meshtastic_Marker_size 191
#define meshtastic_PLI_size 31
@@ -1841,7 +2025,6 @@ extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg;
#define meshtastic_Route_size 1379
#define meshtastic_Status_size 3
#define meshtastic_TAKEnvironment_size 18
#define meshtastic_TAKPacket_size 756
#define meshtastic_TaskRequest_size 132
#ifdef __cplusplus
+37 -8
View File
@@ -290,15 +290,30 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28,
/* ITU Region 2 Amateur Radio 2m band (144-148 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M = 28,
/* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29,
/* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30,
meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31,
/* EU 868MHz band, with narrow presets */
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32,
/* ITU Region 3 Amateur Radio 2m band (144-148 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M = 33,
/* ITU Region 1 Amateur Radio 70cm band (430-440 MHz) */
meshtastic_Config_LoRaConfig_RegionCode_ITU1_70CM = 34,
/* ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law! */
meshtastic_Config_LoRaConfig_RegionCode_ITU2_70CM = 35,
/* ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law! */
meshtastic_Config_LoRaConfig_RegionCode_ITU3_70CM = 36,
/* ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law! */
meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM = 37
} meshtastic_Config_LoRaConfig_RegionCode;
/* Standard predefined channel settings
@@ -345,7 +360,21 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Narrow Slow
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST. */
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13,
/* Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST. */
meshtastic_Config_LoRaConfig_ModemPreset_TINY_FAST = 14,
/* Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE. */
meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW = 15
} meshtastic_Config_LoRaConfig_ModemPreset;
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
@@ -734,12 +763,12 @@ extern "C" {
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1))
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_TINY_SLOW+1))
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
+15 -6
View File
@@ -621,7 +621,9 @@ typedef enum _meshtastic_MeshPacket_TransportMechanism {
/* Arrived via Multicast UDP */
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP = 6,
/* Arrived via API connection */
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API = 7,
/* Arrived via Unicast UDP */
meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP = 8
} meshtastic_MeshPacket_TransportMechanism;
/* Log levels, chosen to match python logging conventions. */
@@ -658,7 +660,14 @@ typedef enum _meshtastic_LockdownStatus_State {
token's TTL. */
meshtastic_LockdownStatus_State_UNLOCKED = 3,
/* Passphrase rejected. backoff_seconds is non-zero when rate-limited. */
meshtastic_LockdownStatus_State_UNLOCK_FAILED = 4
meshtastic_LockdownStatus_State_UNLOCK_FAILED = 4,
/* Lockdown is supported by this firmware but not currently active
(no passphrase has been provisioned, or it was disabled via
AdminMessage.lockdown_auth.disable). The device is operating in
normal, non-encrypted mode. Clients render the lockdown-mode
toggle as OFF on receiving this. Distinct from NEEDS_PROVISION,
which is only used during an in-progress enable flow. */
meshtastic_LockdownStatus_State_DISABLED = 5
} meshtastic_LockdownStatus_State;
/* Struct definitions */
@@ -1525,16 +1534,16 @@ extern "C" {
#define _meshtastic_MeshPacket_Delayed_ARRAYSIZE ((meshtastic_MeshPacket_Delayed)(meshtastic_MeshPacket_Delayed_DELAYED_DIRECT+1))
#define _meshtastic_MeshPacket_TransportMechanism_MIN meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL
#define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API
#define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API+1))
#define _meshtastic_MeshPacket_TransportMechanism_MAX meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP
#define _meshtastic_MeshPacket_TransportMechanism_ARRAYSIZE ((meshtastic_MeshPacket_TransportMechanism)(meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP+1))
#define _meshtastic_LogRecord_Level_MIN meshtastic_LogRecord_Level_UNSET
#define _meshtastic_LogRecord_Level_MAX meshtastic_LogRecord_Level_CRITICAL
#define _meshtastic_LogRecord_Level_ARRAYSIZE ((meshtastic_LogRecord_Level)(meshtastic_LogRecord_Level_CRITICAL+1))
#define _meshtastic_LockdownStatus_State_MIN meshtastic_LockdownStatus_State_STATE_UNSPECIFIED
#define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_UNLOCK_FAILED
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_UNLOCK_FAILED+1))
#define _meshtastic_LockdownStatus_State_MAX meshtastic_LockdownStatus_State_DISABLED
#define _meshtastic_LockdownStatus_State_ARRAYSIZE ((meshtastic_LockdownStatus_State)(meshtastic_LockdownStatus_State_DISABLED+1))
#define meshtastic_Position_location_source_ENUMTYPE meshtastic_Position_LocSource
#define meshtastic_Position_altitude_source_ENUMTYPE meshtastic_Position_AltSource
+253 -174
View File
@@ -11,11 +11,12 @@
#endif
#include "SPILock.h"
#include "power.h"
#include "serialization/JSON.h"
#include <FSCommon.h>
#include <HTTPBodyParser.hpp>
#include <HTTPMultipartBodyParser.hpp>
#include <HTTPURLEncodedBodyParser.hpp>
#include <cmath>
#include <sstream>
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
@@ -259,40 +260,95 @@ void htmlDeleteDir(const char *dirname)
root.close();
}
JSONArray htmlListDir(const char *dirname, uint8_t levels)
// Escape a string into a JSON double-quoted literal. Matches the previous
// SimpleJSON StringifyString behavior (0x00-0x1F and 0x7F -> \u00xx lowercase,
// escapes " \ / \b \f \n \r \t, UTF-8 passes through unchanged).
static std::string jsonEscape(const std::string &str)
{
std::string out = "\"";
for (size_t i = 0; i < str.size(); ++i) {
char chr = str[i];
if (chr == '"' || chr == '\\' || chr == '/') {
out += '\\';
out += chr;
} else if (chr == '\b') {
out += "\\b";
} else if (chr == '\f') {
out += "\\f";
} else if (chr == '\n') {
out += "\\n";
} else if (chr == '\r') {
out += "\\r";
} else if (chr == '\t') {
out += "\\t";
} else if ((unsigned char)chr < 0x20 || chr == 0x7F) {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)chr);
out += buf;
} else {
out += chr;
}
}
out += "\"";
return out;
}
// Format a numeric value the way the previous SimpleJSON serializer did
// (std::stringstream with precision 15, NaN/Inf -> "null").
static std::string jsonNum(double v)
{
if (std::isinf(v) || std::isnan(v))
return "null";
std::ostringstream ss;
ss.precision(15);
ss << v;
return ss.str();
}
// Build a serialized JSON array string listing files in `dirname`.
// Subdirectories recurse as nested arrays (up to `levels` deep).
std::string htmlListDir(const char *dirname, uint8_t levels)
{
File root = FSCom.open(dirname, FILE_O_READ);
JSONArray fileList;
std::string out = "[";
bool first = true;
if (!root) {
return fileList;
out += "]";
return out;
}
if (!root.isDirectory()) {
return fileList;
out += "]";
return out;
}
// iterate over the file list
File file = root.openNextFile();
while (file) {
std::string element;
bool haveElement = false;
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
if (levels) {
#ifdef ARCH_ESP32
fileList.push_back(new JSONValue(htmlListDir(file.path(), levels - 1)));
element = htmlListDir(file.path(), levels - 1);
#else
fileList.push_back(new JSONValue(htmlListDir(file.name(), levels - 1)));
element = htmlListDir(file.name(), levels - 1);
#endif
haveElement = true;
file.close();
}
} else {
JSONObject thisFileMap;
thisFileMap["size"] = new JSONValue((int)file.size());
#ifdef ARCH_ESP32
String fileName = String(file.path()).substring(1);
thisFileMap["name"] = new JSONValue(fileName.c_str());
#else
String fileName = String(file.name()).substring(1);
thisFileMap["name"] = new JSONValue(fileName.c_str());
#endif
String tempName = String(file.name()).substring(1);
// Keys in the previous std::map<string,...> were emitted in
// alphabetical order: name, nameModified, size.
element = "{";
element += jsonEscape("name");
element += ":";
element += jsonEscape(fileName.c_str());
if (tempName.endsWith(".gz")) {
#ifdef ARCH_ESP32
String modifiedFile = String(file.path()).substring(1);
@@ -300,15 +356,30 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels)
String modifiedFile = String(file.name()).substring(1);
#endif
modifiedFile.remove((modifiedFile.length() - 3), 3);
thisFileMap["nameModified"] = new JSONValue(modifiedFile.c_str());
element += ",";
element += jsonEscape("nameModified");
element += ":";
element += jsonEscape(modifiedFile.c_str());
}
fileList.push_back(new JSONValue(thisFileMap));
element += ",";
element += jsonEscape("size");
element += ":";
element += jsonNum((int)file.size());
element += "}";
haveElement = true;
}
if (haveElement) {
if (!first)
out += ",";
out += element;
first = false;
}
file.close();
file = root.openNextFile();
}
root.close();
return fileList;
out += "]";
return out;
}
void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
@@ -318,28 +389,25 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
res->setHeader("Access-Control-Allow-Methods", "GET");
concurrency::LockGuard g(spiLock);
auto fileList = htmlListDir("/static", 10);
std::string fileList = htmlListDir("/static", 10);
// create json output structure
JSONObject filesystemObj;
filesystemObj["total"] = new JSONValue((int)FSCom.totalBytes());
filesystemObj["used"] = new JSONValue((int)FSCom.usedBytes());
filesystemObj["free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
uint64_t total = FSCom.totalBytes();
uint64_t used = FSCom.usedBytes();
JSONObject jsonObjInner;
jsonObjInner["files"] = new JSONValue(fileList);
jsonObjInner["filesystem"] = new JSONValue(filesystemObj);
// Key order matches the previous std::map-based emission (alphabetical).
std::string out;
out.reserve(fileList.size() + 128);
out += "{\"data\":{\"files\":";
out += fileList;
out += ",\"filesystem\":{\"free\":";
out += jsonNum((int)(total - used));
out += ",\"total\":";
out += jsonNum((int)total);
out += ",\"used\":";
out += jsonNum((int)used);
out += "}},\"status\":\"ok\"}";
JSONObject jsonObjOuter;
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
jsonObjOuter["status"] = new JSONValue("ok");
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
res->print(out.c_str());
}
void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
@@ -354,27 +422,13 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
if (params->getQueryParameter("delete", paramValDelete)) {
std::string pathDelete = "/" + paramValDelete;
concurrency::LockGuard g(spiLock);
if (FSCom.remove(pathDelete.c_str())) {
LOG_INFO("%s", pathDelete.c_str());
JSONObject jsonObjOuter;
jsonObjOuter["status"] = new JSONValue("ok");
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
return;
} else {
LOG_INFO("%s", pathDelete.c_str());
JSONObject jsonObjOuter;
jsonObjOuter["status"] = new JSONValue("Error");
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
return;
}
const char *status = FSCom.remove(pathDelete.c_str()) ? "ok" : "Error";
LOG_INFO("%s", pathDelete.c_str());
std::string out = "{\"status\":";
out += jsonEscape(status);
out += "}";
res->print(out.c_str());
return;
}
}
@@ -615,95 +669,113 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
res->println("<pre>");
}
// Helper lambda to create JSON array and clean up memory properly
auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * {
JSONArray tempArray;
auto arrayFromLog = [](const uint32_t *logArray, int count) -> std::string {
std::string s = "[";
for (int i = 0; i < count; i++) {
tempArray.push_back(new JSONValue((int)logArray[i]));
if (i)
s += ",";
s += jsonNum((int)logArray[i]);
}
JSONValue *result = new JSONValue(tempArray);
// Note: Don't delete tempArray elements here - JSONValue now owns them
return result;
s += "]";
return s;
};
// data->airtime->tx_log
uint32_t *logArray;
logArray = airTime->airtimeReport(TX_LOG);
JSONValue *txLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
// data->airtime->rx_log
std::string txLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
logArray = airTime->airtimeReport(RX_LOG);
JSONValue *rxLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
// data->airtime->rx_all_log
std::string rxLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
logArray = airTime->airtimeReport(RX_ALL_LOG);
JSONValue *rxAllLogJsonValue = createJSONArrayFromLog(logArray, airTime->getPeriodsToLog());
std::string rxAllLog = arrayFromLog(logArray, airTime->getPeriodsToLog());
// data->airtime
JSONObject jsonObjAirtime;
jsonObjAirtime["tx_log"] = txLogJsonValue;
jsonObjAirtime["rx_log"] = rxLogJsonValue;
jsonObjAirtime["rx_all_log"] = rxAllLogJsonValue;
jsonObjAirtime["channel_utilization"] = new JSONValue(airTime->channelUtilizationPercent());
jsonObjAirtime["utilization_tx"] = new JSONValue(airTime->utilizationTXPercent());
jsonObjAirtime["seconds_since_boot"] = new JSONValue(int(airTime->getSecondsSinceBoot()));
jsonObjAirtime["seconds_per_period"] = new JSONValue(int(airTime->getSecondsPerPeriod()));
jsonObjAirtime["periods_to_log"] = new JSONValue(airTime->getPeriodsToLog());
// data->wifi
JSONObject jsonObjWifi;
jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
String wifiIPString = WiFi.localIP().toString();
std::string wifiIP = wifiIPString.c_str();
jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
// data->memory
JSONObject jsonObjMemory;
jsonObjMemory["heap_total"] = new JSONValue((int)memGet.getHeapSize());
jsonObjMemory["heap_free"] = new JSONValue((int)memGet.getFreeHeap());
jsonObjMemory["psram_total"] = new JSONValue((int)memGet.getPsramSize());
jsonObjMemory["psram_free"] = new JSONValue((int)memGet.getFreePsram());
spiLock->lock();
jsonObjMemory["fs_total"] = new JSONValue((int)FSCom.totalBytes());
jsonObjMemory["fs_used"] = new JSONValue((int)FSCom.usedBytes());
jsonObjMemory["fs_free"] = new JSONValue(int(FSCom.totalBytes() - FSCom.usedBytes()));
uint64_t fsTotal = FSCom.totalBytes();
uint64_t fsUsed = FSCom.usedBytes();
spiLock->unlock();
// data->power
JSONObject jsonObjPower;
jsonObjPower["battery_percent"] = new JSONValue(powerStatus->getBatteryChargePercent());
jsonObjPower["battery_voltage_mv"] = new JSONValue(powerStatus->getBatteryVoltageMv());
jsonObjPower["has_battery"] = new JSONValue(BoolToString(powerStatus->getHasBattery()));
jsonObjPower["has_usb"] = new JSONValue(BoolToString(powerStatus->getHasUSB()));
jsonObjPower["is_charging"] = new JSONValue(BoolToString(powerStatus->getIsCharging()));
// Emit keys in the same alphabetical order as the previous
// std::map-based JSON output to keep responses byte-compatible.
std::string out;
out.reserve(1024);
out += "{\"data\":{";
// data->device
JSONObject jsonObjDevice;
jsonObjDevice["reboot_counter"] = new JSONValue((int)myNodeInfo.reboot_count);
// airtime
out += "\"airtime\":{";
out += "\"channel_utilization\":";
out += jsonNum(airTime->channelUtilizationPercent());
out += ",\"periods_to_log\":";
out += jsonNum(airTime->getPeriodsToLog());
out += ",\"rx_all_log\":";
out += rxAllLog;
out += ",\"rx_log\":";
out += rxLog;
out += ",\"seconds_per_period\":";
out += jsonNum((int)airTime->getSecondsPerPeriod());
out += ",\"seconds_since_boot\":";
out += jsonNum((int)airTime->getSecondsSinceBoot());
out += ",\"tx_log\":";
out += txLog;
out += ",\"utilization_tx\":";
out += jsonNum(airTime->utilizationTXPercent());
out += "}";
// data->radio
JSONObject jsonObjRadio;
jsonObjRadio["frequency"] = new JSONValue(RadioLibInterface::instance->getFreq());
jsonObjRadio["lora_channel"] = new JSONValue((int)RadioLibInterface::instance->getChannelNum() + 1);
// device
out += ",\"device\":{\"reboot_counter\":";
out += jsonNum((int)myNodeInfo.reboot_count);
out += "}";
// collect data to inner data object
JSONObject jsonObjInner;
jsonObjInner["airtime"] = new JSONValue(jsonObjAirtime);
jsonObjInner["wifi"] = new JSONValue(jsonObjWifi);
jsonObjInner["memory"] = new JSONValue(jsonObjMemory);
jsonObjInner["power"] = new JSONValue(jsonObjPower);
jsonObjInner["device"] = new JSONValue(jsonObjDevice);
jsonObjInner["radio"] = new JSONValue(jsonObjRadio);
// memory
out += ",\"memory\":{";
out += "\"fs_free\":";
out += jsonNum((int)(fsTotal - fsUsed));
out += ",\"fs_total\":";
out += jsonNum((int)fsTotal);
out += ",\"fs_used\":";
out += jsonNum((int)fsUsed);
out += ",\"heap_free\":";
out += jsonNum((int)memGet.getFreeHeap());
out += ",\"heap_total\":";
out += jsonNum((int)memGet.getHeapSize());
out += ",\"psram_free\":";
out += jsonNum((int)memGet.getFreePsram());
out += ",\"psram_total\":";
out += jsonNum((int)memGet.getPsramSize());
out += "}";
// create json output structure
JSONObject jsonObjOuter;
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
jsonObjOuter["status"] = new JSONValue("ok");
// serialize and write it to the stream
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
// power (has_* / is_charging were serialized as the strings "true"/"false")
out += ",\"power\":{";
out += "\"battery_percent\":";
out += jsonNum(powerStatus->getBatteryChargePercent());
out += ",\"battery_voltage_mv\":";
out += jsonNum(powerStatus->getBatteryVoltageMv());
out += ",\"has_battery\":";
out += jsonEscape(BoolToString(powerStatus->getHasBattery()));
out += ",\"has_usb\":";
out += jsonEscape(BoolToString(powerStatus->getHasUSB()));
out += ",\"is_charging\":";
out += jsonEscape(BoolToString(powerStatus->getIsCharging()));
out += "}";
// radio
out += ",\"radio\":{\"frequency\":";
out += jsonNum(RadioLibInterface::instance->getFreq());
out += ",\"lora_channel\":";
out += jsonNum((int)RadioLibInterface::instance->getChannelNum() + 1);
out += "}";
// wifi
out += ",\"wifi\":{\"ip\":";
out += jsonEscape(wifiIP);
out += ",\"rssi\":";
out += jsonNum(WiFi.RSSI());
out += "}";
out += "},\"status\":\"ok\"}";
res->print(out.c_str());
}
void handleNodes(HTTPRequest *req, HTTPResponse *res)
@@ -724,58 +796,66 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
res->println("<pre>");
}
JSONArray nodesArray;
std::string out;
out.reserve(2048);
out += "{\"data\":{\"nodes\":[";
bool firstNode = true;
uint32_t readIndex = 0;
const meshtastic_NodeInfoLite *tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
while (tempNodeInfo != NULL) {
if (nodeInfoLiteHasUser(tempNodeInfo)) {
JSONObject node;
char id[16];
snprintf(id, sizeof(id), "!%08x", tempNodeInfo->num);
node["id"] = new JSONValue(id);
node["snr"] = new JSONValue(tempNodeInfo->snr);
node["via_mqtt"] = new JSONValue(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
node["last_heard"] = new JSONValue((int)tempNodeInfo->last_heard);
node["position"] = new JSONValue();
std::string position;
if (nodeDB->hasValidPosition(tempNodeInfo)) {
meshtastic_PositionLite posLite;
if (nodeDB->copyNodePosition(tempNodeInfo->num, posLite)) {
JSONObject position;
position["latitude"] = new JSONValue((float)posLite.latitude_i * 1e-7);
position["longitude"] = new JSONValue((float)posLite.longitude_i * 1e-7);
position["altitude"] = new JSONValue((int)posLite.altitude);
node["position"] = new JSONValue(position);
position = "{\"altitude\":";
position += jsonNum((int)posLite.altitude);
position += ",\"latitude\":";
position += jsonNum((float)posLite.latitude_i * 1e-7);
position += ",\"longitude\":";
position += jsonNum((float)posLite.longitude_i * 1e-7);
position += "}";
} else {
position = "null";
}
} else {
position = "null";
}
node["long_name"] = new JSONValue(tempNodeInfo->long_name);
node["short_name"] = new JSONValue(tempNodeInfo->short_name);
// mac_address dropped from NodeInfoLite as part of the slim refactor; emit zeros.
node["mac_address"] = new JSONValue("00:00:00:00:00:00");
node["hw_model"] = new JSONValue(tempNodeInfo->hw_model);
if (!firstNode)
out += ",";
firstNode = false;
nodesArray.push_back(new JSONValue(node));
// Alphabetical key order matches previous std::map-based output.
out += "{\"hw_model\":";
out += jsonNum(tempNodeInfo->hw_model);
out += ",\"id\":";
out += jsonEscape(id);
out += ",\"last_heard\":";
out += jsonNum((int)tempNodeInfo->last_heard);
out += ",\"long_name\":";
out += jsonEscape(tempNodeInfo->long_name);
out += ",\"mac_address\":";
out += jsonEscape("00:00:00:00:00:00");
out += ",\"position\":";
out += position;
out += ",\"short_name\":";
out += jsonEscape(tempNodeInfo->short_name);
out += ",\"snr\":";
out += jsonNum(tempNodeInfo->snr);
out += ",\"via_mqtt\":";
out += jsonEscape(BoolToString(nodeInfoLiteViaMqtt(tempNodeInfo)));
out += "}";
}
tempNodeInfo = nodeDB->readNextMeshNode(readIndex);
}
// collect data to inner data object
JSONObject jsonObjInner;
jsonObjInner["nodes"] = new JSONValue(nodesArray);
// create json output structure
JSONObject jsonObjOuter;
jsonObjOuter["data"] = new JSONValue(jsonObjInner);
jsonObjOuter["status"] = new JSONValue("ok");
// serialize and write it to the stream
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
out += "]},\"status\":\"ok\"}";
res->print(out.c_str());
}
/*
@@ -897,20 +977,28 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
int n = WiFi.scanNetworks();
// build list of network objects
JSONArray networkObjs;
std::string out = "{\"data\":[";
bool firstNet = true;
if (n > 0) {
for (int i = 0; i < n; ++i) {
char ssidArray[50];
// The previous implementation pre-escaped quotes before handing
// the value to the JSON serializer; preserve that (byte-compatible
// even if it double-encodes a quote) so existing clients are not
// affected by this refactor.
String ssidString = String(WiFi.SSID(i));
ssidString.replace("\"", "\\\"");
ssidString.toCharArray(ssidArray, 50);
if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {
JSONObject thisNetwork;
thisNetwork["ssid"] = new JSONValue(ssidArray);
thisNetwork["rssi"] = new JSONValue(int(WiFi.RSSI(i)));
networkObjs.push_back(new JSONValue(thisNetwork));
if (!firstNet)
out += ",";
firstNet = false;
out += "{\"rssi\":";
out += jsonNum((int)WiFi.RSSI(i));
out += ",\"ssid\":";
out += jsonEscape(ssidArray);
out += "}";
}
// Yield some cpu cycles to IP stack.
// This is important in case the list is large and it takes us time to return
@@ -918,16 +1006,7 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
yield();
}
}
// build output structure
JSONObject jsonObjOuter;
jsonObjOuter["data"] = new JSONValue(networkObjs);
jsonObjOuter["status"] = new JSONValue("ok");
// serialize and write it to the stream
JSONValue *value = new JSONValue(jsonObjOuter);
std::string jsonString = value->Stringify();
res->print(jsonString.c_str());
delete value;
out += "],\"status\":\"ok\"}";
res->print(out.c_str());
}
#endif
+9
View File
@@ -109,6 +109,15 @@ static inline int get_max_num_nodes()
#define HAS_TRAFFIC_MANAGEMENT 0
#endif
// HopScalingModule - variable hop module: dynamically adjusts broadcast hop_limit based on mesh density
// Enable per-variant by defining HAS_VARIABLE_HOPS=1 in variant.h
#ifdef ARCH_STM32WL
#define HAS_VARIABLE_HOPS 0
#endif
#ifndef HAS_VARIABLE_HOPS
#define HAS_VARIABLE_HOPS 1
#endif
// Cache size for traffic management (number of nodes to track)
// Can be overridden per-variant based on available memory
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
+149
View File
@@ -0,0 +1,149 @@
#pragma once
#ifdef ARCH_PORTDUINO
// Native unicast-UDP connector for meshtasticd. Forwards every outbound MeshPacket to a single
// configured peer (the meshswitch routing daemon) and injects packets received back from it,
// routing on the cleartext header exactly like the multicast handler. This is a meshtasticd-only
// (Linux) feature, so it uses plain POSIX sockets directly rather than the portduino AsyncUDP
// library.
//
// meshtasticd is the client here: it connect()s one UDP socket to the peer, send()s outbound
// packets, and recv()s the daemon's replies on that same socket (the daemon always answers the
// source endpoint it last heard from, so no well-known listen port is required).
#include "configuration.h"
#include "main.h"
#include "mesh/Router.h"
#include "mesh/mesh-pb-constants.h"
#include <atomic>
#include <cstdlib>
#include <string>
#include <thread>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#define UDP_UNICAST_DEFAULT_PORT 4403 // matches the daemon's default listen port
class UdpUnicastConnector final
{
public:
UdpUnicastConnector() = default;
~UdpUnicastConnector() { stop(); }
// peer is "host" or "host:port" (IPv4 literal). Returns true once connected.
bool start(const std::string &peer)
{
if (isRunning)
return true;
std::string host = peer;
uint16_t port = UDP_UNICAST_DEFAULT_PORT;
auto colon = peer.rfind(':');
if (colon != std::string::npos) {
host = peer.substr(0, colon);
port = (uint16_t)atoi(peer.substr(colon + 1).c_str());
}
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
LOG_ERROR("UDP unicast: bad peer address '%s'", peer.c_str());
return false;
}
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
LOG_ERROR("UDP unicast: socket() failed");
return false;
}
// connect() pins the destination for send() and filters recv() to datagrams from the peer.
if (::connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
LOG_ERROR("UDP unicast: connect() to %s:%u failed", host.c_str(), port);
::close(fd);
fd = -1;
return false;
}
isRunning = true;
rxThread = std::thread([this]() { receiveLoop(); });
LOG_INFO("UDP unicast connector to %s:%u started", host.c_str(), port);
return true;
}
void stop()
{
if (!isRunning)
return;
isRunning = false;
if (fd >= 0)
::shutdown(fd, SHUT_RDWR); // unblock the recv() in the rx thread
if (rxThread.joinable())
rxThread.join();
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
// Called from Router::send for every outbound packet.
void onSend(const meshtastic_MeshPacket *mp)
{
if (!isRunning || fd < 0 || !mp)
return;
// Never reflect a packet that arrived over UDP straight back out over UDP. The daemon's
// (from,id) dedup is the backstop if this ever slips through.
if (mp->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP)
return;
uint8_t buffer[meshtastic_MeshPacket_size];
size_t len = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp);
if (len == 0)
return;
#ifdef MSG_NOSIGNAL
constexpr int kSendFlags = MSG_NOSIGNAL;
#else
constexpr int kSendFlags = 0;
#endif
if (::send(fd, buffer, len, kSendFlags) < 0)
LOG_DEBUG("UDP unicast: send() failed for packet id=%u", mp->id);
}
private:
void receiveLoop()
{
uint8_t buf[meshtastic_MeshPacket_size];
while (isRunning) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
if (!isRunning)
break;
continue; // transient error or shutdown in progress
}
meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero;
if (!pb_decode_from_bytes(buf, (size_t)n, &meshtastic_MeshPacket_msg, &mp))
continue;
if (mp.which_payload_variant != meshtastic_MeshPacket_encrypted_tag)
continue;
// Drop packets with spoofed local origin — same guard as the multicast handler.
if (isFromUs(&mp)) {
LOG_WARN("UDP unicast packet with spoofed local from=0x%x, dropping", mp.from);
continue;
}
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_UNICAST_UDP;
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
p->rx_snr = 0;
p->rx_rssi = 0;
if (router)
router->enqueueReceivedMessage(p.release());
}
}
int fd = -1;
std::atomic<bool> isRunning{false};
std::thread rxThread;
};
#endif // ARCH_PORTDUINO

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