From 068f5af4d85cba179728355fc05c7c78136f4a92 Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 27 Mar 2026 12:38:49 -0400 Subject: [PATCH 1/3] Add meshtasticd config metadata (#10001) To be consumed by config-picker utils, and hopefully meshtasticd itself in the future. --- .../OpenWRT/BananaPi-BPI-R4-sx1262.yaml | 6 +++ .../OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml | 6 +++ .../OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml | 6 +++ bin/config.d/README.md | 28 ++++++++++++ bin/config.d/display-waveshare-1-44.yaml | 6 +++ bin/config.d/display-waveshare-2.8.yaml | 6 +++ bin/config.d/lora-Adafruit-RFM9x.yaml | 6 +++ bin/config.d/lora-MeshAdv-900M30S.yaml | 6 +++ bin/config.d/lora-MeshAdv-Mini-900M22S.yaml | 6 +++ bin/config.d/lora-RAK6421-13300-slot1.yaml | 8 +++- bin/config.d/lora-RAK6421-13300-slot2.yaml | 8 +++- bin/config.d/lora-RAK6421-13302-slot1.yaml | 8 +++- bin/config.d/lora-RAK6421-13302-slot2.yaml | 8 +++- ...XO.yaml => lora-femtofox_LR1121_TCXO.yaml} | 42 ++++++++--------- ...XO.yaml => lora-femtofox_SX1262_TCXO.yaml} | 45 ++++++++++--------- ...AL.yaml => lora-femtofox_SX1262_XTAL.yaml} | 45 ++++++++++--------- bin/config.d/lora-hat-rak-6421-pi-hat.yaml | 8 +++- .../lora-lyra-picocalc-wio-sx1262.yaml | 6 +++ bin/config.d/lora-lyra-ultra_1w.yaml | 7 +++ bin/config.d/lora-lyra-ultra_2w.yaml | 7 +++ .../lora-lyra-ws-raspberry-pi-pico-hat.yaml | 6 +++ bin/config.d/lora-meshstick-1262.yaml | 6 +++ bin/config.d/lora-piggystick-lr1121.yaml | 6 +++ bin/config.d/lora-pinedio-usb-sx1262.yaml | 8 +++- ...lora-raxda-rock2f-starter-edition-hat.yaml | 6 +++ .../lora-starter-edition-sx1262-i2c.yaml | 6 +++ bin/config.d/lora-usb-meshstick-1262.yaml | 6 +++ bin/config.d/lora-usb-meshtoad-e22.yaml | 6 +++ bin/config.d/lora-usb-umesh-1262-30dbm.yaml | 6 +++ bin/config.d/lora-usb-umesh-1268-30dbm.yaml | 6 +++ bin/config.d/lora-waveshare-sxxx.yaml | 6 +++ ...a-ws-raspberry-pi-pico-to-rpi-adapter.yaml | 7 +++ ...lora-ws-raspberry-pico-to-orangepi-03.yaml | 6 +++ 33 files changed, 281 insertions(+), 68 deletions(-) create mode 100644 bin/config.d/README.md rename bin/config.d/{femtofox/femtofox_LR1121_TCXO.yaml => lora-femtofox_LR1121_TCXO.yaml} (75%) rename bin/config.d/{femtofox/femtofox_SX1262_TCXO.yaml => lora-femtofox_SX1262_TCXO.yaml} (87%) rename bin/config.d/{femtofox/femtofox_SX1262_XTAL.yaml => lora-femtofox_SX1262_XTAL.yaml} (86%) diff --git a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml index 825ab2699..a854dff5b 100644 --- a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml +++ b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: BananaPi-BPI-R4-sx1262 + support: community + compatible: + - bananapi_bpi-r4 # OpenWrt target + Lora: Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header ## CS: 28 diff --git a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml index ca5b27ebc..7687e0f58 100644 --- a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml +++ b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml @@ -1,4 +1,10 @@ ## https://www.mikroe.com/lr-iot-click +Meta: + name: OpenWRT One mikroBUS LR-IOT-CLICK + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK # CS: 25 diff --git a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml index 6dc1e870d..ab0b62810 100644 --- a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml +++ b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: OpenWRT One mikroBUS sx1262 + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: sx1262 IRQ: 10 diff --git a/bin/config.d/README.md b/bin/config.d/README.md new file mode 100644 index 000000000..b199fb439 --- /dev/null +++ b/bin/config.d/README.md @@ -0,0 +1,28 @@ +# meshtasticd configuration files + +This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware. + +## Metadata structure + +Each configuration file includes a `Meta` section that provides information about the configuration. +This configuration is consumed by configuration-selection tools. + +```yaml +Meta: + name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration. + support: community # community, official, or deprecated; determined by Meshtastic Leads. + compatible: # A list of compatible products or platforms. + - raspberry-pi +``` +`name`: A unique identifier for the configuration, typically reflecting the hardware it supports. + +`support`: Indicates the level of support for this configuration. It can be one of the following: + +- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration. +- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration. +- `deprecated`: No longer recommended for deployment by Meshtastic. + +`compatible`: A list of compatible products or platforms that can use this configuration. +This will vary depending on the intended use case / platform. +Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value. +These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc. diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml index d37f6cf6a..e6b4f8271 100644 --- a/bin/config.d/display-waveshare-1-44.yaml +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 1.44inch LCD HAT + support: community + compatible: + - raspberry-pi + ### Waveshare 1.44inch LCD HAT Display: Panel: ST7735S diff --git a/bin/config.d/display-waveshare-2.8.yaml b/bin/config.d/display-waveshare-2.8.yaml index 2e28276d8..586d1107e 100644 --- a/bin/config.d/display-waveshare-2.8.yaml +++ b/bin/config.d/display-waveshare-2.8.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 2.8inch LCD HAT + support: community + compatible: + - raspberry-pi + Display: ### Waveshare 2.8inch RPi LCD diff --git a/bin/config.d/lora-Adafruit-RFM9x.yaml b/bin/config.d/lora-Adafruit-RFM9x.yaml index 20295dc72..1258af4f5 100644 --- a/bin/config.d/lora-Adafruit-RFM9x.yaml +++ b/bin/config.d/lora-Adafruit-RFM9x.yaml @@ -1,3 +1,9 @@ +Meta: + name: Adafruit RFM9x + support: deprecated + compatible: + - raspberry-pi + Lora: Module: RF95 # Adafruit RFM9x Reset: 25 diff --git a/bin/config.d/lora-MeshAdv-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 5c148bf68..c90391cb0 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -1,5 +1,11 @@ # MeshAdv-Pi E22-900M30S # https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 CS: 21 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index b47b5c996..d878bce1b 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -1,5 +1,11 @@ # MeshAdv Mini E22-900M22S # https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Ebyte E22-900M22S CS: 8 diff --git a/bin/config.d/lora-RAK6421-13300-slot1.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml index 628198887..a88544896 100644 --- a/bin/config.d/lora-RAK6421-13300-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml index f890f0467..40b0cea09 100644 --- a/bin/config.d/lora-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -1,5 +1,11 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13300 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 diff --git a/bin/config.d/lora-RAK6421-13302-slot1.yaml b/bin/config.d/lora-RAK6421-13302-slot1.yaml index 13747d4e7..85b934ce6 100644 --- a/bin/config.d/lora-RAK6421-13302-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13302-slot1.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13302 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-RAK6421-13302-slot2.yaml b/bin/config.d/lora-RAK6421-13302-slot2.yaml index 194172774..5aa23911f 100644 --- a/bin/config.d/lora-RAK6421-13302-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13302-slot2.yaml @@ -1,5 +1,11 @@ +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13302 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 diff --git a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml similarity index 75% rename from bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml rename to bin/config.d/lora-femtofox_LR1121_TCXO.yaml index 7aa860f61..10166fa35 100644 --- a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml +++ b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml @@ -1,20 +1,22 @@ ---- -Lora: -## Ebyte E80-900M22S -## This is a bit experimental -## -## - Module: lr1121 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO3_TCXO_VOLTAGE: 1.8 - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - - - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox Ebyte E80-900M22S with TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml similarity index 87% rename from bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml rename to bin/config.d/lora-femtofox_SX1262_TCXO.yaml index a4dec870a..31012c0f6 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml +++ b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup -## HT-RA62 (Has internal switching, but whatever) -## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) -## Will work with any module with or without RF switching, and with TCXO - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: true - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup +## HT-RA62 (Has internal switching, but whatever) +## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) +## Will work with any module with or without RF switching, and with TCXO + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml similarity index 86% rename from bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml rename to bin/config.d/lora-femtofox_SX1262_XTAL.yaml index 6b956f3e3..7132f382e 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml +++ b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900MM22S with no external RF switching setup -## Waveshare SX126X XXXM, AI Thinker RA-01SH -## Will work with any module with or without RF switching and no TCXO - - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: false - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 XTAL + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900MM22S with no external RF switching setup +## Waveshare SX126X XXXM, AI Thinker RA-01SH +## Will work with any module with or without RF switching and no TCXO + + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: false + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml index cf25caf07..b0ac0306a 100644 --- a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -1,6 +1,12 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 (Autoconf default) + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml index 2fd128ce8..f944a7949 100644 --- a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Luckfox Lyra PicoCalc Wio LoRa SX1262 + support: official + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_1w.yaml b/bin/config.d/lora-lyra-ultra_1w.yaml index 0bdc05fef..71d05f84e 100644 --- a/bin/config.d/lora-lyra-ultra_1w.yaml +++ b/bin/config.d/lora-lyra-ultra_1w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 1 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 1W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_2w.yaml b/bin/config.d/lora-lyra-ultra_2w.yaml index a1fe6d7f7..e3bb18c7b 100644 --- a/bin/config.d/lora-lyra-ultra_2w.yaml +++ b/bin/config.d/lora-lyra-ultra_2w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 2 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 2W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml index 8425fc385..bdd98a41c 100644 --- a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml +++ b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml @@ -2,6 +2,12 @@ # Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config # Waveshare LoRa HAT for Raspberry Pi Pico # https://www.waveshare.com/wiki/Pico-LoRa-SX1262 +Meta: + name: Waveshare LoRa HAT for Raspberry Pi Pico + support: community + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-meshstick-1262.yaml b/bin/config.d/lora-meshstick-1262.yaml index 3f8d6c617..355d0bc0f 100644 --- a/bin/config.d/lora-meshstick-1262.yaml +++ b/bin/config.d/lora-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-piggystick-lr1121.yaml b/bin/config.d/lora-piggystick-lr1121.yaml index 348db61b1..e11c78dd3 100644 --- a/bin/config.d/lora-piggystick-lr1121.yaml +++ b/bin/config.d/lora-piggystick-lr1121.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: community + compatible: + - usb + Lora: Module: lr1121 CS: 0 diff --git a/bin/config.d/lora-pinedio-usb-sx1262.yaml b/bin/config.d/lora-pinedio-usb-sx1262.yaml index 6b8a9fc95..b2351c05a 100644 --- a/bin/config.d/lora-pinedio-usb-sx1262.yaml +++ b/bin/config.d/lora-pinedio-usb-sx1262.yaml @@ -1,5 +1,11 @@ +Meta: + name: Pinedio USB SX1262 + support: deprecated + compatible: + - usb + Lora: Module: sx1262 CS: 0 IRQ: 10 - spidev: ch341 \ No newline at end of file + spidev: ch341 diff --git a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml index ea86a3728..7337f39ca 100644 --- a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml +++ b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml @@ -1,3 +1,9 @@ +Meta: + name: raxda-rock2f-starter-edition-hat + support: community + compatible: + - rock-2f # Armbian + Lora: ### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx diff --git a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml index d9b64c7da..185417cce 100644 --- a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml +++ b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml @@ -1,5 +1,11 @@ # https://www.waveshare.com/core1262-868m.htm # https://github.com/markbirss/lora-starter-edition-sx1262-i2c +Meta: + name: lora-starter-edition-sx1262-i2c + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml index a539d76a1..79ca132df 100644 --- a/bin/config.d/lora-usb-meshstick-1262.yaml +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshstick-1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-meshtoad-e22.yaml b/bin/config.d/lora-usb-meshtoad-e22.yaml index b6cb61c6b..49182c83e 100644 --- a/bin/config.d/lora-usb-meshtoad-e22.yaml +++ b/bin/config.d/lora-usb-meshtoad-e22.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshtoad-e22 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml index 8b32c5af2..9f30217e0 100644 --- a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1262-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml index df772184c..45c8e21d0 100644 --- a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1268-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1268 CS: 0 diff --git a/bin/config.d/lora-waveshare-sxxx.yaml b/bin/config.d/lora-waveshare-sxxx.yaml index a9ff13653..641cf1e49 100644 --- a/bin/config.d/lora-waveshare-sxxx.yaml +++ b/bin/config.d/lora-waveshare-sxxx.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare SX126X XXXM DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml index 1e1c325e7..b84e18d5b 100644 --- a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml +++ b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml @@ -1,5 +1,12 @@ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter + +Meta: + name: ws-raspberry-pico-to-rpi-adapter + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml index 37d7e27d2..5743a9ed6 100644 --- a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml +++ b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml @@ -15,6 +15,12 @@ # 5 CS 24 # 26 DIO1/IRQ 26 +Meta: + name: ws-raspberry-pico-to-orangepi-03 + support: community + compatible: + - orange-pi-zero-3 # Armbian + Lora: Module: sx1262 # Waveshare Raspberry Pico Lora module DIO2_AS_RF_SWITCH: true From 99abfebc4ae0b14caf40b22398672c0c9516097e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Mar 2026 15:38:41 -0500 Subject: [PATCH 2/3] Fix TransmitHistory to improve epoch handling (#10017) * Fix TransmitHistory to improve epoch handling * Enable epoch handling in unit tests * Improve comments and test handling for epoch persistence in TransmitHistory * Add boot-relative timestamp handling and unit tests for TransmitHistory * loadFromDisk should handle legacy entries and clean up old v1 files after migration * Revert "loadFromDisk should handle legacy entries and clean up old v1 files after migration" This reverts commit eb7e5c7acfa4ac077fe50980be752e4b42a739b8. * Add NodeInfoModule integration for RTC quality changes and trigger immediate checks * Update test conditions for RTC quality checks --- src/gps/RTC.cpp | 28 ++++ src/gps/RTC.h | 4 + src/mesh/Throttle.cpp | 3 +- src/mesh/TransmitHistory.cpp | 163 ++++++++++++++++++----- src/mesh/TransmitHistory.h | 48 ++++++- src/modules/NodeInfoModule.cpp | 6 + src/modules/NodeInfoModule.h | 6 + test/test_transmit_history/test_main.cpp | 148 +++++++++++++++++--- 8 files changed, 344 insertions(+), 62 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index a0315559f..a8288a069 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -2,6 +2,7 @@ #include "configuration.h" #include "detect/ScanI2C.h" #include "main.h" +#include "modules/NodeInfoModule.h" #include #include #include @@ -12,6 +13,14 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds +static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality) +{ + if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) { + LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality)); + nodeInfoModule->triggerImmediateNodeInfoCheck(); + } +} + RTCQuality getRTCQuality() { return currentQuality; @@ -61,9 +70,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -105,9 +116,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -139,9 +152,11 @@ RTCSetResult readFromRTC() } #endif if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } @@ -214,6 +229,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd } if (shouldSet) { + RTCQuality oldQuality = currentQuality; currentQuality = q; lastSetMsec = now; if (currentQuality >= RTCQualityNTP) { @@ -281,6 +297,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #endif readFromRTC(); + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time @@ -397,6 +414,17 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) +{ + currentQuality = RTCQualityNone; + zeroOffsetSecs = 0; + timeStartMsec = millis() - (secondsSinceBoot * 1000); + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; +} +#endif + time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 16ecd8245..cd1e1d002 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -54,6 +54,10 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +#endif + time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index f278cc843..a4f8347b2 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -31,5 +31,6 @@ bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, vo /// @param timeSpanMs The interval in milliseconds of the timespan bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { - return (millis() - lastExecutionMs) < timeSpanMs; + uint32_t now = millis(); + return (now - lastExecutionMs) < timeSpanMs; } \ No newline at end of file diff --git a/src/mesh/TransmitHistory.cpp b/src/mesh/TransmitHistory.cpp index b615c307a..33da7d35c 100644 --- a/src/mesh/TransmitHistory.cpp +++ b/src/mesh/TransmitHistory.cpp @@ -16,6 +16,20 @@ TransmitHistory *TransmitHistory::getInstance() return transmitHistory; } +TransmitHistory::StoredTimestamp TransmitHistory::makeStoredTimestamp(uint32_t seconds, uint8_t flags) +{ + StoredTimestamp stored; + stored.seconds = seconds; + stored.flags = flags; + return stored; +} + +TransmitHistory::StoredTimestamp TransmitHistory::decodeLegacyTimestamp(uint32_t seconds) +{ + const bool isProbablyBootRelative = seconds > 0 && seconds <= LEGACY_BOOT_RELATIVE_MAX_SEC; + return makeStoredTimestamp(seconds, isProbablyBootRelative ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE); +} + void TransmitHistory::loadFromDisk() { spiLock->lock(); @@ -23,16 +37,33 @@ void TransmitHistory::loadFromDisk() if (file) { FileHeader header{}; if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && - header.version == VERSION && header.count <= MAX_ENTRIES) { + (header.version == 1 || header.version == VERSION) && header.count <= MAX_ENTRIES) { for (uint8_t i = 0; i < header.count; i++) { - Entry entry{}; - if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) { - if (entry.epochSeconds > 0) { - history[entry.key] = entry.epochSeconds; - // Seed in-memory millis so throttle works even without RTC/GPS. - // Treating stored entries as "just sent" is safe — worst case the - // node waits one full interval before its first broadcast. - lastMillis[entry.key] = millis(); + if (header.version == 1) { + LegacyEntry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = decodeLegacyTimestamp(entry.epochSeconds); + } + } else { + Entry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = makeStoredTimestamp(entry.epochSeconds, entry.flags); + // Do NOT seed lastMillis here. + // + // getLastSentToMeshMillis() reconstructs a millis()-relative value + // from the stored epoch, and Throttle::isWithinTimespanMs() uses + // the same unsigned subtraction pattern. Once getTime() has a valid + // wall-clock epoch comparable to stored values, recent reboots still + // throttle correctly while long power-off periods no longer look like + // "just sent" and incorrectly suppress the first send. + // + // Before RTC/NTP/GPS time is valid, persisted absolute epochs do not + // contribute, but boot-relative entries still suppress near-term reboot + // chatter via a narrow recovery window. + // + // If we seeded lastMillis to millis() here, every loaded entry would + // appear to have been sent at boot time, regardless of the true age + // of the last transmission. That was the regression behind #9901. } } } @@ -53,7 +84,8 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) lastMillis[key] = millis(); uint32_t now = getTime(); if (now >= 2) { - history[key] = now; + const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE; + history[key] = makeStoredTimestamp(now, flags); dirty = true; // Don't flush to disk on every transmit — flash has limited write endurance. // The in-memory lastMillis map handles throttle during normal operation. @@ -68,15 +100,84 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) } } +#ifdef PIO_UNIT_TESTING +void TransmitHistory::setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds) +{ + if (epochSeconds > 0) { + history[key] = makeStoredTimestamp(epochSeconds, ENTRY_FLAG_NONE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} + +void TransmitHistory::setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot) +{ + if (secondsSinceBoot > 0) { + history[key] = makeStoredTimestamp(secondsSinceBoot, ENTRY_FLAG_BOOT_RELATIVE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} +#endif + uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const { auto it = history.find(key); if (it != history.end()) { - return it->second; + return it->second.seconds; } return 0; } +uint32_t TransmitHistory::getLastSentAbsoluteMillis(uint32_t storedEpoch) const +{ + uint32_t now = getTime(); + if (now < 2) { + return 0; + } + + if (storedEpoch > now) { + return 0; + } + + uint32_t secondsAgo = now - storedEpoch; + uint32_t msAgo = secondsAgo * 1000; + + if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { + return 0; + } + + return millis() - msAgo; +} + +uint32_t TransmitHistory::getLastSentBootRelativeMillis(uint32_t storedSeconds) const +{ + if (getRTCQuality() != RTCQualityNone) { + return 0; + } + + uint32_t now = getTime(); + + if (storedSeconds <= now) { + uint32_t secondsAgo = now - storedSeconds; + if (secondsAgo > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + return millis() - (secondsAgo * 1000); + } + + uint32_t secondsAhead = storedSeconds - now; + if (secondsAhead > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + + return millis(); +} + uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const { // Prefer runtime millis value (accurate within this boot) @@ -86,34 +187,23 @@ uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const } // Fall back to epoch conversion (loaded from disk after reboot) - uint32_t storedEpoch = getLastSentToMeshEpoch(key); - if (storedEpoch == 0) { + auto it = history.find(key); + if (it == history.end() || it->second.seconds == 0) { return 0; // No stored time — module has never sent } - uint32_t now = getTime(); - if (now < 2) { - // No valid RTC time yet — can't convert to millis. Return 0 so throttle doesn't block. - return 0; + // Convert to a millis()-relative timestamp: millis() - msAgo. + // + // The result may wrap if msAgo is larger than the current uptime, and that is + // intentional. Throttle::isWithinTimespanMs() also uses unsigned subtraction, + // so the reconstructed age is preserved across wraparound: + // - recent reboot, 5 min ago -> (millis() - lastMs) == 300000, still throttled + // - long reboot, 30 min ago -> (millis() - lastMs) == 1800000, allowed + if ((it->second.flags & ENTRY_FLAG_BOOT_RELATIVE) != 0) { + return getLastSentBootRelativeMillis(it->second.seconds); } - if (storedEpoch > now) { - // Stored time is in the future (clock went backwards?) — treat as stale - return 0; - } - - uint32_t secondsAgo = now - storedEpoch; - uint32_t msAgo = secondsAgo * 1000; - - // Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle) - if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { - return 0; - } - - // Convert to a millis()-relative timestamp: millis() - msAgo - // This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval), - // correctly reports whether the transmit was within interval ms. - return millis() - msAgo; + return getLastSentAbsoluteMillis(it->second.seconds); } bool TransmitHistory::saveToDisk() @@ -141,12 +231,13 @@ bool TransmitHistory::saveToDisk() file.write((uint8_t *)&header, sizeof(header)); uint8_t written = 0; - for (const auto &[key, epochSeconds] : history) { + for (const auto &[key, stored] : history) { if (written >= MAX_ENTRIES) break; Entry entry{}; entry.key = key; - entry.epochSeconds = epochSeconds; + entry.epochSeconds = stored.seconds; + entry.flags = stored.flags; file.write((uint8_t *)&entry, sizeof(entry)); written++; } diff --git a/src/mesh/TransmitHistory.h b/src/mesh/TransmitHistory.h index 01201eaac..1a79048ea 100644 --- a/src/mesh/TransmitHistory.h +++ b/src/mesh/TransmitHistory.h @@ -35,8 +35,25 @@ class TransmitHistory */ void setLastSentToMesh(uint16_t key); +#ifdef PIO_UNIT_TESTING /** - * Get the last transmit epoch seconds for a given key, or 0 if unknown. + * Directly set the stored epoch for a key without touching the runtime lastMillis map. + * Intended for testing purposes: lets tests simulate "the last broadcast happened N + * seconds ago" without needing to fake the system clock. + */ + void setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds); + + /** + * Directly set a boot-relative timestamp (seconds since boot) for testing. + */ + void setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot); +#endif + + /** + * Get the raw persisted timestamp seconds for a given key, or 0 if unknown. + * + * The returned value is an absolute epoch when persisted with valid RTC/NTP/GPS time, + * or boot-relative seconds when ENTRY_FLAG_BOOT_RELATIVE is set. */ uint32_t getLastSentToMeshEpoch(uint16_t key) const; @@ -64,13 +81,31 @@ class TransmitHistory static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; static constexpr uint32_t MAGIC = 0x54485354; // "THST" - static constexpr uint8_t VERSION = 1; + static constexpr uint8_t VERSION = 2; static constexpr uint8_t MAX_ENTRIES = 16; static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + static constexpr uint32_t BOOT_RELATIVE_RECOVERY_WINDOW_SEC = 2 * 60; + static constexpr uint32_t LEGACY_BOOT_RELATIVE_MAX_SEC = 365UL * 24 * 60 * 60; + + enum EntryFlags : uint8_t { + ENTRY_FLAG_NONE = 0, + ENTRY_FLAG_BOOT_RELATIVE = 0x01, + }; + + struct StoredTimestamp { + uint32_t seconds = 0; + uint8_t flags = ENTRY_FLAG_NONE; + }; struct __attribute__((packed)) Entry { uint16_t key; uint32_t epochSeconds; + uint8_t flags; + }; + + struct __attribute__((packed)) LegacyEntry { + uint16_t key; + uint32_t epochSeconds; }; struct __attribute__((packed)) FileHeader { @@ -79,8 +114,13 @@ class TransmitHistory uint8_t count; }; - std::map history; // key -> epoch seconds (for disk persistence) - std::map lastMillis; // key -> millis() value (for runtime throttle) + uint32_t getLastSentAbsoluteMillis(uint32_t storedEpoch) const; + uint32_t getLastSentBootRelativeMillis(uint32_t storedSeconds) const; + static StoredTimestamp makeStoredTimestamp(uint32_t seconds, uint8_t flags = ENTRY_FLAG_NONE); + static StoredTimestamp decodeLegacyTimestamp(uint32_t seconds); + + std::map history; // key -> persisted transmit time + std::map lastMillis; // key -> millis() value (for runtime throttle) bool dirty = false; uint32_t lastDiskSave = 0; // millis() of last disk flush }; diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index f41fafdee..4de479241 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -118,6 +118,12 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha } } +void NodeInfoModule::triggerImmediateNodeInfoCheck() +{ + LOG_DEBUG("NodeInfo: scheduling immediate periodic check"); + setIntervalFromNow(0); +} + meshtastic_MeshPacket *NodeInfoModule::allocReply() { // Only apply suppression when actually replying to someone else's request, not for periodic broadcasts. diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 0c0dec849..9b3b66cae 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -24,6 +24,12 @@ class NodeInfoModule : public ProtobufModule, private concurren void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); + /** + * Schedule an immediate NodeInfo periodic check. + * Used when external conditions change (for example time source quality). + */ + void triggerImmediateNodeInfoCheck(); + protected: /** Called to handle a particular incoming message diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp index 992668d97..3bd84b55c 100644 --- a/test/test_transmit_history/test_main.cpp +++ b/test/test_transmit_history/test_main.cpp @@ -1,5 +1,6 @@ #include "TestUtil.h" #include "TransmitHistory.h" +#include "gps/RTC.h" #include #include @@ -161,44 +162,141 @@ static void test_save_and_load_round_trip() // After loadFromDisk, millis should be seeded (non-zero) for stored entries uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); if (restoredNodeInfo > 0) { - // If epoch was stored, millis should be seeded from load + // If epoch was stored (set seconds ago), epoch-conversion gives elapsed ≈ 0 s, + // so getLastSentToMeshMillis() should return a non-zero value. TEST_ASSERT_NOT_EQUAL(0, restoredMillis); } } // --- Boot without RTC scenario --- -static void test_load_seeds_millis_even_without_rtc() +// Crash-reboot protection: a send that happened moments before the reboot must still +// throttle after reload. This works because getLastSentToMeshMillis() reconstructs +// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned +// subtraction so the age survives wraparound even when uptime is near zero. +static void test_boot_after_recent_send_still_throttles() { - // This tests the critical crash-reboot scenario: - // After loadFromDisk(), even if getTime() returns 0 (no RTC), - // lastMillis should be seeded so throttle blocks immediate re-broadcast. - transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); transmitHistory->saveToDisk(); - // Simulate reboot: destroy and recreate + // Simulate reboot delete transmitHistory; transmitHistory = nullptr; transmitHistory = TransmitHistory::getInstance(); transmitHistory->loadFromDisk(); - // The key insight: after load, getLastSentToMeshMillis should return non-zero - // because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry. - // This ensures throttle works even without RTC. + // Epoch was set seconds ago; reconstructed age is still within the 10-min window. uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); - uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); - if (epoch > 0) { - // Data was persisted — millis must be seeded - TEST_ASSERT_NOT_EQUAL(0, result); - - // And it should cause throttle to block (treating as "just sent") - bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); - TEST_ASSERT_TRUE(withinInterval); + if (epoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; } - // If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore. - // This is expected on platforms without RTC during the very first boot. + + TEST_ASSERT_NOT_EQUAL(0, result); + bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +// Regression test for issue #9901: +// A device powered off for longer than the throttle window must broadcast NodeInfo +// on its next boot — it must not be silenced because loadFromDisk() once treated +// every loaded entry as "just sent" by seeding lastMillis to millis() at boot. +static void test_boot_after_long_gap_allows_nodeinfo() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)"); +} + +// Complementary: a rapid reboot must still throttle (crash-loop protection), even +// though the reconstructed lastMs may wrap because current uptime is small. +static void test_boot_within_throttle_window_still_throttles() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window"); +} + +static void test_boot_without_time_source_still_throttles_recent_restart() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(31); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo"); +} + +static void test_boot_without_time_source_expires_boot_relative_history() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(400); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts"); } void setup() @@ -222,7 +320,15 @@ void setup() // Persistence RUN_TEST(test_save_and_load_round_trip); - RUN_TEST(test_load_seeds_millis_even_without_rtc); + RUN_TEST(test_boot_after_recent_send_still_throttles); + + // Issue #9901 regression tests + RUN_TEST(test_boot_after_long_gap_allows_nodeinfo); + RUN_TEST(test_boot_within_throttle_window_still_throttles); + + // No-RTC regression tests + RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart); + RUN_TEST(test_boot_without_time_source_expires_boot_relative_history); exit(UNITY_END()); } From aec98b61b9b69a1e90080be716f0579e7d7509f9 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:39:26 +0100 Subject: [PATCH 3/3] MUI: WiFi map tile download: heltec V4 adaptations (#10011) * rotated MUI * mui-maps heltec-v4 adaptations --------- Co-authored-by: Ben Meadors --- platformio.ini | 2 +- src/graphics/tftSetup.cpp | 6 +++++- variants/esp32s3/heltec_v4/platformio.ini | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 2fcfc480d..9b32c61de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,7 +126,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/f36d2a953524e372b78c5b4147ec55f38716964e.zip + https://github.com/meshtastic/device-ui/archive/03fbf26f5d6095f2c7c77ee2d064af01669ac38c.zip ; Common libs for environmental measurements in telemetry module [environmental_base] diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..708cd8967 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -16,6 +16,10 @@ DeviceScreen *deviceScreen = nullptr; +#ifndef TFT_TASK_STACK_SIZE +#define TFT_TASK_STACK_SIZE 16384 +#endif + #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = @@ -127,7 +131,7 @@ void tftSetup(void) #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 9acf30c21..cb795c65d 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -68,7 +68,10 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1860 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE