Compare commits

...
68 Commits
Author SHA1 Message Date
Thomas GöttgensandGitHub 82a66f2d70 Merge branch 'develop' into zps-module 2026-06-07 10:08:31 +02: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
Thomas Göttgens d98bc0b4ee fix(zps): build against bundled NimBLE host stack after pioarduino migration 2026-06-03 14:48:34 +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
Thomas Göttgens 5e6dc43a59 Merge branch 'zps-module' of https://github.com/meshtastic/firmware into zps-module 2026-06-03 12:34:11 +02:00
Thomas GöttgensandGitHub 110bb1b858 Merge branch 'develop' into zps-module 2026-06-03 12:32:39 +02: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
Thomas Göttgens cc1eeb7eb9 Merge branch 'zps-module' of https://github.com/meshtastic/firmware into zps-module 2026-04-12 18:01:43 +02:00
Thomas GöttgensandGitHub 8b9e06b05f Merge branch 'develop' into zps-module 2026-04-12 17:19:30 +02:00
Thomas Göttgens feed29eaf5 Merge branch 'zps-module' of https://github.com/meshtastic/firmware into zps-module 2026-02-04 14:04:41 +01:00
Thomas GöttgensandGitHub b0060b61fe Merge branch 'develop' into zps-module 2026-02-04 14:03:29 +01:00
Thomas Göttgens c0a1b2058b Merge branch 'zps-module' of https://github.com/meshtastic/firmware into zps-module 2026-02-04 14:02:14 +01:00
Thomas GöttgensandTom Fifield a7435b85a1 trunk fmt 2025-08-28 13:50:22 +10:00
Thomas GöttgensandTom Fifield 729d6c576f everyone's a critic, especially copilot. 2025-08-28 13:50:22 +10:00
Thomas GöttgensandTom Fifield 15b84fca01 Fix compile and check issues 2025-08-28 13:50:22 +10:00
Thomas GöttgensandTom Fifield 285b30dff0 the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS - work in progress, adapted to 2.x firmware convention, disabled by default 2025-08-28 13:50:22 +10:00
Thomas Göttgens 0258057adf trunk fmt 2025-08-19 17:44:25 +02:00
Thomas Göttgens aeb14a697d everyone's a critic, especially copilot. 2025-08-19 15:52:27 +02:00
Thomas Göttgens b546de1661 Fix compile and check issues 2025-08-19 14:47:14 +02:00
Thomas Göttgens f65a708148 Merge branch 'zps-module' of https://github.com/meshtastic/firmware into zps-module 2025-08-19 13:13:26 +02:00
Thomas Göttgens 5c874945fa the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS - work in progress, adapted to 2.x firmware convention, disabled by default 2025-08-19 13:12:31 +02:00
Thomas Göttgens 493742308a the original ZPS module from https://github.com/a-f-G-U-C/Meshtastic-ZPS - work in progress, adapted to 2.x firmware convention, disabled by default 2025-08-17 13:40:53 +02:00
165 changed files with 7121 additions and 5783 deletions
+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
+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,
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import re
Import("env")
# The ZPS module scans BLE through the NimBLE *host* API ble_gap_disc(), which the
# ESP-IDF only compiles when CONFIG_BT_NIMBLE_ROLE_OBSERVER=y. The base esp32 config
# disables the observer role to save flash (see variants/esp32/esp32-common.ini), so
# re-enable it ONLY when the ZPS module is actually built. This keeps a single flag
# (-DMESHTASTIC_EXCLUDE_ZPS) driving both the C++ module and the IDF sdkconfig.
#
# This runs as a pre: script, before the arduino framework builder reads
# custom_sdkconfig (PlatformIO core runs pre-scripts before $BUILD_SCRIPT). Appending
# to custom_sdkconfig changes its hash and triggers the framework's IDF rebuild path,
# but only for ZPS-enabled envs; for every normal build this is a no-op.
flags = env.GetProjectOption("build_flags", "")
if isinstance(flags, (list, tuple)):
flags = " ".join(flags)
# Mirror the C semantics of `#if !MESHTASTIC_EXCLUDE_ZPS`:
# flag absent -> module enabled
# -DMESHTASTIC_EXCLUDE_ZPS=0 -> module enabled
# -DMESHTASTIC_EXCLUDE_ZPS or =1 (or anything else) -> module excluded
match = re.search(r"\bMESHTASTIC_EXCLUDE_ZPS\b(?:=(\S+))?", flags)
zps_enabled = (match is None) or (match.group(1) == "0")
section = "env:" + env["PIOENV"]
config = env.GetProjectConfig()
if zps_enabled and config.has_option(section, "custom_sdkconfig"):
sdkconfig = env.GetProjectOption("custom_sdkconfig")
if "CONFIG_BT_NIMBLE_ROLE_OBSERVER" not in sdkconfig:
config.set(
section,
"custom_sdkconfig",
sdkconfig.rstrip("\n") + "\n CONFIG_BT_NIMBLE_ROLE_OBSERVER=y\n",
)
print("[ZPS] module enabled -> CONFIG_BT_NIMBLE_ROLE_OBSERVER=y (IDF rebuild)")
+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)
+5 -6
View File
@@ -53,6 +53,7 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_ADSB=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_ZPS=1
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
@@ -186,12 +187,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 +209,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);
+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");
+10
View File
@@ -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
+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:
+9 -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"
@@ -816,16 +817,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.
---
+24 -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"
@@ -206,6 +210,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 +437,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 +682,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 +699,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 +784,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);
@@ -1220,7 +1243,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();
+5
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;
@@ -73,6 +74,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;
+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;
+11
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>
@@ -2538,6 +2541,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();
+41 -17
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
@@ -550,9 +580,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 +875,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
+10 -3
View File
@@ -658,7 +658,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 */
@@ -1533,8 +1540,8 @@ extern "C" {
#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
+6
View File
@@ -66,4 +66,10 @@ inline uint32_t pow_of_2(uint32_t n)
return 1 << n;
}
/// Returns true if n is a power of two (n >= 1).
template <typename T> constexpr bool is_pow_of_2(T n)
{
return n >= T(1) && (n & (n - T(1))) == T(0);
}
#define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__)
+2 -2
View File
@@ -824,7 +824,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
// Ensure initRegion() uses the newly validated region
config.lora.region = validatedLora.region;
initRegion();
if (myRegion->dutyCycle < 100) {
if (getEffectiveDutyCycle() < 100) {
validatedLora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
}
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
@@ -1013,11 +1013,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
case meshtastic_ModuleConfig_neighbor_info_tag:
LOG_INFO("Set module config: Neighbor Info");
moduleConfig.has_neighbor_info = true;
moduleConfig.neighbor_info = c.payload_variant.neighbor_info;
if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) {
LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs);
moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs;
}
moduleConfig.neighbor_info = c.payload_variant.neighbor_info;
break;
case meshtastic_ModuleConfig_detection_sensor_tag:
LOG_INFO("Set module config: Detection Sensor");
+5 -201
View File
@@ -1,207 +1,11 @@
#include "AtakPluginModule.h"
#include "Default.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
#include "main.h"
#include "mesh/compression/unishox2.h"
#include "meshUtils.h"
#include "meshtastic/atak.pb.h"
AtakPluginModule *atakPluginModule;
AtakPluginModule::AtakPluginModule()
: ProtobufModule("atak", meshtastic_PortNum_ATAK_PLUGIN, &meshtastic_TAKPacket_msg), concurrency::OSThread("AtakPlugin")
AtakPluginModule::AtakPluginModule() : SinglePortModule("atak", meshtastic_PortNum_ATAK_PLUGIN_V2) {}
ProcessMessage AtakPluginModule::handleReceived(const meshtastic_MeshPacket &mp)
{
ourPortNum = meshtastic_PortNum_ATAK_PLUGIN;
(void)mp; // Passthrough — no processing needed, apps handle compression/decompression
return ProcessMessage::CONTINUE;
}
/*
Encompasses the full construction and sending packet to mesh
Will be used for broadcast.
*/
int32_t AtakPluginModule::runOnce()
{
return default_broadcast_interval_secs;
}
bool AtakPluginModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *r)
{
return false;
}
meshtastic_TAKPacket AtakPluginModule::cloneTAKPacketData(meshtastic_TAKPacket *t)
{
meshtastic_TAKPacket clone = meshtastic_TAKPacket_init_zero;
if (t->has_group) {
clone.has_group = true;
clone.group = t->group;
}
if (t->has_status) {
clone.has_status = true;
clone.status = t->status;
}
if (t->has_contact) {
clone.has_contact = true;
clone.contact = {0};
}
if (t->which_payload_variant == meshtastic_TAKPacket_pli_tag) {
clone.which_payload_variant = meshtastic_TAKPacket_pli_tag;
clone.payload_variant.pli = t->payload_variant.pli;
} else if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
clone.which_payload_variant = meshtastic_TAKPacket_chat_tag;
clone.payload_variant.chat = {0};
} else if (t->which_payload_variant == meshtastic_TAKPacket_detail_tag) {
clone.which_payload_variant = meshtastic_TAKPacket_detail_tag;
clone.payload_variant.detail.size = t->payload_variant.detail.size;
memcpy(clone.payload_variant.detail.bytes, t->payload_variant.detail.bytes, t->payload_variant.detail.size);
}
return clone;
}
void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t)
{
// From Phone (EUD)
if (mp.from == 0) {
LOG_DEBUG("Received uncompressed TAK payload from phone: %d bytes", mp.decoded.payload.size);
// Compress for LoRA transport
auto compressed = cloneTAKPacketData(t);
compressed.is_compressed = true;
if (t->has_contact) {
auto length = unishox2_compress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed callsign: %d bytes", length);
length = unishox2_compress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed device_callsign: %d bytes", length);
}
if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_compress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed chat message: %d bytes", length);
if (t->payload_variant.chat.has_to) {
compressed.payload_variant.chat.has_to = true;
length = unishox2_compress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed chat to: %d bytes", length);
}
if (t->payload_variant.chat.has_to_callsign) {
compressed.payload_variant.chat.has_to_callsign = true;
length = unishox2_compress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet");
return;
}
LOG_DEBUG("Compressed chat to_callsign: %d bytes", length);
}
}
mp.decoded.payload.size = pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes),
meshtastic_TAKPacket_fields, &compressed);
LOG_DEBUG("Final payload: %d bytes", mp.decoded.payload.size);
} else {
if (!t->is_compressed) {
// Not compressed. Something is wrong
LOG_WARN("Received uncompressed TAKPacket over radio! Skip");
return;
}
// Decompress for Phone (EUD)
auto uncompressed = cloneTAKPacketData(t);
uncompressed.is_compressed = false;
if (t->has_contact) {
auto length = unishox2_decompress_lines(
t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)),
uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed callsign: %d bytes", length);
length = unishox2_decompress_lines(
t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)),
uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow contact.device_callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed device_callsign: %d bytes", length);
}
if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) {
auto length = unishox2_decompress_lines(
t->payload_variant.chat.message,
pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)),
uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT,
NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.message. Bailing out");
return;
}
LOG_DEBUG("Decompressed chat message: %d bytes", length);
if (t->payload_variant.chat.has_to) {
uncompressed.payload_variant.chat.has_to = true;
length = unishox2_decompress_lines(
t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)),
uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to. Bailing out");
return;
}
LOG_DEBUG("Decompressed chat to: %d bytes", length);
}
if (t->payload_variant.chat.has_to_callsign) {
uncompressed.payload_variant.chat.has_to_callsign = true;
length = unishox2_decompress_lines(
t->payload_variant.chat.to_callsign,
pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)),
uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1,
USX_PSET_DFLT, NULL);
if (length < 0) {
LOG_WARN("Decompress overflow chat.to_callsign. Bailing out");
return;
}
LOG_DEBUG("Decompressed chat to_callsign: %d bytes", length);
}
}
auto decompressedCopy = packetPool.allocCopy(mp);
decompressedCopy->decoded.payload.size =
pb_encode_to_bytes(decompressedCopy->decoded.payload.bytes, sizeof(decompressedCopy->decoded.payload),
meshtastic_TAKPacket_fields, &uncompressed);
service->sendToPhone(decompressedCopy);
}
return;
}
+10 -12
View File
@@ -1,11 +1,15 @@
#pragma once
#include "ProtobufModule.h"
#include "meshtastic/atak.pb.h"
#include "SinglePortModule.h"
/**
* Waypoint message handling for meshtastic
* ATAK Plugin V2 module - passthrough for ATAK_PLUGIN_V2 payloads.
* The wire format includes a leading flags byte followed by opaque payload bytes.
* Depending on the flags, the payload may be zstd dictionary-compressed or raw/uncompressed protobuf.
* Compression/decompression and payload interpretation are handled by the apps
* (Android, iOS, ATAK plugin); firmware forwards the bytes unchanged on the
* ATAK_PLUGIN_V2 port.
*/
class AtakPluginModule : public ProtobufModule<meshtastic_TAKPacket>, private concurrency::OSThread
class AtakPluginModule : public SinglePortModule
{
public:
/** Constructor
@@ -14,13 +18,7 @@ class AtakPluginModule : public ProtobufModule<meshtastic_TAKPacket>, private co
AtakPluginModule();
protected:
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override;
virtual void alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_TAKPacket *t) override;
/* Does our periodic broadcast */
int32_t runOnce() override;
private:
meshtastic_TAKPacket cloneTAKPacketData(meshtastic_TAKPacket *t);
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
};
extern AtakPluginModule *atakPluginModule;
extern AtakPluginModule *atakPluginModule;
+9 -8
View File
@@ -6,6 +6,7 @@
#include "CannedMessageModule.h"
#include "Channels.h"
#include "FSCommon.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
@@ -2103,16 +2104,16 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset)
{
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;
+493
View File
@@ -0,0 +1,493 @@
#include "HopScalingModule.h"
#include "SafeFile.h"
#include "meshUtils.h"
#if HAS_VARIABLE_HOPS
#include "FSCommon.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "concurrency/LockGuard.h"
#include "mesh-pb-constants.h"
#include <algorithm>
#include <cmath>
#include <cstring>
namespace
{
// Module scheduling
constexpr uint32_t INITIAL_DELAY_MS = 30 * 1000UL; // Startup grace period before first run
constexpr uint32_t RUN_INTERVAL_MS = 5 * 60 * 1000UL; // Emit micro-summary every 5 minutes
// RUNS_PER_HOUR is a public class constant in HopScalingModule.h
// Persistence
// Note: this only needs incrementing if the published arrangement changes. For testing purposes, or prior to widespread release,
// it can stay the same even if the internal layout changes.
constexpr uint32_t HISTOGRAM_STATE_MAGIC = 0x48535432; // 'HST2' — layout v2
constexpr uint8_t HISTOGRAM_STATE_VERSION = 1;
constexpr const char *HISTOGRAM_STATE_FILE = "/prefs/hopScalingState.bin";
#pragma pack(push, 1)
struct PersistedHistogram {
uint32_t magic;
uint8_t version;
uint8_t samplingDenominator;
uint8_t filteringDenominator;
uint8_t filterDenomHoldRollsRemaining; // rollHour() calls remaining in the hold; 0 when expired/not active
uint16_t hashSeed;
Record entries[HopScalingModule::CAPACITY]; // full 512-byte array; count derived on load
};
#pragma pack(pop)
} // namespace
HopScalingModule *hopScalingModule;
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
HopScalingModule::HopScalingModule() : concurrency::OSThread("HopScaling")
{
clear();
loadFromDisk();
setIntervalFromNow(INITIAL_DELAY_MS);
}
void HopScalingModule::clear()
{
memset(entries, 0, sizeof(entries));
count = 0;
samplingDenominator = DENOM_MIN;
filteringDenominator = DENOM_MIN;
filteringDenomHoldRollsRemaining = 0;
lastPerHopCounts = {};
lastSuggestedHop = MAX_HOP;
lastPoliteNumer = POLITENESS_DEFAULT;
lastTrendStats = {};
memset(denominatorHistory, DENOM_MIN, sizeof(denominatorHistory));
#ifndef PIO_UNIT_TESTING
hashSeed = static_cast<uint16_t>(random());
#else
hashSeed = 0; // deterministic in unit tests
#endif
}
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
void HopScalingModule::saveToDisk() const
{
#ifdef FSCom
FSCom.mkdir("/prefs");
PersistedHistogram state{};
state.magic = HISTOGRAM_STATE_MAGIC;
state.version = HISTOGRAM_STATE_VERSION;
state.samplingDenominator = samplingDenominator;
state.filteringDenominator = filteringDenominator;
state.filterDenomHoldRollsRemaining = filteringDenomHoldRollsRemaining;
state.hashSeed = hashSeed;
// Save all CAPACITY slots; count is reconstructed on load by scanning seenHoursAgo.
memcpy(state.entries, entries, sizeof(state.entries));
auto file = SafeFile(HISTOGRAM_STATE_FILE, true);
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&state), sizeof(state));
if (file.close() && written == sizeof(state)) {
LOG_DEBUG("[HOPSCALE] Saved: count=%u samp=1/%u filt=1/%u holdRollsRemaining=%u", count, samplingDenominator,
filteringDenominator, state.filterDenomHoldRollsRemaining);
} else {
LOG_WARN("[HOPSCALE] Failed to write %s (%u of %u bytes)", HISTOGRAM_STATE_FILE, static_cast<unsigned>(written),
static_cast<unsigned>(sizeof(state)));
}
#endif
}
void HopScalingModule::loadFromDisk()
{
#ifdef FSCom
concurrency::LockGuard g(spiLock);
auto file = FSCom.open(HISTOGRAM_STATE_FILE, FILE_O_READ);
if (!file)
return;
PersistedHistogram state{};
const bool readOk = (file.read(reinterpret_cast<uint8_t *>(&state), sizeof(state)) == sizeof(state));
file.close();
// Validate magic, version, denom range, denom power-of-two invariant, and hold counter.
if (!readOk || state.magic != HISTOGRAM_STATE_MAGIC || state.version != HISTOGRAM_STATE_VERSION ||
state.samplingDenominator < DENOM_MIN || state.samplingDenominator > DENOM_MAX ||
state.filteringDenominator < state.samplingDenominator || state.filteringDenominator > DENOM_MAX ||
!is_pow_of_2(state.samplingDenominator) || !is_pow_of_2(state.filteringDenominator) ||
state.filterDenomHoldRollsRemaining > FILTER_DENOM_HOLD_ROLLS) {
LOG_DEBUG("[HOPSCALE] No valid persisted state (magic=%08x ver=%u samp=%u filt=%u hold=%u), starting fresh", state.magic,
state.version, state.samplingDenominator, state.filteringDenominator, state.filterDenomHoldRollsRemaining);
return;
}
// Derive count by scanning: active entries have seenHoursAgo != 0; pack them to the front.
uint8_t restored = 0;
for (uint8_t i = 0; i < CAPACITY && restored < CAPACITY; i++) {
if (state.entries[i].seenHoursAgo != 0u) {
entries[restored++] = state.entries[i];
}
}
count = restored;
samplingDenominator = state.samplingDenominator;
filteringDenominator = state.filteringDenominator;
filteringDenomHoldRollsRemaining = state.filterDenomHoldRollsRemaining;
// denominatorHistory can't be recovered; initialise all slots to filteringDenominator so
// the first few post-reboot scaledPerHour values use a safe (slightly conservative) multiplier.
memset(denominatorHistory, filteringDenominator, sizeof(denominatorHistory));
hashSeed = state.hashSeed;
LOG_INFO("[HOPSCALE] Restored: count=%u samp=1/%u filt=1/%u holdRollsRemaining=%u", count, samplingDenominator,
filteringDenominator, state.filterDenomHoldRollsRemaining);
#endif
}
// ---------------------------------------------------------------------------
// Core API
// ---------------------------------------------------------------------------
void HopScalingModule::samplePacketForHistogram(uint32_t nodeId, uint8_t hopCount)
{
const uint16_t hash = hashNodeId(nodeId);
if (!passesFilter(hash, samplingDenominator))
return;
hopCount = std::min(hopCount, MAX_HOP);
// Update an existing entry
Record *entry = nullptr;
for (uint8_t i = 0; i < count; i++) {
if (entries[i].nodeHash == hash) {
entry = &entries[i];
break;
}
}
if (entry) {
entry->hops_away = hopCount;
markCurrentHour(*entry);
return;
}
// New node: trim if necessary before allocating a slot
if (getFillPercentage() >= FILL_HIGH_PCT) {
trimIfNeeded();
}
if (count < CAPACITY) {
entries[count].nodeHash = hash;
entries[count].hops_away = hopCount;
entries[count].seenHoursAgo = 1u; // mark current hour
count++;
} else {
LOG_WARN("[HOPSCALE] Histogram full at samp=1/%u (DENOM_MAX=%u); dropping node hash=0x%04x; hop recommendation may be "
"skewed!!!",
samplingDenominator, DENOM_MAX, hash);
}
}
void HopScalingModule::rollHour()
{
// Advance denominatorHistory before the tally so each slot h holds the filteringDenominator
// that was active when seenHoursAgo bit h was set. hourlyRaw[h] is then gated per-slot by
// denominatorHistory[h], giving a correct population estimate for each historical hour even
// when filteringDenominator changes between rolls. Scale-up backfills the entire array so
// the invariant holds retroactively (see trimIfNeeded()).
for (uint8_t h = 12; h > 0; h--)
denominatorHistory[h] = denominatorHistory[h - 1];
denominatorHistory[0] = filteringDenominator;
// 1. Tally per-hop counts and per-slot hourly activity in one pass.
// hourlyRaw[h]: gated per-slot by denominatorHistory[h] so the raw count and its
// multiplier are always consistent, even across filteringDenominator transitions.
// counts.*: gated uniformly by the current filteringDenominator for a consistent
// population estimate used by the hop-walk recommendation (step 2).
PerHopCounts counts{};
uint16_t hourlyRaw[13] = {};
uint16_t trendNewThisHour = 0;
uint16_t trendReturning = 0;
uint16_t trendLapsed = 0;
uint16_t trendOlderThan4h = 0;
uint16_t trendAgingOut = 0;
for (uint8_t i = 0; i < count; i++) {
const uint16_t hash = entries[i].nodeHash;
const uint32_t seen = entries[i].seenHoursAgo;
// Per-slot hourly activity: gate each slot by its own denominator.
for (uint8_t h = 0; h < 13; h++) {
if ((seen & (1u << h)) && passesFilter(hash, denominatorHistory[h]))
hourlyRaw[h]++;
}
// Hop counts and trend stats: uniform current-denominator gate.
if (!passesFilter(hash, filteringDenominator))
continue;
if (seenInLast13h(entries[i])) {
counts.perHop[entries[i].hops_away]++;
counts.total++;
}
const bool heardThisHour = (seen & 1u) != 0u;
const bool heardLastHour = (seen & 2u) != 0u;
const bool hasOlderHistory = (seen >> 1u) != 0u;
const bool recentlySilent = (seen & 0xFu) == 0u;
if (heardThisHour && !hasOlderHistory)
trendNewThisHour++;
else if (heardThisHour && hasOlderHistory)
trendReturning++;
if (!heardThisHour && heardLastHour)
trendLapsed++;
if (recentlySilent && (seen & 0x1FF0u) != 0u)
trendOlderThan4h++;
if (seen == (1u << 12u))
trendAgingOut++;
}
lastPerHopCounts = counts;
// 1b. Compute politeness factor from the 0-2 h vs 1-3 h activity ratio.
{
const uint32_t recent = static_cast<uint32_t>(hourlyRaw[0]) + hourlyRaw[1];
const uint32_t older = static_cast<uint32_t>(hourlyRaw[1]) + hourlyRaw[2];
if (older > 1 && recent > 1) {
const uint32_t r = static_cast<uint32_t>(recent) * ACTIVITY_WEIGHT_SCALE;
const uint32_t o = static_cast<uint32_t>(older);
if (r < o * ACTIVITY_WEIGHT_GENEROUS_MAX_NUMER)
lastPoliteNumer = POLITENESS_GENEROUS;
else if (r > o * ACTIVITY_WEIGHT_STRICT_MIN_NUMER)
lastPoliteNumer = POLITENESS_STRICT;
else
lastPoliteNumer = POLITENESS_DEFAULT;
} else {
lastPoliteNumer = POLITENESS_DEFAULT;
}
}
// 1c. Scale and cache trend stats (denominatorHistory already advanced above).
{
MeshTrendStats t{};
for (uint8_t h = 0; h < 13; h++) {
const uint32_t s = static_cast<uint32_t>(hourlyRaw[h]) * denominatorHistory[h];
t.scaledPerHour[h] = static_cast<uint16_t>(std::min<uint32_t>(s, UINT16_MAX));
}
auto scale = [&](uint16_t raw) -> uint16_t {
return static_cast<uint16_t>(std::min<uint32_t>(static_cast<uint32_t>(raw) * filteringDenominator, UINT16_MAX));
};
t.newThisHour = scale(trendNewThisHour);
t.returningThisHour = scale(trendReturning);
t.lapsedSinceLastHour = scale(trendLapsed);
t.olderThan4h = scale(trendOlderThan4h);
t.agingOut = scale(trendAgingOut);
lastTrendStats = t;
}
// 2. Walk scaled hop buckets to produce a hop-limit recommendation.
// effectiveMin: walk threshold — first hop whose cumulative count reaches this.
// effectiveMax: ceiling on the one-hop extension check with GENEROUS politeness.
const uint16_t effectiveMin = TARGET_AFFECTED_NODES;
const uint16_t effectiveMax = MAX_TARGET_NODES;
uint8_t suggested = MAX_HOP;
if (counts.total > 0) {
uint32_t cumulative = 0;
for (uint8_t hop = 0; hop <= MAX_HOP; hop++) {
cumulative += static_cast<uint32_t>(counts.perHop[hop]) * filteringDenominator;
if (cumulative >= effectiveMin) {
suggested = hop;
break;
}
}
if (suggested < MAX_HOP) {
const uint32_t atNext = static_cast<uint32_t>(counts.perHop[suggested + 1]) * filteringDenominator;
// politeLimit = effectiveMin + gap * politeNumer / POLITENESS_DENOM
// Multiply both sides by POLITENESS_DENOM to stay in integers.
const uint32_t gap = static_cast<uint32_t>(effectiveMax) - static_cast<uint32_t>(effectiveMin);
if ((cumulative + atNext) * POLITENESS_DENOM <=
static_cast<uint32_t>(effectiveMin) * POLITENESS_DENOM + gap * lastPoliteNumer) {
suggested++;
}
}
}
lastSuggestedHop = suggested;
// 3. Log scaled per-hop counts and recommendation.
{
uint16_t scaled[MAX_HOP + 1];
for (uint8_t h = 0; h <= MAX_HOP; h++) {
const uint32_t s = static_cast<uint32_t>(counts.perHop[h]) * filteringDenominator;
scaled[h] = static_cast<uint16_t>(std::min<uint32_t>(s, UINT16_MAX));
}
const uint32_t scaledTotal = static_cast<uint32_t>(counts.total) * filteringDenominator;
memcpy(lastScaledPerHop, scaled, sizeof(lastScaledPerHop));
LOG_INFO("[HOPSCALE] rollHour: entries=%u/128 samp=1/%u filt=1/%u counted=%u est=%u suggestedHop=%u polite=%u/4", count,
samplingDenominator, filteringDenominator, counts.total, static_cast<unsigned>(scaledTotal), suggested,
lastPoliteNumer);
const auto &ts = lastTrendStats;
LOG_INFO("[HOPSCALE] scaledSeenPerHour (h0=now): [%u %u %u %u %u %u %u %u %u %u %u %u %u]", ts.scaledPerHour[0],
ts.scaledPerHour[1], ts.scaledPerHour[2], ts.scaledPerHour[3], ts.scaledPerHour[4], ts.scaledPerHour[5],
ts.scaledPerHour[6], ts.scaledPerHour[7], ts.scaledPerHour[8], ts.scaledPerHour[9], ts.scaledPerHour[10],
ts.scaledPerHour[11], ts.scaledPerHour[12]);
LOG_INFO("[HOPSCALE] trend: new=%u returning=%u lapsed=%u olderThan4h=%u agingOut=%u", ts.newThisHour,
ts.returningThisHour, ts.lapsedSinceLastHour, ts.olderThan4h, ts.agingOut);
}
// 4. Scale-down check: if fewer than FILL_LOW_PCT% of capacity pass the filteringDenominator
// gate and are active, halve samplingDenominator to admit more nodes.
// Note: during a filteringDenominator hold period, lowering samplingDenominator does not
// immediately improve counts.total (new admissions don't pass the elevated
// filteringDenominator). On a genuinely quieting mesh this check can therefore fire on
// consecutive hours, cascading samplingDenominator toward DENOM_MIN. This is intentional:
// rapid re-admission allows quick recovery if the mesh returns. The hop recommendation
// stays conservative (MAX_HOP) throughout because filteringDenominator remains elevated;
// step 5 below re-synchronises the denominators once the hold expires.
if (counts.total * 100u < static_cast<uint32_t>(CAPACITY) * FILL_LOW_PCT) {
if (samplingDenominator > DENOM_MIN) {
samplingDenominator = static_cast<uint8_t>(samplingDenominator / 2u);
LOG_INFO("[HOPSCALE] Scale-down: sampling denom halved to %u (filter denom=%u)", samplingDenominator,
filteringDenominator);
}
}
// 5. Tick down the hold counter; once it reaches zero, halve filteringDenominator toward
// samplingDenominator once per rollHour() (= once per hour) rather than a single jump:
// avoids a sudden large change in the hop-walk count when samplingDenominator cascaded
// down significantly during the hold period. No new hold is placed on each step — the
// 13-roll hold already guaranteed that re-admitted nodes have full seenHoursAgo history;
// further pacing is provided naturally by the 1-step-per-hour rate. denominatorHistory
// is updated automatically by the shift at the top of rollHour(), so no backfill here.
if (filteringDenominator > samplingDenominator) {
if (filteringDenomHoldRollsRemaining > 0)
filteringDenomHoldRollsRemaining--;
if (filteringDenomHoldRollsRemaining == 0) {
const uint8_t stepped = static_cast<uint8_t>(filteringDenominator / 2u);
filteringDenominator = (stepped > samplingDenominator) ? stepped : samplingDenominator;
LOG_INFO("[HOPSCALE] Filter denom stepped to %u (samp=1/%u)", filteringDenominator, samplingDenominator);
}
}
// 6. Shift all seen bitmaps left by one slot (opens a fresh slot for the new hour).
for (uint8_t i = 0; i < count; i++) {
rollSeenBits(entries[i]);
}
if (histogramRollCount < 255)
histogramRollCount++;
saveToDisk();
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
void HopScalingModule::trimIfNeeded()
{
// Step 1: evict stale entries (not seen in any of the past 13 hours).
uint8_t newCount = 0;
for (uint8_t i = 0; i < count; i++) {
if (seenInLast13h(entries[i])) {
if (i != newCount) {
entries[newCount] = entries[i];
}
newCount++;
}
}
count = newCount;
// Step 2: if still too full, double the sampling denominator and remove non-matching entries.
if (getFillPercentage() >= FILL_HIGH_PCT && samplingDenominator < DENOM_MAX) {
samplingDenominator = static_cast<uint8_t>(
std::min<uint16_t>(static_cast<uint16_t>(samplingDenominator) * 2u, static_cast<uint16_t>(DENOM_MAX)));
filteringDenominator = std::max(filteringDenominator, samplingDenominator);
filteringDenomHoldRollsRemaining = FILTER_DENOM_HOLD_ROLLS;
// Raise any denominatorHistory slot that is below the new filteringDenominator.
// Slots already above it (recorded during a prior scale-up that hasn't fully stepped
// down yet) are left untouched: eviction at samplingDenominator retains exactly those
// entries, so the old higher gate remains accurate for those historical hours.
// Slots below the new value must be raised because the eviction removed entries that
// had been admitted at the looser old gate — the remaining entries represent a 1/N
// subsample where N is the new filteringDenominator, not the old smaller value.
for (uint8_t h = 0; h < 13; h++)
denominatorHistory[h] = std::max(denominatorHistory[h], filteringDenominator);
LOG_INFO("[HOPSCALE] Scale-up: samp denom doubled to %u (filt=%u)", samplingDenominator, filteringDenominator);
newCount = 0;
for (uint8_t i = 0; i < count; i++) {
if (passesFilter(entries[i].nodeHash, samplingDenominator)) {
if (i != newCount) {
entries[newCount] = entries[i];
}
newCount++;
}
}
count = newCount;
}
}
void HopScalingModule::logStatusReport(bool didHourlyUpdate) const
{
const bool histActive = (histogramRollCount > 0 && count > 0);
const auto &histCounts = lastPerHopCounts;
const uint8_t runsRemaining = didHourlyUpdate ? RUNS_PER_HOUR : (RUNS_PER_HOUR - runsSinceLastHourlyUpdate);
const uint8_t minsUntilRollover = runsRemaining * (RUN_INTERVAL_MS / (60 * 1000UL));
LOG_INFO("[HOPSCALE] hop=%u histActive=%u fill=%u%% samp=1/%u filt=1/%u entries=%u lastCounted=%u polite=%u/4 "
"nextRoll=%umin",
lastRequiredHop, histActive ? 1u : 0u, getFillPercentage(), samplingDenominator, filteringDenominator, count,
histCounts.total, lastPoliteNumer, minsUntilRollover);
LOG_INFO("[HOPSCALE] nodes perHop: [%u %u %u %u %u %u %u %u]", histCounts.perHop[0], histCounts.perHop[1],
histCounts.perHop[2], histCounts.perHop[3], histCounts.perHop[4], histCounts.perHop[5], histCounts.perHop[6],
histCounts.perHop[7]);
LOG_INFO("[HOPSCALE] last scaled perHop: [%u %u %u %u %u %u %u %u]", lastScaledPerHop[0], lastScaledPerHop[1],
lastScaledPerHop[2], lastScaledPerHop[3], lastScaledPerHop[4], lastScaledPerHop[5], lastScaledPerHop[6],
lastScaledPerHop[7]);
}
int32_t HopScalingModule::runOnce()
{
const bool isFirstRun = !hasCompletedInitialRun;
bool didHourlyUpdate = false;
if (isFirstRun) {
hasCompletedInitialRun = true;
runsSinceLastHourlyUpdate = 0;
didHourlyUpdate = true;
} else {
runsSinceLastHourlyUpdate++;
if (runsSinceLastHourlyUpdate >= RUNS_PER_HOUR) {
runsSinceLastHourlyUpdate = 0;
didHourlyUpdate = true;
}
}
if (didHourlyUpdate && !isFirstRun) {
rollHour();
}
if (didHourlyUpdate) {
uint8_t suggested = (histogramRollCount > 0 && count > 0) ? lastSuggestedHop : HOP_MAX;
// Role-based hop floor: TRACKER/TAK_TRACKER always reach at least 2 hops,
// SENSOR reaches at least 1, so these reporting roles remain reachable even
// on a dense mesh where the histogram recommends a lower hop count.
uint8_t roleFloor = 0;
switch (config.device.role) {
case meshtastic_Config_DeviceConfig_Role_TRACKER:
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
roleFloor = 2;
break;
case meshtastic_Config_DeviceConfig_Role_SENSOR:
roleFloor = 1;
break;
default:
break;
}
lastRequiredHop = std::max(suggested, roleFloor);
}
logStatusReport(didHourlyUpdate);
return RUN_INTERVAL_MS;
}
#endif
+351
View File
@@ -0,0 +1,351 @@
#pragma once
#include "MeshTypes.h"
#include "concurrency/OSThread.h"
#include "configuration.h"
#include "mesh/Default.h"
#include "mesh/mesh-pb-constants.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
#if HAS_VARIABLE_HOPS
/**
* HopScalingModule: Sampled hop-distance histogram for mesh-aware hop limit recommendations.
*
* Memory layout: 512 bytes total (128 entries × 4 bytes/entry, no padding)
* - 16-bit XOR-fold hash of node ID
* - 3-bit hops away (07)
* - 13-bit hourly seen bitmap
* All three fields are packed into a single 32-bit Record; sizeof(Record) == 4.
*
* Sampling:
* - A node is added only when passesFilter(hashNodeId(nodeId), samplingDenominator),
* i.e. (hash16(nodeId) & (samplingDenominator 1)) == 0 (hash-space subsample, not raw ID)
* - samplingDenominator starts at 1 (sample all), doubles when the list exceeds FILL_HIGH_PCT
* - filteringDenominator tracks samplingDenominator upward immediately but does not drop back
* down until FILTER_DENOM_HOLD_MS (13 h) have elapsed since the last scale-up
*
* Hourly rollover (rollHour()):
* - Summarises per-hop node counts for entries matching filteringDenominator and seen in the
* last 13 hours
* - Scales each hop bucket by filteringDenominator and walks the buckets to recommend a hop
* limit, matching the same algorithm used in HopScalingModule
* - Shifts the 13-bit seen bitmap left by one slot to open a fresh slot for the new hour;
* nodes not seen in 13 consecutive hours have all seen bits cleared (stale)
* - Checks for scale-down: if fewer than FILL_LOW_PCT of capacity pass filteringDenominator,
* samplingDenominator is halved (filteringDenominator is held until the 13-h lock expires)
*
* Thread-safety: all access is single-threaded via the main loop cooperative scheduler.
*/
struct Record {
uint32_t nodeHash : 16;
uint32_t hops_away : 3;
uint32_t seenHoursAgo : 13;
};
static_assert(sizeof(Record) == 4);
class HopScalingModule : private concurrency::OSThread
{
public:
// -----------------------------------------------------------------------
// Capacity and memory layout
// -----------------------------------------------------------------------
static constexpr size_t CAPACITY = 128;
static constexpr size_t ENTRY_BYTES = sizeof(Record);
static constexpr size_t TOTAL_BYTES = CAPACITY * ENTRY_BYTES;
// Denominator limits (must be powers of 2)
static constexpr uint8_t DENOM_MIN = 1;
static constexpr uint8_t DENOM_MAX = 128;
static constexpr uint8_t MAX_HOP = 7;
// Fill-level thresholds (percent of CAPACITY)
static constexpr uint8_t FILL_HIGH_PCT = 80;
static constexpr uint8_t FILL_LOW_PCT = 20;
// How long filteringDenominator is held at an elevated level before it may drop.
//
// This value is deliberately equal to the seenHoursAgo window (13 hours / 13 bits).
// Invariant: every entry that existed when a scale-up fired had seenHoursAgo != 0 at
// that moment (trimIfNeeded() evicts stale entries before doubling the denominator),
// so it remains seenInLast13h for at most 13 more rollHour() calls — exactly the
// hold duration. That means entries from the scale-up event keep counts.total above
// the scale-down threshold for the entire hold period under normal (active) mesh
// conditions. On a genuinely quieting mesh the scale-down CAN fire before the hold
// expires — each firing halves samplingDenominator but filteringDenominator stays
// elevated, so the hop recommendation correctly stays conservative (MAX_HOP) while
// the cascade runs. The cascade is bounded at DENOM_MIN (7 halvings from DENOM_MAX);
// when the hold finally expires, step 5 of rollHour() halves filteringDenominator
// once per hour (rather than jumping directly to samplingDenominator) until the two
// converge, giving the hop-walk a gradual, 1-step-per-hour descent.
static constexpr uint32_t FILTER_DENOM_HOLD_MS = 13UL * 60UL * 60UL * 1000UL; // 13 h (documentation only)
// Number of rollHour() calls the hold spans — equals the seenHoursAgo window width.
// filteringDenomHoldRollsRemaining is initialised to this value on scale-up and
// decremented once per rollHour(); step-down begins when it reaches zero.
static constexpr uint8_t FILTER_DENOM_HOLD_ROLLS = 13u;
// Hop-walk: target cumulative affected-node count when choosing a hop limit
static constexpr uint16_t TARGET_AFFECTED_NODES = default_hop_scaling_min_target_nodes;
// Clamp bounds enforced on min_target_nodes / max_target_nodes
static constexpr uint16_t MIN_TARGET_NODES_FLOOR = default_hop_scaling_min_target_nodes_floor;
static constexpr uint16_t MAX_TARGET_NODES_CEILING = default_hop_scaling_max_target_nodes_ceiling;
static constexpr uint16_t MAX_TARGET_NODES = default_hop_scaling_max_target_nodes;
// Politeness factors for the one-hop extension check in the hop walk.
// Stored as integer numerators over POLITENESS_DENOM (4):
// politeLimit = min + gap * politeNumer / POLITENESS_DENOM
// STRICT → min + 25% of gap; DEFAULT → midpoint; GENEROUS → max
static constexpr uint8_t POLITENESS_DENOM = 4u;
static constexpr uint8_t POLITENESS_GENEROUS = 4u; // 4/4 = 1.00
static constexpr uint8_t POLITENESS_DEFAULT = 2u; // 2/4 = 0.50
static constexpr uint8_t POLITENESS_STRICT = 1u; // 1/4 = 0.25
// Activity weight thresholds (ratio of 0-2 h window vs 1-3 h window).
// Cross-multiply form: recent * ACTIVITY_WEIGHT_SCALE vs older * threshold_numer.
// GENEROUS if recent*10 < older*9 (ratio < 0.9); STRICT if recent*10 > older*12 (ratio > 1.2)
static constexpr uint8_t ACTIVITY_WEIGHT_SCALE = 10u;
static constexpr uint8_t ACTIVITY_WEIGHT_GENEROUS_MAX_NUMER = 9u;
static constexpr uint8_t ACTIVITY_WEIGHT_STRICT_MIN_NUMER = 12u;
// Scheduling: number of 5-minute runOnce() ticks that make up one hourly rollover
static constexpr uint8_t RUNS_PER_HOUR = 12;
// -----------------------------------------------------------------------
// Types
// -----------------------------------------------------------------------
/// Per-hop node counts produced at each hourly rollover.
struct PerHopCounts {
uint16_t perHop[MAX_HOP + 1] = {};
uint16_t total = 0;
};
/// Mesh activity trend stats produced at each hourly rollover.
/// All counts are scaled by filteringDenominator (i.e. estimated full-mesh population).
///
/// Bitmap interpretation (before the hourly shift): bit 0 = just-completed hour, bit 12 = 12 h ago.
struct MeshTrendStats {
/// Estimated node count per hour slot (h=0 is the just-completed hour, h=12 is 12 h ago).
uint16_t scaledPerHour[13] = {};
/// Nodes heard only this hour with no prior bitmap history — indicates new arrivals.
uint16_t newThisHour = 0;
/// Nodes heard this hour that also appeared in at least one older hour — stable regulars.
uint16_t returningThisHour = 0;
/// Nodes heard last hour but silent this hour — potential departures.
uint16_t lapsedSinceLastHour = 0;
/// Nodes absent from the last 4 hours but still present in some older hour (513 h) — quieting down.
uint16_t olderThan4h = 0;
/// Nodes whose only remaining history is the 13th hour (bit 12 only) — about to age out entirely.
uint16_t agingOut = 0;
};
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
HopScalingModule();
~HopScalingModule() = default;
/// Reset all entries and state.
void clear();
// -----------------------------------------------------------------------
// Core API
// -----------------------------------------------------------------------
/// Record a received packet.
/// Adds or updates an entry when passesFilter(hashNodeId(nodeId), samplingDenominator),
/// i.e. when the 16-bit XOR-fold hash of the node ID falls in the 1/samplingDenominator
/// subsample of the hash space. This is NOT a raw nodeId modulo check.
/// Marks the current hour as seen and updates the stored hop count to the last observed value.
/// Triggers a trim pass if the list exceeds FILL_HIGH_PCT after the insertion.
void samplePacketForHistogram(uint32_t nodeId, uint8_t hopCount);
// -----------------------------------------------------------------------
// Accessors
// -----------------------------------------------------------------------
uint8_t getLastRequiredHop() const { return lastRequiredHop; }
uint8_t getEntryCount() const { return count; }
uint8_t getFillPercentage() const { return static_cast<uint8_t>((static_cast<uint16_t>(count) * 100u) / CAPACITY); }
uint8_t getSamplingDenominator() const { return samplingDenominator; }
uint8_t getFilteringDenominator() const { return filteringDenominator; }
float getPoliteness() const { return lastPoliteNumer / static_cast<float>(POLITENESS_DENOM); }
const PerHopCounts &getLastPerHopCounts() const { return lastPerHopCounts; }
uint8_t getLastSuggestedHop() const { return lastSuggestedHop; }
const MeshTrendStats &getLastTrendStats() const { return lastTrendStats; }
// Compatibility accessors used by tests
uint8_t getCompactHistogramEntryCount() const { return getEntryCount(); }
uint8_t getCompactHistogramDenominator() const { return getSamplingDenominator(); }
uint8_t getCompactHistogramFilterDenominator() const { return getFilteringDenominator(); }
uint8_t getCompactHistogramSuggestedHop() const { return getLastSuggestedHop(); }
size_t getCompactHistogramAllSampleCount() const { return getEntryCount(); }
/// Force both sampling and filtering denominators to a specific value.
/// Intended for unit tests that need a deterministic starting denominator.
void setSamplingDenominator(uint8_t d)
{
samplingDenominator = (d < DENOM_MIN) ? DENOM_MIN : (d > DENOM_MAX ? DENOM_MAX : d);
filteringDenominator = samplingDenominator;
filteringDenomHoldRollsRemaining = 0;
}
#ifdef PIO_UNIT_TESTING
// Writable from tests as HopScalingModule::s_testNowMs; drives nowMs() in PIO_UNIT_TESTING builds.
inline static uint32_t s_testNowMs = 0;
/// Override the per-session hash seed. Use in tests that need a specific sampling distribution.
void setHashSeed(uint16_t seed) { hashSeed = seed; }
uint16_t getHashSeed() const { return hashSeed; }
/// Expose hashNodeId for tests that need to compute which node IDs pass a given denominator.
uint16_t hashNodeIdPublic(uint32_t nodeId) const { return hashNodeId(nodeId); }
#endif
protected:
int32_t runOnce() override;
private:
#ifdef PIO_UNIT_TESTING
friend class HopScalingTestShim;
#endif
/// Perform hourly rollover.
/// 1. Tallies per-hop counts for entries matching filteringDenominator and seen in 13 h.
/// 2. Walks the scaled hop buckets and returns the recommended hop limit.
/// 3. Logs scaled per-hop counts and recommendation.
/// 4. Checks for scale-down (< FILL_LOW_PCT of capacity pass filteringDenominator).
/// 5. Decrements filteringDenomHoldRollsRemaining (if > 0); once it reaches zero, halves
/// filteringDenominator once toward samplingDenominator per rollHour() call.
/// 6. Shifts all seen bitmaps left by one hour slot.
void rollHour();
// -----------------------------------------------------------------------
// Persistence
// -----------------------------------------------------------------------
/// Persist the histogram state (entries, denominators, hold-timer) to flash.
/// No-op on platforms without a filesystem. Performs a full delete-and-rewrite of
/// the state file on each call; avoid calling more frequently than once per rollHour().
void saveToDisk() const;
/// Restore histogram state from flash. Safe to call even when no file exists.
/// Call once after construction, before the first rollHour(), to warm-start the
/// histogram across reboots without waiting 13 hours for data to re-accumulate.
/// The restored entries are available immediately for sampling, but the first
/// rollHour() (triggered by the second runOnce() tick) is needed before a warm-start
/// recommendation replaces the HOP_MAX boot default.
void loadFromDisk();
/// Remove stale entries (seen-bits all zero) and, if the list is still crowded,
/// double samplingDenominator and filteringDenominator and remove non-matching entries.
void trimIfNeeded();
void logStatusReport(bool didHourlyUpdate) const;
// -----------------------------------------------------------------------
// Histogram storage
// -----------------------------------------------------------------------
Record entries[CAPACITY] = {};
uint8_t count = 0;
// -----------------------------------------------------------------------
// Denominator state
//
// Two separate denominators control two distinct gates:
//
// samplingDenominator — admission gate. A node is added/updated only when
// passesFilter(hash, samplingDenominator). Lower value = more permissive =
// more nodes enter = represents recent mesh state.
//
// filteringDenominator — counting gate. The hop-walk tally in rollHour() only
// counts entries that pass passesFilter(hash, filteringDenominator). It moves
// up with samplingDenominator immediately (scale-up) but is held at the
// elevated value for FILTER_DENOM_HOLD_MS (13 h) after any scale-up before it
// may drop back down (scale-down).
//
// Why the estimate is invariant: passesFilter uses a hash-based uniform subsample.
// For any two powers-of-two denominators D ≤ F, the fraction of D-sampled entries
// that also pass F is exactly D/F. Therefore:
// raw_count × F = (total × D/F) × F = total × D
// The population estimate is the same whether we count with D or with F.
// The hold period is not about accuracy — it is about stability: it prevents the
// hop recommendation from reacting to recently-admitted nodes that have not yet
// accumulated enough seenHoursAgo history to be statistically reliable.
//
// denominatorHistory[h] — the filteringDenominator used to both gate and scale
// hourlyRaw[h]. Invariant: denominatorHistory[h] always equals the
// filteringDenominator that was active when seenHoursAgo bit h was set.
// rollHour() advances the array at the very start (before the tally loop), then
// gates hourlyRaw[h] per-slot by denominatorHistory[h] — each slot's raw count
// and multiplier are therefore always consistent, even when filteringDenominator
// changes between rolls (e.g. hold expiry). On scale-up (trimIfNeeded()), the
// entire array is backfilled uniformly with the new filteringDenominator to
// preserve the invariant retroactively for all 13 slots. Initialised to
// DENOM_MIN (1); scaledPerHour slots that draw from a 1 entry are unscaled —
// correct for a fresh instance with no prior history.
// -----------------------------------------------------------------------
uint8_t samplingDenominator = DENOM_MIN;
uint8_t filteringDenominator = DENOM_MIN;
uint8_t filteringDenomHoldRollsRemaining = 0; // counts down from FILTER_DENOM_HOLD_ROLLS to 0; step-down fires at 0
uint8_t denominatorHistory[13] = {};
uint16_t hashSeed = 0;
// -----------------------------------------------------------------------
// Cached hourly results
// -----------------------------------------------------------------------
PerHopCounts lastPerHopCounts = {};
uint16_t lastScaledPerHop[MAX_HOP + 1] = {};
uint8_t lastSuggestedHop = MAX_HOP;
uint8_t lastPoliteNumer = POLITENESS_DEFAULT;
MeshTrendStats lastTrendStats = {};
// -----------------------------------------------------------------------
// Hop recommendation state
// -----------------------------------------------------------------------
uint8_t lastRequiredHop = HOP_MAX;
uint8_t histogramRollCount = 0;
// -----------------------------------------------------------------------
// Scheduler state
// -----------------------------------------------------------------------
bool hasCompletedInitialRun = false;
uint8_t runsSinceLastHourlyUpdate = 0;
// -----------------------------------------------------------------------
// Inline record helpers
// -----------------------------------------------------------------------
// Record field semantics:
// nodeHash → XOR-fold of full 32-bit node ID to 16 bits
// hops_away → hop distance (07)
// seenHoursAgo → 13-bit per-hour seen bitmap
// bit 0 = seen in the current / most-recent hour
// bit 12 = seen 12 hours ago
// Shifts left on each rollHour(); 0 means not seen in 13 h.
/// XOR-fold + golden-ratio hash of a 32-bit node ID to 16 bits, mixed with the session seed.
/// Multiplying by floor(2^32 / φ) gives uniform avalanche; XORing the seed ensures different
/// devices (or the same device after a clear()) sample a different subset of node IDs.
/// For seed=0 the function is deterministic, which is used in PIO_UNIT_TESTING builds.
uint16_t hashNodeId(uint32_t nodeId) const { return static_cast<uint16_t>((nodeId * 2654435761u) >> 16) ^ hashSeed; }
static bool seenInLast13h(const Record &r) { return r.seenHoursAgo != 0u; }
static void markCurrentHour(Record &r) { r.seenHoursAgo |= 1u; }
static void rollSeenBits(Record &r) { r.seenHoursAgo = (r.seenHoursAgo << 1u) & 0x1FFFu; }
static bool passesFilter(uint16_t nodeHash, uint8_t denom) { return (nodeHash & static_cast<uint16_t>(denom - 1u)) == 0u; }
public:
// Clock — public so tests can share the same timebase via HopScalingModule::s_testNowMs
#ifdef PIO_UNIT_TESTING
static uint32_t nowMs() { return s_testNowMs; }
#else
static uint32_t nowMs() { return millis(); }
#endif
};
extern HopScalingModule *hopScalingModule;
#endif

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