diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 706b9cfe7..806df4e7a 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -4,9 +4,14 @@ on: workflow_dispatch: inputs: # trunk-ignore(checkov/CKV_GHA_7) + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. arch: type: choice options: + - all - esp32 - esp32s3 - esp32c3 @@ -15,32 +20,18 @@ on: - rp2040 - rp2350 - stm32 - target: - type: string - required: false - description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. - # find-target: - # type: boolean - # default: true - # description: 'Find the available targets' + description: Choose an arch to limit the search, or 'all' to search all architectures. + default: all permissions: read-all jobs: find-targets: - if: ${{ inputs.target == '' }} strategy: fail-fast: false matrix: arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 + - all runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -51,14 +42,37 @@ jobs: - run: pip install -U platformio - name: Generate matrix id: jsonStep + env: + BUILDTARGET: ${{ inputs.target }} + MATRIXARCH: ${{ inputs.arch }} run: | TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra) - echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY - echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY - echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY - echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY - echo "Targets:" >> $GITHUB_STEP_SUMMARY - echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY + if [ "$BUILDTARGET" = "" ]; then + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "## 🎯 The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY + else + echo "We build this one:" >> $GITHUB_STEP_SUMMARY + ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform') + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "$ARCH" == "" ]]; then + echo "## ❌ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY + else + echo "## βœ… Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY + fi + echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY + echo "arch=$ARCH" >> $GITHUB_OUTPUT + fi + outputs: + arch: ${{ steps.jsonStep.outputs.arch }} version: if: ${{ inputs.target != '' }} @@ -78,12 +92,12 @@ jobs: build: if: ${{ inputs.target != '' && inputs.arch != 'native' }} - needs: [version] + needs: [version, find-targets] uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ inputs.target }} - platform: ${{ inputs.arch }} + platform: ${{ needs.find-targets.outputs.arch }} gather-artifacts: permissions: diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index d0179bba8..2fabf0591 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -136,7 +136,7 @@ jobs: merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.6.0 + uses: dorny/test-reporter@v3.0.0 with: name: PlatformIO Tests path: testreport.xml diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 0385ffd6a..3a19e7424 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,15 +8,15 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.510 - - renovate@43.84.0 + - checkov@3.2.513 + - renovate@43.100.0 - prettier@3.8.1 - - trufflehog@3.93.8 + - trufflehog@3.94.1 - yamllint@1.38.0 - bandit@1.9.4 - trivy@0.69.3 - taplo@0.10.0 - - ruff@0.15.7 + - ruff@0.15.8 - isort@8.0.1 - markdownlint@0.48.0 - oxipng@10.1.0 @@ -28,7 +28,7 @@ lint: - shellcheck@0.11.0 - black@26.3.1 - git-diff-check - - gitleaks@8.30.0 + - gitleaks@8.30.1 - clang-format@16.0.3 ignore: - linters: [ALL] 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/femtofox/femtofox_E80-900M2213S.yaml b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml new file mode 100644 index 000000000..2f2b24603 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml @@ -0,0 +1,30 @@ +--- +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 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, HIGH, LOW] + MODE_TX: [HIGH, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + MODE_TX_HF: [LOW, LOW, LOW] + MODE_GNSS: [LOW, LOW, HIGH] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_LR1121 generic.yaml b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml new file mode 100644 index 000000000..c66eebed5 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml @@ -0,0 +1,46 @@ +--- +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 + +rfswitch_table: + pins: + - DIO5 + - DIO6 + MODE_STBY: + - LOW + - LOW + MODE_RX: + - HIGH + - LOW + MODE_TX: + - HIGH + - HIGH + MODE_TX_HP: + - LOW + - HIGH + MODE_TX_HF: + - LOW + - LOW + MODE_GNSS: + - LOW + - LOW + MODE_WIFI: + - LOW + - LOW + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml new file mode 100644 index 000000000..c2ab76d46 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml @@ -0,0 +1,30 @@ +--- +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 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, LOW, LOW] + MODE_TX: [LOW, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + # MODE_TX_HF: [] + # MODE_GNSS: [] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 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 diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index b4c18c05b..1458e4390 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -43,7 +43,7 @@ for pio_env in pio_envs: env = { "ci": {"board": pio_env, "platform": env_platform}, "board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), - "board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), + "board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true", } all_envs.append(env) diff --git a/boards/t5-epaper-s3.json b/boards/t5-epaper-s3.json new file mode 100644 index 000000000..16106198e --- /dev/null +++ b/boards/t5-epaper-s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T5-ePaper-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://lilygo.cc/products/t5-e-paper-s3-pro", + "vendor": "LILYGO" +} diff --git a/boards/ttgo-tbeam.json b/boards/ttgo-tbeam.json new file mode 100644 index 000000000..a4c43d525 --- /dev/null +++ b/boards/ttgo-tbeam.json @@ -0,0 +1,29 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_T_Beam", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "tbeam" + }, + "connectivity": ["wifi", "bluetooth", "can", "ethernet"], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "TTGO T-Beam (PSRAM Disabled)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 1310720, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/LilyGO/TTGO-T-Beam", + "vendor": "TTGO" +} diff --git a/platformio.ini b/platformio.ini index 53d2632db..5c7a07ffe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -77,10 +77,10 @@ lib_deps = https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip - # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb - nanopb/Nanopb@0.4.91 - # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 - erriez/ErriezCRC32@1.0.1 + # renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb + https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip + # renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32 + https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip ; Used for the code analysis in PIO Home / Inspect check_tool = cppcheck @@ -95,8 +95,8 @@ check_flags = framework = arduino lib_deps = ${env.lib_deps} - # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL - end2endzone/NonBlockingRTTTL@1.4.0 + # renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL + https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip build_unflags = -std=c++11 -std=gnu++11 @@ -107,21 +107,21 @@ build_src_filter = ${env.build_src_filter} - -_client->begin(this->_port); this->_enabled = true; } @@ -166,14 +165,21 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess if ((pri & LOG_FACMASK) == 0) pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + // W5100S: acquire UDP socket on-demand to avoid permanent socket consumption + if (!this->_client->begin(this->_port)) { + return false; + } + if (this->_server != NULL) { result = this->_client->beginPacket(this->_server, this->_port); } else { result = this->_client->beginPacket(this->_ip, this->_port); } - if (result != 1) + if (result != 1) { + this->_client->stop(); return false; + } this->_client->print('<'); this->_client->print(pri); @@ -193,6 +199,8 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess this->_client->print(message); this->_client->endPacket(); + this->_client->stop(); // W5100S: release UDP socket for other services + return true; } diff --git a/src/Power.cpp b/src/Power.cpp index ea4fcf42a..d82c870ed 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -35,6 +35,11 @@ #include "nrfx_power.h" #endif +#if defined(ARCH_NRF52) +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" +#endif + #if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #include "target_specific.h" @@ -328,6 +333,9 @@ class AnalogBatteryLevel : public HasBatteryLevel scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); scaled *= operativeAdcMultiplier; #else // block for all other platforms +#ifdef ARCH_NRF52 + concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock); +#endif for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { raw += analogRead(BATTERY_PIN); } diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6fb28a6ac..6692d996d 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -6,6 +6,11 @@ #include "Tone.h" #endif +#if defined(HAS_I2S) +#include "main.h" +#include +#endif + #if !defined(ARCH_PORTDUINO) extern "C" void delay(uint32_t dwMs); #endif @@ -50,6 +55,50 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note +#ifdef HAS_I2S +void playTonesRTTTL(const ToneDuration *tone_durations, int size) +{ + // translate ToneDuration[] to RTTTL string and play using audioThread + static std::unordered_map freqToNote = { + {NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"}, + {NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"}, + {NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"}, + {NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}}; + + char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave + for (int i = 0; i < size; i++) { + const auto &td = tone_durations[i]; + std::string note = "b4"; + if (freqToNote.find(td.frequency_khz) != freqToNote.end()) { + note = freqToNote[td.frequency_khz]; + } + int dur = 32; // default duration + if (td.duration_ms >= 1000) + dur = 1; + else if (td.duration_ms >= 500) + dur = 2; + else if (td.duration_ms >= 250) + dur = 4; + else if (td.duration_ms >= 125) + dur = 8; + else if (td.duration_ms >= 62) + dur = 16; + else + dur = 32; + + char noteStr[64]; + snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur); + strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1); + + audioThread->beginRttl(rtttl, strlen(rtttl)); + while (audioThread->isPlaying()) { + delay(10); + } + return; + } +} +#endif + void playTones(const ToneDuration *tone_durations, int size) { if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || @@ -57,7 +106,13 @@ void playTones(const ToneDuration *tone_durations, int size) // Buzzer is disabled or not set to system tones return; } -#ifdef PIN_BUZZER +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) { + playTonesRTTTL(tone_durations, size); + return; + } +#endif +#if defined(PIN_BUZZER) if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; #endif 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/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 12e229da3..704487bc8 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "EInkDisplay2.h" #include "SPILock.h" #include "main.h" diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 7a86b0f57..645a3f2d0 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "GxEPD2_BW.h" #include diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp new file mode 100644 index 000000000..b870e111b --- /dev/null +++ b/src/graphics/EInkParallelDisplay.cpp @@ -0,0 +1,427 @@ +#include "EInkParallelDisplay.h" + +#ifdef USE_EINK_PARALLELDISPLAY + +#include "Wire.h" +#include "variant.h" +#include +#include +#include +#include + +#include "FastEPD.h" + +// Thresholds for choosing partial vs full update +#ifndef EPD_PARTIAL_THRESHOLD_ROWS +#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial +#endif +#ifndef EPD_FULLSLOW_PERIOD +#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh +#endif +#ifndef EPD_RESPONSIVE_MIN_MS +#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates +#endif + +EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot) +{ + LOG_INFO("init EInkParallelDisplay"); + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = width; + this->displayHeight = height; + + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(width, height); + uint16_t longSide = max(width, height); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; + + this->displayBufferSize = longSide * (shortSide / 8); + +#ifdef EINK_LIMIT_GHOSTING_PX + // allocate dirty pixel buffer same size as epaper buffers (rowBytes * height) + size_t rowBytes = (this->displayWidth + 7) / 8; + dirtyPixelsSize = rowBytes * this->displayHeight; + dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1); + ghostPixelCount = 0; +#endif +} + +EInkParallelDisplay::~EInkParallelDisplay() +{ +#ifdef EINK_LIMIT_GHOSTING_PX + if (dirtyPixels) { + free(dirtyPixels); + dirtyPixels = nullptr; + } +#endif + // If an async full update is running, wait for it to finish + if (asyncFullRunning.load()) { + // wait a short while for task to finish + for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) { + delay(50); + } + if (asyncTaskHandle) { + // Let it finish or delete it + vTaskDelete(asyncTaskHandle); + asyncTaskHandle = nullptr; + } + } + + delete epaper; +} + +/* + * Called by the OLEDDisplay::init() path. + */ +bool EInkParallelDisplay::connect() +{ + LOG_INFO("Do EPD init"); + if (!epaper) { + epaper = new FASTEPD; +#if defined(T5_S3_EPAPER_PRO_V1) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); +#elif defined(T5_S3_EPAPER_PRO_V2) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + // initialize all port 0 pins (0-7) as outputs / HIGH + for (int i = 0; i < 8; i++) { + epaper->ioPinMode(i, OUTPUT); + epaper->ioWrite(i, HIGH); + } +#else +#error "unsupported EPD device!" +#endif + } + + // epaper->setRotation(rotation); // does not work, messes up width/height + epaper->setMode(BB_MODE_1BPP); + epaper->clearWhite(); + epaper->fullUpdate(true); + +#ifdef EINK_LIMIT_GHOSTING_PX + // After a full/clear the dirty tracking should be reset + resetGhostPixelTracking(); +#endif + + return true; +} + +/* + * sendCommand - simple passthrough (not required for epd_driver-based path) + */ +void EInkParallelDisplay::sendCommand(uint8_t com) +{ + LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com); +} + +/* + * Start a background task that will perform a blocking fullUpdate(). This lets + * display() return quickly while the heavy refresh runs in the background. + */ +void EInkParallelDisplay::startAsyncFullUpdate(int clearMode) +{ + if (asyncFullRunning.load()) + return; // already running + + asyncFullRunning.store(true); + // pass 'this' as parameter + BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t), + this, 2, &asyncTaskHandle, +#if CONFIG_FREERTOS_UNICORE + 0 +#else + 1 +#endif + ); + if (rc != pdPASS) { + LOG_WARN("Failed to create async full-update task, falling back to blocking update"); + epaper->fullUpdate(clearMode, false); + epaper->backupPlane(); + asyncFullRunning.store(false); + asyncTaskHandle = nullptr; + } +} + +/* + * FreeRTOS task entry: runs the full update and then backs up plane. + */ +void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters) +{ + EInkParallelDisplay *self = static_cast(pvParameters); + if (!self) { + vTaskDelete(nullptr); + return; + } + + // choose CLEAR_SLOW occasionally + int clearMode = CLEAR_FAST; + if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) { + clearMode = CLEAR_SLOW; + self->fastRefreshCount = 0; + } else { + // when running async full, treat it as a full so reset fast count + self->fastRefreshCount = 0; + } + + self->epaper->fullUpdate(clearMode, false); + self->epaper->backupPlane(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // A full refresh clears ghosting state + self->resetGhostPixelTracking(); +#endif + + self->asyncFullRunning.store(false); + self->asyncTaskHandle = nullptr; + + // delete this task + vTaskDelete(nullptr); +} + +/* + * Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes + * buffer used by the FASTEPD library. For performance we write directly into FASTEPD's + * currentBuffer() while comparing against previousBuffer() to detect changed rows. + * After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according + * to a heuristic so only the minimal region is refreshed. + */ +void EInkParallelDisplay::display(void) +{ + const uint16_t w = this->displayWidth; + const uint16_t h = this->displayHeight; + + // Simple rate limiting: avoid very-frequent responsive updates + uint32_t nowMs = millis(); + if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) { + LOG_DEBUG("rate-limited, skipping update"); + return; + } + + // bytes per row in epd format (one byte = 8 horizontal pixels) + const uint32_t rowBytes = (w + 7) / 8; + + // Get pointers to internal buffers + uint8_t *cur = epaper->currentBuffer(); + uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init + + // Track changed row range while converting + int newTop = h; // min changed row (initialized to out-of-range) + int newBottom = -1; // max changed row + +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Track changed byte column range (for clipped fullUpdate fallback) + int newLeftByte = (int)rowBytes; + int newRightByte = -1; +#endif + + // Compute a quick hash of the incoming OLED buffer (so we can skip identical frames) + uint32_t imageHash = 0; + uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size + for (uint32_t bi = 0; bi < bufBytes; ++bi) { + imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31); + } + if (imageHash == previousImageHash) { + // LOG_DEBUG("image identical to previous, skipping update"); + return; + } + +#ifdef EINK_LIMIT_GHOSTING_PX + // reset ghost count for this conversion pass; we'll mark bits that change + ghostPixelCount = 0; +#endif + + // Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur, + // comparing against prev when available to detect changes. + for (uint32_t y = 0; y < h; ++y) { + const uint32_t base = (y >> 3) * w; // (y/8) * width + const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout + const uint32_t rowBase = y * rowBytes; + + // process full 8-pixel bytes + for (uint32_t xb = 0; xb < rowBytes; ++xb) { + uint32_t x0 = xb * 8; + // read up to 8 source bytes (vertical-byte per column) + uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0; + uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0; + uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0; + uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0; + uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0; + uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0; + uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0; + uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0; + + // build output byte: MSB = leftmost pixel + uint8_t out = 0; + out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00); + out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00); + out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00); + out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00); + out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00); + out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00); + out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00); + out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00); + + // handle partial byte at end of row by masking off invalid bits + uint8_t mask = 0xFF; + uint32_t bitsRemain = (w > x0) ? (w - x0) : 0; + if (bitsRemain > 0 && bitsRemain < 8) { + mask = (uint8_t)(0xFF << (8 - bitsRemain)); + out &= mask; + } + + // invert to FASTEPD polarity + out = (~out) & mask; + + uint32_t pos = rowBase + xb; + uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00; + // Consider this byte changed if previous buffer differs (or prev is null) + bool changed = (prev == nullptr) || (prevVal != out); + +#ifdef EINK_LIMIT_GHOSTING_PX + if (changed && prev) + markDirtyBits(prev, pos, mask, out); +#endif + + // mark row changed only if the previous buffer differs + if (changed) { + if (y < (uint32_t)newTop) + newTop = y; + if ((int)y > newBottom) + newBottom = y; +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // record changed column bytes + if ((int)xb < newLeftByte) + newLeftByte = (int)xb; + if ((int)xb > newRightByte) + newRightByte = (int)xb; +#endif + } + + // Always write the computed value into the current buffer (avoid leaving stale bytes) + cur[pos] = (cur[pos] & ~mask) | out; + } + } + + // If nothing changed, avoid any panel update + if (newBottom < 0) { + LOG_DEBUG("no pixel changes detected, skipping update (conv)"); + previousImageHash = imageHash; // still remember that frame + return; + } + + // Choose partial vs full update using heuristic + // Decide if we should force a full update after many fast updates + bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD); + +#ifdef EINK_LIMIT_GHOSTING_PX + // If ghost pixels exceed limit, force a full update to clear ghosting + if (ghostPixelCount > ghostPixelLimit) { + LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit); + forceFull = true; + } +#endif + + // Compute pixel bounds from newTop/newBottom + int startRow = (newTop / 8) * 8; + int endRow = (newBottom / 8) * 8 + 7; + + LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes); + + if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) { + // Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback. +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Workaround for FastEPD partial update bug: use clipped fullUpdate instead + // Build a pixel rectangle for a clipped fullUpdate using the changed columns + int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0; + int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1); + + BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1}; + // LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h); + epaper->fullUpdate(CLEAR_FAST, false, &rect); +#else + // Use rows for partial update + LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow); + epaper->partialUpdate(true, startRow, endRow); +#endif + epaper->backupPlane(); + fastRefreshCount++; + } else { + // Full update: run async if possible (startAsyncFullUpdate will fall back to blocking) + startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST); + } + + lastUpdateMs = millis(); + previousImageHash = imageHash; + + // Keep same behavior as before + lastDrawMsec = millis(); +} + +#ifdef EINK_LIMIT_GHOSTING_PX +// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount +void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out) +{ + // defensive: need dirtyPixels allocated and prevBuf valid + if (!dirtyPixels || !prevBuf) + return; + + // 'out' is in FASTEPD polarity (1 = black, 0 = white) + uint8_t newBlack = out & mask; // bits that will be black now + uint8_t newWhite = (~out) & mask; // bits that will be white now + + // previously recorded dirty bits for this byte + uint8_t before = dirtyPixels[pos]; + + // Ghost bits: bits that were previously marked dirty and are now being driven white + uint8_t ghostBits = before & newWhite; + if (ghostBits) { + ghostPixelCount += __builtin_popcount((unsigned)ghostBits); + } + + // Only mark bits dirty when they turn black now (accumulate until a full refresh) + uint8_t newlyDirty = newBlack & (~before); + if (newlyDirty) { + dirtyPixels[pos] |= newlyDirty; + } +} + +// reset ghost tracking (call after a full refresh) +void EInkParallelDisplay::resetGhostPixelTracking() +{ + if (!dirtyPixels) + return; + memset(dirtyPixels, 0, dirtyPixelsSize); + ghostPixelCount = 0; +} +#endif + +/* + * forceDisplay: use lastDrawMsec + */ +bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) +{ + uint32_t now = millis(); + if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { + display(); + return true; + } + return false; +} + +void EInkParallelDisplay::endUpdate() +{ + { + // ensure any async full update is started/completed + if (asyncFullRunning.load()) { + // nothing to do; background task will run and call backupPlane when done + } else { + epaper->fullUpdate(CLEAR_FAST, false); + epaper->backupPlane(); +#ifdef EINK_LIMIT_GHOSTING_PX + resetGhostPixelTracking(); +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h new file mode 100644 index 000000000..81189e400 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.h @@ -0,0 +1,69 @@ +#pragma once + +#include "configuration.h" + +#ifdef USE_EINK_PARALLELDISPLAY +#include + +#include +#include +#include + +class FASTEPD; + +/** + * Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library + */ +class EInkParallelDisplay : public OLEDDisplay +{ + public: + enum EpdRotation { + EPD_ROT_LANDSCAPE = 0, + EPD_ROT_PORTRAIT = 90, + EPD_ROT_INVERTED_LANDSCAPE = 180, + EPD_ROT_INVERTED_PORTRAIT = 270, + }; + + EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation); + virtual ~EInkParallelDisplay(); + + // OLEDDisplay virtuals + bool connect() override; + void sendCommand(uint8_t com) override; + int getBufferOffset(void) override { return 0; } + + void display(void) override; + bool forceDisplay(uint32_t msecLimit = 1000); + void endUpdate(); + + protected: + uint32_t lastDrawMsec = 0; + FASTEPD *epaper; + + private: + // Async full-refresh support + std::atomic asyncFullRunning{false}; + TaskHandle_t asyncTaskHandle = nullptr; + void startAsyncFullUpdate(int clearMode); + static void asyncFullUpdateTask(void *pvParameters); + +#ifdef EINK_LIMIT_GHOSTING_PX + // helpers + void resetGhostPixelTracking(); + void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out); + void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull); + + // per-bit dirty buffer (same format as epaper buffers): one bit == one pixel + uint8_t *dirtyPixels = nullptr; + size_t dirtyPixelsSize = 0; + uint32_t ghostPixelCount = 0; + uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX; +#endif + + EpdRotation rotation; + uint32_t previousImageHash = 0; + uint32_t lastUpdateMs = 0; + int fastRefreshCount = 0; +}; + +#endif diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 724fd2007..55ec93db5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -27,6 +27,7 @@ along with this program. If not, see . #include "configuration.h" #include "meshUtils.h" #if HAS_SCREEN +#include "EInkParallelDisplay.h" #include #include "DisplayFormatters.h" @@ -364,12 +365,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY) dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK_PARALLELDISPLAY) + dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -759,7 +762,11 @@ void Screen::forceDisplay(bool forceUiUpdate) } // Tell EInk class to update the display +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(); +#elif defined(USE_EINK) static_cast(dispdev)->forceDisplay(); +#endif #else // No delay between UI frame rendering if (forceUiUpdate) { @@ -998,8 +1005,10 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) ui->update(); } while (ui->getUiState()->lastUpdate < startUpdate); +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(0); +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) // Old EInkDisplay class -#if !defined(USE_EINK_DYNAMICDISPLAY) static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif @@ -1011,7 +1020,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) #ifdef EINK_HASQUIRK_GHOSTING EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index ed2e200bb..26276edb2 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -20,7 +20,7 @@ #include "graphics/fonts/OLEDDisplayFontsGR.h" #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif @@ -90,7 +90,7 @@ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 @@ -106,7 +106,7 @@ #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 27a50d3f9..7a12650ca 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -544,6 +544,9 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #ifndef T_DECK_PRO barsOffset -= 12; #endif +#if defined(T5_S3_EPAPER_PRO) + barsOffset += 60; +#endif #endif int barX = x + barsOffset; if (currentResolution == ScreenResolution::UltraLow) { @@ -593,11 +596,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); uint32_t heapTotal = memGet.getHeapSize(); - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); +#endif flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif @@ -616,10 +620,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO if (psramUsed > 0) { line += 1; drawUsageRow("PSRAM:", psramUsed, psramTotal); } +#endif if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index daefff8ee..157d4c314 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1221,9 +1221,11 @@ void menuHandler::positionBaseMenu() }; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array baseLabels{}; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array calibrateLabels{}; +#endif auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { @@ -1249,9 +1251,11 @@ void menuHandler::positionBaseMenu() screen->runNow(); break; case PositionAction::CompassCalibrate: +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { accelerometerThread->calibrate(30); } +#endif break; case PositionAction::GPSSmartPosition: menuQueue = GpsSmartPositionMenu; @@ -1269,11 +1273,15 @@ void menuHandler::positionBaseMenu() }; BannerOverlayOptions bannerOptions; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); } else { bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); } +#else + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); +#endif screen->showOverlayBanner(bannerOptions); } @@ -2203,9 +2211,9 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; int options = 1; optionsArray[options] = "Number Picker"; @@ -2213,6 +2221,10 @@ void menuHandler::testMenu() optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; optionsEnumArray[options++] = ShowChirpy; +#ifdef HAS_I2S + optionsArray[options] = "Test Announce"; + optionsEnumArray[options++] = TestAnnounce; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Hidden Test Menu"; @@ -2227,6 +2239,10 @@ void menuHandler::testMenu() screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); + } else if (selected == TestAnnounce) { +#ifdef HAS_I2S + audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test."); +#endif } else { menuQueue = SystemBaseMenu; screen->runNow(); diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 39551b47e..3c14c2607 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -15,6 +15,7 @@ #include // GFXRoot drawing lib +#include "mesh/MeshModule.h" #include "mesh/MeshTypes.h" #include "./AppletFont.h" 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/src/main.cpp b/src/main.cpp index 8a46b3f5b..6f78c0b96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,7 +123,7 @@ void printPartitionTable() #include "AmbientLightingThread.h" #include "PowerFSMThread.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif @@ -657,7 +657,7 @@ void setup() } #endif -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER auto acc_info = i2cScanner->firstAccelerometer(); accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); @@ -737,7 +737,7 @@ void setup() #endif #if !MESHTASTIC_EXCLUDE_I2C -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER if (acc_info.type != ScanI2C::DeviceType::NONE) { accelerometerThread = new AccelerometerThread(acc_info.type); } diff --git a/src/main.h b/src/main.h index 91e27951f..56f048134 100644 --- a/src/main.h +++ b/src/main.h @@ -65,7 +65,7 @@ extern UdpMulticastHandler *udpHandler; // Global Screen singleton. extern graphics::Screen *screen; -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" extern AccelerometerThread *accelerometerThread; #endif 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/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 80741810a..440f7b76a 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -102,7 +102,6 @@ static int32_t reconnectETH() #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); - timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif @@ -159,6 +158,7 @@ static int32_t reconnectETH() LOG_ERROR("NTP Update failed"); ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes } + timeClient.end(); // W5100S: release UDP socket for other services } #endif diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7..762afc618 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -5,6 +5,8 @@ #include #include +#if !MESHTASTIC_EXCLUDE_WEBSERVER + void initWebServer(); void createSSLCert(); @@ -24,3 +26,20 @@ class WebServerThread : private concurrency::OSThread }; extern WebServerThread *webServerThread; + +#else +// Stub implementations when web server is excluded +inline void initWebServer() {} +inline void createSSLCert() {} + +class WebServerThread +{ + public: + WebServerThread() {} + uint32_t requestRestart = 0; + void markActivity() {} +}; + +inline WebServerThread *webServerThread = nullptr; + +#endif diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index e3a501097..9a52a9aff 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -40,7 +40,7 @@ #include "modules/PositionModule.h" #endif -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ @@ -664,7 +664,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) case meshtastic_Config_device_tag: LOG_INFO("Set config: Device"); config.has_device = true; -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && accelerometerThread->enabled == false) { config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; @@ -766,7 +767,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.bluetooth.enabled = false; } -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && accelerometerThread->enabled == false) { config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index cc7124f0e..3addc4b3a 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -350,12 +350,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP { // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); -#endif if (!isFromUs(&mp)) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. auto &p = mp.decoded; @@ -380,60 +374,69 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); - if (containsBell || !is_muted) { - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || - moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra || - ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz())) { - nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout - ? (moduleConfig.external_notification.nag_timeout * 1000) - : moduleConfig.external_notification.output_ms); - LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); - isNagging = true; - } + // Each output evaluates its own alert condition independently: + // alert_bell_* fires only when a bell character is present. + // alert_message_* fires on any non-muted message. - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { - LOG_INFO("externalNotificationModule - Notification Module or Bell"); - setExternalState(0, true); - } + // Alert when receiving a bell = alertBell: true + // Alert when receiving a message = alertMessage: true + const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) || + (moduleConfig.external_notification.alert_message && !is_muted); - if (moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); - setExternalState(1, true); - } + // Alert GPIO Vibra when receiving a bell = alertBellVibra: true + // Alert GPIO Vibra when receiving a message = alertMessageVibra: true + const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) || + (moduleConfig.external_notification.alert_message_vibra && !is_muted); - if ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz()) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); - if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } else { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us -#ifdef T_LORA_PAGER - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); + // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true + // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true + const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + + if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); + isNagging = true; + } + + if (genericShouldAlert) { + LOG_INFO("externalNotificationModule - Generic alert"); + setExternalState(0, true); + } + + if (vibraShouldAlert) { + LOG_INFO("externalNotificationModule - Vibra alert"); + setExternalState(1, true); + } + + if (buzzerShouldAlert) { + LOG_INFO("externalNotificationModule - Buzzer alert"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } else { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us +#ifdef HAS_DRV2605 + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); #endif + + if (moduleConfig.external_notification.use_i2s_as_buzzer) { #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); #endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } else { - setExternalState(2, true); - } + } else if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } } @@ -513,4 +516,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) return 1; } return 0; -} \ No newline at end of file +} 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/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 9724192c2..d2205fd2a 100644 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -4,7 +4,7 @@ #include "configuration.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "../concurrency/OSThread.h" #ifdef HAS_BMA423 diff --git a/src/platform/nrf52/Nrf52SaadcLock.cpp b/src/platform/nrf52/Nrf52SaadcLock.cpp new file mode 100644 index 000000000..21f4f4dfd --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.cpp @@ -0,0 +1,13 @@ +#include "Nrf52SaadcLock.h" +#include "concurrency/Lock.h" +#include "configuration.h" + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +static Lock nrf52SaadcLockInstance; +Lock *nrf52SaadcLock = &nrf52SaadcLockInstance; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/Nrf52SaadcLock.h b/src/platform/nrf52/Nrf52SaadcLock.h new file mode 100644 index 000000000..77024eea3 --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +class Lock; +/** Shared mutex for SAADC configuration and reads (VDD + battery analog path). */ +extern Lock *nrf52SaadcLock; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c705521ad..73780b6eb 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -25,6 +25,8 @@ #include "power.h" #include +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" #include #ifdef BQ25703A_ADDR @@ -51,6 +53,10 @@ uint16_t getVDDVoltage(); void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} +// Optional variant hook called each nrf52Loop(); e.g. for low-VDD System OFF. +void variant_nrf52LoopHook(void) __attribute__((weak)); +void variant_nrf52LoopHook(void) {} + static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; @@ -74,11 +80,18 @@ bool powerHAL_isVBUSConnected() bool powerHAL_isPowerLevelSafe() { - static bool powerLevelSafe = true; - uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV - uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_MV + uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD_MV; +#else + uint16_t threshold = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD * 1000.0f + 0.5f); // convert V to mV +#endif +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV + uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV; +#else + uint16_t hysteresis = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000.0f + 0.5f); +#endif if (powerLevelSafe) { if (getVDDVoltage() < threshold) { @@ -125,11 +138,12 @@ void powerHAL_platformInit() // get VDD voltage (in millivolts) uint16_t getVDDVoltage() { - // we use the same values as regular battery read so there is no conflict on SAADC + concurrency::LockGuard guard(concurrency::nrf52SaadcLock); + + // Match battery read resolution; SAADC is shared with AnalogBatteryLevel in Power.cpp. analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V - // let's hope battery reading runs in same task and we don't have race condition analogReference(AR_INTERNAL); uint16_t vddADCRead = analogReadVDD(); @@ -326,6 +340,8 @@ void nrf52Loop() checkSDEvents(); reportLittleFSCorruptionOnce(); + + variant_nrf52LoopHook(); // Optional variant hook called each nrf52Loop(); } #ifdef USE_SEMIHOSTING 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()); } diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index 701183280..bf8459f24 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -8,7 +8,7 @@ platform = platformio/espressif32@6.13.0 platform_packages = # renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs - platformio/tool-mklittlefs@^1.203.210628 + platformio/tool-mklittlefs@1.203.210628 extra_scripts = ${env.extra_scripts} @@ -70,11 +70,11 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index e0c05896d..f40f1d064 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -4,25 +4,29 @@ extends = esp32_common custom_esp32_kind = esp32 +build_src_filter = + ${esp32_common.build_src_filter} + - + - + build_flags = ${esp32_common.build_flags} -DMESHTASTIC_EXCLUDE_AUDIO=1 -; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra -; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets + -DMESHTASTIC_EXCLUDE_ACCELEROMETER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} + ${radiolib_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} - ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b78f12c86ea65c3ca08968840ff554ff7ed69b60.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 - # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master - https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + h2zero/NimBLE-Arduino@1.4.3 + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto - rweather/Crypto@0.4.0 \ No newline at end of file + rweather/Crypto@0.4.0 diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 16ecb99cc..b27ebf28e 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 Explorer custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER diff --git a/variants/esp32/nano-g1/platformio.ini b/variants/esp32/nano-g1/platformio.ini index 724e008c7..b2e392dbd 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1 diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index fad003b20..5a7f33485 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Station G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D STATION_G1 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index dbaccee8f..c9e6cce1f 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -10,14 +10,12 @@ custom_meshtastic_images = tbeam.svg custom_meshtastic_tags = LilyGo extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue -ULED_BUILTIN upload_speed = 921600 @@ -32,5 +30,5 @@ lib_deps = ${env:tbeam.lib_deps} # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip - # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index e2763fdec..1809ba56c 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D TBEAM_V07 diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index 9ab185d02..cdd9f9868 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -3,6 +3,9 @@ extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip +platform_packages = + # HACK: This release was automatically removed upstream + framework-arduinoespressif32 @ https://github.com/vidplace7/platform-espressif32/releases/download/meshtastic-esp32c6/framework-arduinoespressif32-all-release_v5.1-124d64e.zip build_flags = ${arduino_base.build_flags} -Wall 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 diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini index 91d2e568a..e2411bc6e 100644 --- a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -8,15 +8,17 @@ upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -D M5STACK_CARDPUTER_ADV - -D BOARD_HAS_PSRAM -D ARDUINO_USB_CDC_ON_BOOT=1 -I variants/esp32s3/m5stack_cardputer_adv +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/m5stack_cardputer_adv> lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip -# # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver -# https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp new file mode 100644 index 000000000..2bbe8e2e3 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp @@ -0,0 +1,40 @@ +#include "AudioBoard.h" +#include "configuration.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// M5stack Cardputer ADV specific init + +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); + + // extra ES8311 init + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 i2c address + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // CLOCK_MANAGER/ MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0dB) + es8311_write_reg(0x37, 0x08); // EQ bypass +} diff --git a/variants/esp32s3/t5s3_epaper/nicheGraphics.h b/variants/esp32s3/t5s3_epaper/nicheGraphics.h new file mode 100644 index 000000000..699a82de0 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/nicheGraphics.h @@ -0,0 +1,123 @@ +/* + +Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. +NicheGraphics attempts a different approach: + +Per-device config takes place in this setupNicheGraphics() method +(And a small amount in platformio.ini) + +This file sets up InkHUD for Heltec VM-E290. +Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. + +*/ + +#pragma once + +#include "configuration.h" +#include "mesh/MeshModule.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +// #include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + + // Setup backlight + // Note: AUX button behavior configured further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet); + // inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + + // Setup the main user button (0) + buttons->setWiring(0, BUTTON_PIN); + buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); }); + buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); }); + + // Setup the aux button (1) + // Bonus feature of VME290 + buttons->setWiring(1, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/pins_arduino.h b/variants/esp32s3/t5s3_epaper/pins_arduino.h new file mode 100644 index 000000000..4978cff2a --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/pins_arduino.h @@ -0,0 +1,43 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#if defined(T5_S3_EPAPER_PRO_V1) +// The default Wire will be mapped to RTC, Touch, BQ25896, and BQ27220 +static const uint8_t SDA = 6; +static const uint8_t SCL = 5; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 8; +static const uint8_t SCK = 18; + +#define SPI_MOSI (17) +#define SPI_SCK (18) +#define SPI_MISO (8) +#define SPI_CS (16) + +#else // T5_S3_EPAPER_PRO_V2 +// The default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, and BQ27220 +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 21; +static const uint8_t SCK = 14; + +#define SPI_MOSI (13) +#define SPI_SCK (14) +#define SPI_MISO (21) +#define SPI_CS (12) + +#endif + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini new file mode 100644 index 000000000..bad36706c --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -0,0 +1,61 @@ +[t5s3_epaper_base] +extends = esp32s3_base +board = t5-epaper-s3 +board_build.partition = default_16MB.csv +board_check = true +upload_protocol = esptool +build_flags = -fno-strict-aliasing + ${esp32_base.build_flags} + -I variants/esp32s3/t5s3_epaper + -D T5_S3_EPAPER_PRO + -D USE_EINK + -D USE_EINK_PARALLELDISPLAY + -D PRIVATE_HW + -D TOUCH_THRESHOLD_X=60 + -D TOUCH_THRESHOLD_Y=40 + -D TIME_LONG_PRESS=500 +; -D EINK_LIMIT_GHOSTING_PX=5000 + -D EPD_FULLSLOW_PERIOD=100 + -D FAST_EPD_PARTIAL_UPDATE_BUG ; use rect area update instead of partial + +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t5s3_epaper> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +[env:t5s3_epaper_inkhud] +extends = t5s3_epaper_base, inkhud +board_level = extra # inkhud port is incomplete +board_check = false +build_flags = + ${t5s3_epaper_base.build_flags} + ${inkhud.build_flags} + -D SDCARD_USE_SPI1 + -D T5_S3_EPAPER_PRO_V2 +build_src_filter = + ${t5s3_epaper_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${t5s3_epaper_base.lib_deps} + + +[env:t5s3-epaper-v1] ; H752 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V1 + -D GPS_DEFAULT_NOT_PRESENT=1 + +[env:t5s3-epaper-v2] ; H752-01 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V2 + -D SDCARD_USE_SPI1 + -D GPS_POWER_TOGGLE diff --git a/variants/esp32s3/t5s3_epaper/variant.cpp b/variants/esp32s3/t5s3_epaper/variant.cpp new file mode 100644 index 000000000..e10d7c347 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.cpp @@ -0,0 +1,47 @@ +#include "configuration.h" + +#ifdef T5_S3_EPAPER_PRO + +#include "TouchDrvGT911.hpp" +#include "Wire.h" +#include "input/TouchScreenImpl1.h" + +TouchDrvGT911 touch; + +bool readTouch(int16_t *x, int16_t *y) +{ + if (!digitalRead(GT911_PIN_INT)) { + int16_t raw_x; + int16_t raw_y; + if (touch.getPoint(&raw_x, &raw_y)) { + // rotate 90Β° for landscape + *x = raw_y; + *y = EPD_WIDTH - 1 - raw_x; + LOG_DEBUG("touched(%d/%d)", *x, *y); + return true; + } + } + return false; +} + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(BOARD_BL_EN, OUTPUT); +} + +// T5-S3-ePaper Pro specific (late-) init +void lateInitVariant(void) +{ + touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { + touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); + touchScreenImpl1->init(); + } else { + LOG_ERROR("Failed to find touch controller!"); + } +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/variant.h b/variants/esp32s3/t5s3_epaper/variant.h new file mode 100644 index 000000000..c2c001373 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.h @@ -0,0 +1,92 @@ + +// Display (E-Ink) ED047TC1 - 8bit parallel +#define EPD_WIDTH 960 +#define EPD_HEIGHT 540 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BOARD_BL_EN 40 +#else +#define BOARD_BL_EN 11 +#endif + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define HAS_TOUCHSCREEN 1 +#define GT911_PIN_SDA SDA +#define GT911_PIN_SCL SCL +#if defined(T5_S3_EPAPER_PRO_V1) +#define GT911_PIN_INT 15 +#define GT911_PIN_RST 41 +#else +#define GT911_PIN_INT 3 +#define GT911_PIN_RST 9 +#endif + +#define PCF85063_RTC 0x51 +#define HAS_RTC 1 +#define PCF85063_INT 2 + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GPS +#if !defined(T5_S3_EPAPER_PRO_V1) +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#endif + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BUTTON_PIN 48 +#define PIN_BUTTON2 0 +#define ALT_BUTTON_PIN PIN_BUTTON2 +#else +#define BUTTON_PIN 0 +#endif + +// SD card +#define HAS_SDCARD +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// battery charger BQ25896 +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// battery quality management BQ27220 +#define HAS_BQ27220 1 +#define BQ27220_I2C_SDA SDA +#define BQ27220_I2C_SCL SCL +#define BQ27220_DESIGN_CAPACITY 1500 + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK SCK +#define LORA_MISO MISO +#define LORA_MOSI MOSI +#define LORA_CS 46 + +#define LORA_DIO0 -1 +#if defined(T5_S3_EPAPER_PRO_V1) +#define LORA_RESET 43 +#define LORA_DIO1 3 // SX1262 IRQ +#define LORA_DIO2 44 // SX1262 BUSY +#define LORA_DIO3 +#else +#define LORA_RESET 1 +#define LORA_DIO1 10 // SX1262 IRQ +#define LORA_DIO2 47 // SX1262 BUSY +#define LORA_DIO3 +#endif + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 2.4 diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 17828f6f6..a247b0af1 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -34,8 +34,8 @@ lib_deps = adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library + adafruit/Adafruit BME680 Library@2.0.6 build_flags = ${arduino_base.build_flags} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini index 9a2b3a467..f96e6038c 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -12,4 +12,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 71508c037..ac7ef57c4 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -12,9 +12,15 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 - {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, - {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, + // clang-format off + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, + END_OF_MODE_TABLE, + // clang-format on }; diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index 7df05f9f5..f42c29308 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -25,6 +25,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -Os + -std=gnu++17 build_unflags = -Ofast -Og diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index a035fbaf0..a0394b2dd 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -19,7 +19,11 @@ */ #include "variant.h" +#include "Arduino.h" +#include "FreeRTOS.h" #include "nrf.h" +#include "power/PowerHAL.h" +#include "sleep.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -40,3 +44,39 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +#ifdef LOW_VDD_SYSTEMOFF_DELAY_MS +void variant_nrf52LoopHook(void) +{ + // If VDD stays unsafe for a while (brownout), force System OFF. + // Skip when VBUS present to allow recovery while USB-powered. + if (!powerHAL_isVBUSConnected()) { + // Rate-limit VDD safety checks: powerHAL_isPowerLevelSafe() calls getVDDVoltage() each time. + static constexpr uint32_t POWER_LEVEL_CHECK_INTERVAL_MS = 100; + static uint32_t last_vdd_check_ms = 0; + static bool last_power_level_safe = true; + + const uint32_t now = millis(); + if (last_vdd_check_ms == 0 || (uint32_t)(now - last_vdd_check_ms) >= POWER_LEVEL_CHECK_INTERVAL_MS) { + last_vdd_check_ms = now; + last_power_level_safe = powerHAL_isPowerLevelSafe(); + } + + // Do not use millis()==0 as a sentinel: at boot, millis() may be 0 while VDD is unsafe. + static bool low_vdd_timer_armed = false; + static uint32_t low_vdd_since_ms = 0; + + if (!last_power_level_safe) { + if (!low_vdd_timer_armed) { + low_vdd_since_ms = now; + low_vdd_timer_armed = true; + } + if ((uint32_t)(now - low_vdd_since_ms) >= (uint32_t)LOW_VDD_SYSTEMOFF_DELAY_MS) { + cpuDeepSleep(portMAX_DELAY); + } + } else { + low_vdd_timer_armed = false; + } + } +} +#endif diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 5b20e4d93..9ea215e42 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -225,7 +225,41 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 +#define OCV_ARRAY 4160, 4020, 3940, 3870, 3810, 3760, 3740, 3720, 3680, 3620, 2990 // updated OCV array for rak_wismeshtag + +// Wake from System OFF when battery rises again (LPCOMP). +// BAT_ADC divider: R22=1M (top), R24=1.5M (bottom) => V_BAT_ADC = VBAT * (1.5 / (1.0 + 1.5)) = 0.6 * VBAT +// RAK4630 module: AIN0 = nrf52840 AIN3 = Pin 5 (A0/BATTERY_PIN) +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 +// LPCOMP compares the selected input to a fraction of VDD (here 5/8 of VDD at the LPCOMP input). +// With VDD β‰ˆ 3.3 V: threshold at input β‰ˆ (5/8) * 3.3 V β‰ˆ 2.06 V. +// BAT_ADC divider: V_BAT_ADC = 0.6 * VBAT β†’ equivalent VBAT β‰ˆ 2.06 / 0.6 β‰ˆ 3.4 V (wake when battery recovers). +// +// Note: if VDD is drooping/tracking VBAT in the low-voltage region, using a fraction >= divider ratio helps ensure the +// input is below the threshold at shutdown; the intended wake event happens when the supply recovers enough for a rising +// crossing to occur. +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_5_8 + +// Low voltage protection: +// If VDD is below SAFE_VDD_VOLTAGE_THRESHOLD for longer than this delay (and no USB VBUS), +// the device will enter System OFF to avoid brownout loops and flash corruption. +#ifndef LOW_VDD_SYSTEMOFF_DELAY_MS +#define LOW_VDD_SYSTEMOFF_DELAY_MS 5000 +#endif + +// Prefer integer mV so platform code avoids floatβ†’int truncation quirks (e.g. 0.1 V β†’ 99 vs 100 mV). +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_MV 2900 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV 100 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD +#define SAFE_VDD_VOLTAGE_THRESHOLD (SAFE_VDD_VOLTAGE_THRESHOLD_MV / 1000.0f) +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST (SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV / 1000.0f) +#endif #define RAK_4631 1 diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 3f9a4f7af..7018d054e 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_DEFAULT + -DCONFIG_NFCT_PINS_AS_GPIOS=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink @@ -31,4 +32,4 @@ board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant - -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define \ No newline at end of file + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini index b77d54748..9ec9187e8 100644 --- a/variants/nrf52840/t-echo-plus/platformio.ini +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -22,5 +22,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo- lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 diff --git a/variants/nrf52840/wio-sdk-wm1110/platformio.ini b/variants/nrf52840/wio-sdk-wm1110/platformio.ini index 7c11ef6f6..9fac82289 100644 --- a/variants/nrf52840/wio-sdk-wm1110/platformio.ini +++ b/variants/nrf52840/wio-sdk-wm1110/platformio.ini @@ -9,6 +9,7 @@ extra_scripts = # Remove adafruit USB serial from the build (it is incompatible with using the ch340 serial chip on this board) build_unflags = + ${nrf52840_base.build_unflags} -Ofast -Og -ggdb3 diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index 7a3b37642..d2c155398 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -27,7 +27,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. - -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. + ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections