From 82ddf4732a60e70783dc40f5a6597df2d4f907ff Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 23 Jul 2025 05:57:17 -0500 Subject: [PATCH 01/25] Deprecate disable_triple_click config (#7425) --- src/input/ExpressLRSFiveWay.cpp | 2 +- src/mesh/NodeDB.cpp | 5 ----- src/modules/AdminModule.cpp | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 77f9e9993..776b9001d 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -199,7 +199,7 @@ void ExpressLRSFiveWay::sendKey(input_broker_event key) void ExpressLRSFiveWay::toggleGPS() { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS - if (!config.device.disable_triple_click && (gps != nullptr)) { + if (gps != nullptr) { gps->toggleGpsMode(); screen->startAlert("GPS Toggled"); alerting = true; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 38e213167..b7120a064 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -628,11 +628,6 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #ifdef PIN_GPS_EN config.position.gps_en_gpio = PIN_GPS_EN; #endif -#ifdef GPS_POWER_TOGGLE - config.device.disable_triple_click = false; -#else - config.device.disable_triple_click = true; -#endif #if defined(USERPREFS_CONFIG_GPS_MODE) config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE; #elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 8d3e710df..33d5e1016 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -596,7 +596,6 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && config.device.role == c.payload_variant.device.role && - config.device.disable_triple_click == c.payload_variant.device.disable_triple_click && config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) { requiresReboot = false; } From 54c0cbeb66535b365fb8cb94122481d6f4504034 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 07:46:34 -0500 Subject: [PATCH 02/25] Update meshtastic/device-ui digest to c75d545 (#7435) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ad7100efd..8bf56cf5b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -110,7 +110,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/86a09a7360f92d10053fbbf8d74f67f85b0ceb09.zip + https://github.com/meshtastic/device-ui/archive/c75d545bf9e8d1fe20051c319f427f711113ff22.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 46e2ae88600a40849d6187687fbcb0983c616819 Mon Sep 17 00:00:00 2001 From: saiman pokhrel <70017194+WOD-MN@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:39:43 +0545 Subject: [PATCH 03/25] =?UTF-8?q?Add=20Nepal=20865=E2=80=AFMHz=20to=20868?= =?UTF-8?q?=E2=80=AFMHz=20(#7380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tom Fifield Co-authored-by: Austin --- src/mesh/RadioInterface.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index faa67a1c2..985cfdf7f 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -169,6 +169,14 @@ const RegionInfo regions[] = { */ RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, true), + + /* + Nepal + 865 MHz to 868 MHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, specifically in non-cellular mode. + https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf + */ + RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ From 4eb6c9fb8ed694e3678761801ce7b254046b008a Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Wed, 23 Jul 2025 22:55:17 +1000 Subject: [PATCH 04/25] Add BR_902, Brazil 902MHz-907.5MHz (#7399) As reported by @barbabarros , the Brazilian government has specific support for LoRA[1] across multiple frequencies[2][3]. We currently support Brazil through the ANZ/AU915 band. However, Brazil also has another frequency available for use: 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions [1] https://sistemas.anatel.gov.br/anexar-api/publico/anexos/download/a028ab5cc4e3f97442830bba0c8bd1dd [2] https://informacoes.anatel.gov.br/legislacao/resolucoes/2025/2001-resolucao-772 [3] https://informacoes.anatel.gov.br/legislacao/atos-de-certificacao-de-produtos/2017/1139-ato-14448#item10 Protobuf patch: https://github.com/meshtastic/protobufs/pull/737 Fixes https://github.com/meshtastic/firmware/issues/3741 Co-authored-by: Austin --- src/mesh/RadioInterface.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 985cfdf7f..7590ac34d 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -67,6 +67,7 @@ const RegionInfo regions[] = { /* https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf + Also used in Brazil. */ RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), @@ -177,6 +178,13 @@ const RegionInfo regions[] = { */ RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false), + /* + Brazil + 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions + https://github.com/meshtastic/firmware/issues/3741 + */ + RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false), + /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ From 516597a73e99c6023046ce153d3f214482ef7ed9 Mon Sep 17 00:00:00 2001 From: Pedro <40841971+barbabarros@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:56:22 -0300 Subject: [PATCH 05/25] Add NP_865 and BR_902 to device menu (#7434) --- src/graphics/draw/MenuHandler.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 83198a7c5..5eaa2c6bf 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -51,12 +51,14 @@ void menuHandler::LoraRegionPicker(uint32_t duration) "PH_915", "ANZ_433", "KZ_433", - "KZ_863"}; + "KZ_863", + "NP_865", + "BR_902"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Set the LoRa region"; bannerOptions.durationMs = duration; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 25; + bannerOptions.optionsCount = 27; bannerOptions.InitialSelected = 0; bannerOptions.bannerCallback = [](int selected) -> void { if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { From 66a831dfa802ed3164b73886ac5fc0d8f1d5d685 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 23 Jul 2025 13:41:08 -0400 Subject: [PATCH 06/25] Actions: Combine embedded builds // split by variant subdir (#7417) --- .github/workflows/build_esp32.yml | 42 ------------------ .github/workflows/build_esp32_c3.yml | 42 ------------------ .github/workflows/build_esp32_c6.yml | 42 ------------------ .github/workflows/build_esp32_s3.yml | 42 ------------------ .github/workflows/build_firmware.yml | 66 ++++++++++++++++++++++++++++ .github/workflows/build_nrf52.yml | 42 ------------------ .github/workflows/build_rpi2040.yml | 40 ----------------- .github/workflows/build_stm32.yml | 41 ----------------- .github/workflows/main_matrix.yml | 51 +++++++++++---------- 9 files changed, 95 insertions(+), 313 deletions(-) delete mode 100644 .github/workflows/build_esp32.yml delete mode 100644 .github/workflows/build_esp32_c3.yml delete mode 100644 .github/workflows/build_esp32_c6.yml delete mode 100644 .github/workflows/build_esp32_s3.yml create mode 100644 .github/workflows/build_firmware.yml delete mode 100644 .github/workflows/build_nrf52.yml delete mode 100644 .github/workflows/build_rpi2040.yml delete mode 100644 .github/workflows/build_stm32.yml diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml deleted file mode 100644 index 2c4622f43..000000000 --- a/.github/workflows/build_esp32.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build ESP32 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-esp32: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build ESP32 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: esp32 - pio_env: ${{ inputs.board }} - pio_target: build - ota_firmware_source: firmware.bin - ota_firmware_target: release/bleota.bin - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-esp32-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.bin - release/*.elf diff --git a/.github/workflows/build_esp32_c3.yml b/.github/workflows/build_esp32_c3.yml deleted file mode 100644 index 3e7746166..000000000 --- a/.github/workflows/build_esp32_c3.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build ESP32-C3 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-esp32-c3: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build ESP32-C3 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: esp32 - pio_env: ${{ inputs.board }} - pio_target: build - ota_firmware_source: firmware-c3.bin - ota_firmware_target: release/bleota-c3.bin - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-esp32c3-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.bin - release/*.elf diff --git a/.github/workflows/build_esp32_c6.yml b/.github/workflows/build_esp32_c6.yml deleted file mode 100644 index 6f32eb3c6..000000000 --- a/.github/workflows/build_esp32_c6.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build ESP32-C6 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-esp32-c6: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build ESP32-C6 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: esp32 - pio_env: ${{ inputs.board }} - pio_target: build - ota_firmware_source: firmware-c3.bin - ota_firmware_target: release/bleota-c3.bin - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-esp32c6-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.bin - release/*.elf diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml deleted file mode 100644 index 6527d6d7c..000000000 --- a/.github/workflows/build_esp32_s3.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build ESP32-S3 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-esp32-s3: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build ESP32-S3 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: esp32 - pio_env: ${{ inputs.board }} - pio_target: build - ota_firmware_source: firmware-s3.bin - ota_firmware_target: release/bleota-s3.bin - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-esp32s3-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.bin - release/*.elf diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml new file mode 100644 index 000000000..df1035e62 --- /dev/null +++ b/.github/workflows/build_firmware.yml @@ -0,0 +1,66 @@ +name: Build + +on: + workflow_call: + inputs: + version: + required: true + type: string + platform: + required: true + type: string + pio_env: + required: true + type: string + +permissions: read-all + +jobs: + pio-build: + name: build-${{ inputs.platform }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - name: Set OTA firmware source and target + if: startsWith(inputs.platform, 'esp32') + id: ota_dir + env: + PIO_PLATFORM: ${{ inputs.platform }} + run: | + if [ "$PIO_PLATFORM" = "esp32s3" ]; then + echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT + echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT + elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then + echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT + echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT + elif [ "$PIO_PLATFORM" = "esp32" ]; then + echo "src=firmware.bin" >> $GITHUB_OUTPUT + echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT + fi + + - name: Build ${{ inputs.platform }} + id: build + uses: meshtastic/gh-action-firmware@main + with: + pio_platform: ${{ inputs.platform }} + pio_env: ${{ inputs.pio_env }} + pio_target: build + ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} + ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip + overwrite: true + path: | + release/*.bin + release/*.elf + release/*.uf2 + release/*.hex + release/*-ota.zip diff --git a/.github/workflows/build_nrf52.yml b/.github/workflows/build_nrf52.yml deleted file mode 100644 index 89be40187..000000000 --- a/.github/workflows/build_nrf52.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build NRF52 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-nrf52: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build NRF52 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: nrf52 - pio_env: ${{ inputs.board }} - pio_target: build - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-nrf52840-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.uf2 - release/*.elf - release/*.hex - release/*-ota.zip diff --git a/.github/workflows/build_rpi2040.yml b/.github/workflows/build_rpi2040.yml deleted file mode 100644 index fbaa21684..000000000 --- a/.github/workflows/build_rpi2040.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build RPI2040 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-rpi2040: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build Raspberry Pi 2040 - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: rp2xx0 - pio_env: ${{ inputs.board }} - pio_target: build - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-rp2040-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.uf2 - release/*.elf diff --git a/.github/workflows/build_stm32.yml b/.github/workflows/build_stm32.yml deleted file mode 100644 index f06e8f3b8..000000000 --- a/.github/workflows/build_stm32.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build STM32 - -on: - workflow_call: - inputs: - version: - required: true - type: string - board: - required: true - type: string - -permissions: read-all - -jobs: - build-stm32: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - name: Build STM32WL - id: build - uses: meshtastic/gh-action-firmware@main - with: - pio_platform: stm32wl - pio_env: ${{ inputs.board }} - pio_target: build - - - name: Store binaries as an artifact - uses: actions/upload-artifact@v4 - with: - name: firmware-stm32-${{ inputs.board }}-${{ inputs.version }}.zip - overwrite: true - path: | - release/*.hex - release/*.bin - release/*.elf diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index a3e8caf15..0760c0491 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -95,70 +95,77 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.esp32) }} - uses: ./.github/workflows/build_esp32.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: esp32 - build-esp32-s3: + build-esp32s3: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} - uses: ./.github/workflows/build_esp32_s3.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: esp32s3 - build-esp32-c3: + build-esp32c3: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} - uses: ./.github/workflows/build_esp32_c3.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: esp32c3 - build-esp32-c6: + build-esp32c6: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} - uses: ./.github/workflows/build_esp32_c6.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: esp32c6 - build-nrf52: + build-nrf52840: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} - uses: ./.github/workflows/build_nrf52.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: nrf52840 build-rpi2040: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} - uses: ./.github/workflows/build_rpi2040.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: rp2040 build-stm32: needs: [setup, version] strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.stm32) }} - uses: ./.github/workflows/build_stm32.yml + uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} - board: ${{ matrix.board }} + pio_env: ${{ matrix.board }} + platform: stm32 build-debian-src: if: github.repository == 'meshtastic/firmware' @@ -242,10 +249,10 @@ jobs: [ version, build-esp32, - build-esp32-s3, - build-esp32-c3, - build-esp32-c6, - build-nrf52, + build-esp32s3, + build-esp32c3, + build-esp32c6, + build-nrf52840, build-rpi2040, build-stm32, ] From 4f895f744b77c4c63d731f2247347e174c2d2fde Mon Sep 17 00:00:00 2001 From: Austin Date: Thu, 24 Jul 2025 07:13:23 -0400 Subject: [PATCH 07/25] Take control of our PRs! (#7445) --- .github/workflows/main_matrix.yml | 2 +- bin/generate_ci_matrix.py | 22 +++++++++++-------- variants/esp32/rak11200/platformio.ini | 1 + variants/esp32/tbeam/platformio.ini | 1 + .../esp32c3/heltec_esp32c3/platformio.ini | 1 + variants/esp32c6/tlora_c6/platformio.ini | 1 + variants/esp32s3/elecrow_panel/platformio.ini | 1 + variants/esp32s3/heltec_v3/platformio.ini | 1 + .../heltec_vision_master_e213/platformio.ini | 1 + variants/esp32s3/rak3312/platformio.ini | 1 + .../seeed-sensecap-indicator/platformio.ini | 2 +- variants/esp32s3/seeed_xiao_s3/platformio.ini | 1 + variants/esp32s3/station-g2/platformio.ini | 1 + variants/esp32s3/t-deck/platformio.ini | 1 + variants/esp32s3/t-eth-elite/platformio.ini | 1 + .../heltec_mesh_node_t114/platformio.ini | 1 + variants/nrf52840/rak4631/platformio.ini | 1 + .../seeed_xiao_nrf52840_kit/platformio.ini | 1 + variants/nrf52840/t-echo/platformio.ini | 2 ++ .../nrf52840/tracker-t1000-e/platformio.ini | 1 + variants/rp2040/rak11310/platformio.ini | 1 + variants/rp2040/rpipico/platformio.ini | 1 + variants/rp2040/rpipicow/platformio.ini | 1 + variants/rp2350/rpipico2/platformio.ini | 1 + variants/rp2350/rpipico2w/platformio.ini | 1 + variants/stm32/rak3172/platformio.ini | 1 + variants/stm32/wio-e5/platformio.ini | 1 + 27 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 0760c0491..c62d80888 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -45,7 +45,7 @@ jobs: if [[ "$GITHUB_HEAD_REF" == "" ]]; then TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) else - TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} quick) + TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr) fi echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS" echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index acc0a9fb7..aaa76aa45 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -45,24 +45,28 @@ for pio_env in pio_envs: all_envs.append(env) # Filter outputs based on options -# Check is currently mutually exclusive with other options +# Check is mutually exclusive with other options (except 'pr') if "check" in options: for env in all_envs: if env['board_check']: - outlist.append(env['name']) + if "pr" in options: + if env['board_level'] == 'pr': + outlist.append(env['name']) + else: + outlist.append(env['name']) # Filter (non-check) builds by platform else: for env in all_envs: if options[0] == env['platform']: - # If no board level is specified, always include it - if not env['board_level']: + # Always include board_level = 'pr' + if env['board_level'] == 'pr': outlist.append(env['name']) - # Include `extra` boards when requested + # Include board_level = 'extra' when requested elif "extra" in options and env['board_level'] == "extra": outlist.append(env['name']) + # If no board level is specified, include in release builds (not PR) + elif "pr" not in options and not env['board_level']: + outlist.append(env['name']) # Return as a JSON list -if ("quick" in options) and (len(outlist) > 3): - print(json.dumps(random.sample(outlist, 3))) -else: - print(json.dumps(outlist)) +print(json.dumps(outlist)) diff --git a/variants/esp32/rak11200/platformio.ini b/variants/esp32/rak11200/platformio.ini index 6149333f6..170e80b41 100644 --- a/variants/esp32/rak11200/platformio.ini +++ b/variants/esp32/rak11200/platformio.ini @@ -1,6 +1,7 @@ [env:rak11200] extends = esp32_base board = wiscore_rak11200 +board_level = pr board_check = true build_flags = ${esp32_base.build_flags} diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index 084a981da..ea17751c6 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -2,6 +2,7 @@ [env:tbeam] extends = esp32_base board = ttgo-t-beam +board_level = pr board_check = true lib_deps = ${esp32_base.lib_deps} diff --git a/variants/esp32c3/heltec_esp32c3/platformio.ini b/variants/esp32c3/heltec_esp32c3/platformio.ini index d21d64d2a..705e2e996 100644 --- a/variants/esp32c3/heltec_esp32c3/platformio.ini +++ b/variants/esp32c3/heltec_esp32c3/platformio.ini @@ -1,6 +1,7 @@ [env:heltec-ht62-esp32c3-sx1262] extends = esp32c3_base board = esp32-c3-devkitm-1 +board_level = pr build_flags = ${esp32_base.build_flags} -D HELTEC_HT62 diff --git a/variants/esp32c6/tlora_c6/platformio.ini b/variants/esp32c6/tlora_c6/platformio.ini index a06306add..6b402d7c5 100644 --- a/variants/esp32c6/tlora_c6/platformio.ini +++ b/variants/esp32c6/tlora_c6/platformio.ini @@ -1,6 +1,7 @@ [env:tlora-c6] extends = esp32c6_base board = esp32-c6-devkitm-1 +board_level = pr build_flags = ${esp32c6_base.build_flags} -D TLORA_C6 diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 203a4c0d0..59bc26000 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -98,6 +98,7 @@ build_flags = [env:elecrow-adv-35-tft] extends = crowpanel_small_esp32s3_base +board_level = pr build_flags = ${crowpanel_small_esp32s3_base.build_flags} -D LV_CACHE_DEF_SIZE=2097152 diff --git a/variants/esp32s3/heltec_v3/platformio.ini b/variants/esp32s3/heltec_v3/platformio.ini index 8dda72ceb..b521e11ca 100644 --- a/variants/esp32s3/heltec_v3/platformio.ini +++ b/variants/esp32s3/heltec_v3/platformio.ini @@ -1,6 +1,7 @@ [env:heltec-v3] extends = esp32s3_base board = heltec_wifi_lora_32_V3 +board_level = pr board_check = true board_build.partitions = default_8MB.csv build_flags = diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 2b4eebe64..43f6199af 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -24,6 +24,7 @@ upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] extends = esp32s3_base, inkhud board = heltec_vision_master_e213 +board_level = pr board_build.partitions = default_8MB.csv build_src_filter = ${esp32_base.build_src_filter} diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 50b0c5020..0de36498f 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -1,6 +1,7 @@ [env:rak3312] extends = esp32s3_base board = wiscore_rak3312 +board_level = pr board_check = true upload_protocol = esptool diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 1d55b31ca..f408054cf 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -31,7 +31,7 @@ lib_deps = ${esp32s3_base.lib_deps} [env:seeed-sensecap-indicator-tft] extends = env:seeed-sensecap-indicator -board_level = main +board_level = pr upload_speed = 460800 build_flags = diff --git a/variants/esp32s3/seeed_xiao_s3/platformio.ini b/variants/esp32s3/seeed_xiao_s3/platformio.ini index ad09efabd..ffc6e9638 100644 --- a/variants/esp32s3/seeed_xiao_s3/platformio.ini +++ b/variants/esp32s3/seeed_xiao_s3/platformio.ini @@ -1,6 +1,7 @@ [env:seeed-xiao-s3] extends = esp32s3_base board = seeed-xiao-s3 +board_level = pr board_check = true board_build.partitions = default_8MB.csv upload_protocol = esptool diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini index 0aed5e7ce..056d543d9 100755 --- a/variants/esp32s3/station-g2/platformio.ini +++ b/variants/esp32s3/station-g2/platformio.ini @@ -1,6 +1,7 @@ [env:station-g2] extends = esp32s3_base board = station-g2 +board_level = pr board_check = true board_build.partitions = default_16MB.csv board_build.mcu = esp32s3 diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 9d55ee365..7c8070c3e 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -19,6 +19,7 @@ lib_deps = ${esp32s3_base.lib_deps} [env:t-deck-tft] extends = env:t-deck +board_level = pr build_flags = ${env:t-deck.build_flags} diff --git a/variants/esp32s3/t-eth-elite/platformio.ini b/variants/esp32s3/t-eth-elite/platformio.ini index 889270ceb..6107185ce 100644 --- a/variants/esp32s3/t-eth-elite/platformio.ini +++ b/variants/esp32s3/t-eth-elite/platformio.ini @@ -1,6 +1,7 @@ [env:t-eth-elite] extends = esp32s3_base board = esp32s3box +board_level = pr board_check = true board_build.partitions = default_16MB.csv build_flags = diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index ead787bb1..c7b30b339 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -2,6 +2,7 @@ [env:heltec-mesh-node-t114] extends = nrf52840_base board = heltec_mesh_node_t114 +board_level = pr debug_tool = jlink # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini index 7b695779c..199e17570 100644 --- a/variants/nrf52840/rak4631/platformio.ini +++ b/variants/nrf52840/rak4631/platformio.ini @@ -2,6 +2,7 @@ [env:rak4631] extends = nrf52840_base board = wiscore_rak4631 +board_level = pr board_check = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak4631 diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 27352875d..623eace71 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -2,6 +2,7 @@ [env:seeed_xiao_nrf52840_kit] extends = nrf52840_base board = xiao_ble_sense +board_level = pr build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index b80958d5d..6541c9796 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -2,6 +2,7 @@ [env:t-echo] extends = nrf52840_base board = t-echo +board_level = pr board_check = true debug_tool = jlink @@ -27,6 +28,7 @@ lib_deps = [env:t-echo-inkhud] extends = nrf52840_base, inkhud board = t-echo +board_level = pr board_check = true debug_tool = jlink build_flags = diff --git a/variants/nrf52840/tracker-t1000-e/platformio.ini b/variants/nrf52840/tracker-t1000-e/platformio.ini index 45c8c5d00..c6c3f269c 100644 --- a/variants/nrf52840/tracker-t1000-e/platformio.ini +++ b/variants/nrf52840/tracker-t1000-e/platformio.ini @@ -1,6 +1,7 @@ [env:tracker-t1000-e] extends = nrf52840_base board = tracker-t1000-e +board_level = pr build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/tracker-t1000-e -Isrc/platform/nrf52/softdevice diff --git a/variants/rp2040/rak11310/platformio.ini b/variants/rp2040/rak11310/platformio.ini index aca24656b..f3eaa176e 100644 --- a/variants/rp2040/rak11310/platformio.ini +++ b/variants/rp2040/rak11310/platformio.ini @@ -1,6 +1,7 @@ [env:rak11310] extends = rp2040_base board = rakwireless_rak11300 +board_level = pr upload_protocol = picotool # add our variants files to the include and src paths build_flags = diff --git a/variants/rp2040/rpipico/platformio.ini b/variants/rp2040/rpipico/platformio.ini index 81db2a312..a6171bbac 100644 --- a/variants/rp2040/rpipico/platformio.ini +++ b/variants/rp2040/rpipico/platformio.ini @@ -1,6 +1,7 @@ [env:pico] extends = rp2040_base board = rpipico +board_level = pr upload_protocol = picotool # add our variants files to the include and src paths diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index f3fd07f8d..658d113d7 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -1,6 +1,7 @@ [env:picow] extends = rp2040_base board = rpipicow +board_level = pr upload_protocol = picotool # add our variants files to the include and src paths build_flags = diff --git a/variants/rp2350/rpipico2/platformio.ini b/variants/rp2350/rpipico2/platformio.ini index 485523eb0..ad7a4ce51 100644 --- a/variants/rp2350/rpipico2/platformio.ini +++ b/variants/rp2350/rpipico2/platformio.ini @@ -1,6 +1,7 @@ [env:pico2] extends = rp2350_base board = rpipico2 +board_level = pr upload_protocol = picotool # add our variants files to the include and src paths diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 3e5f2dbdd..59b1354b4 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -1,6 +1,7 @@ [env:pico2w] extends = rp2350_base board = rpipico2w +board_level = pr upload_protocol = jlink # debug settings for external openocd with RP2040 support (custom build) debug_tool = custom diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index 9799fc879..4f9edbb92 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -1,6 +1,7 @@ [env:rak3172] extends = stm32_base board = wiscore_rak3172 +board_level = pr board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index c057946dd..a9fcf51d6 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -1,6 +1,7 @@ [env:wio-e5] extends = stm32_base board = lora_e5_dev_board +board_level = pr board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} From 7a4a91531288bf250d53b4313f36fd4d7802d2b1 Mon Sep 17 00:00:00 2001 From: Wilson Date: Fri, 25 Jul 2025 06:23:45 +0800 Subject: [PATCH 08/25] Add Trace Route on BaseUI (#7386) * Add TraceRoute function to menus and modules to support node path tracing * Adjust text spacing and line wrapping logic in trace route result result. * Add HAS_SCREEN for TraceRouteModule drawFrame. --------- Co-authored-by: Tom Fifield Co-authored-by: Ben Meadors --- src/graphics/draw/MenuHandler.cpp | 31 +- src/graphics/draw/MenuHandler.h | 4 +- src/modules/TraceRouteModule.cpp | 585 +++++++++++++++++++++++++++++- src/modules/TraceRouteModule.h | 39 +- 4 files changed, 652 insertions(+), 7 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 5eaa2c6bf..731cae3df 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -14,6 +14,7 @@ #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" +#include "modules/TraceRouteModule.h" extern uint16_t TFT_MESH; @@ -428,7 +429,7 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, Remove, enumEnd }; + enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; static int optionsEnumArray[enumEnd] = {Back, Preset}; int options = 2; @@ -437,6 +438,8 @@ void menuHandler::favoriteBaseMenu() optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; @@ -453,6 +456,10 @@ void menuHandler::favoriteBaseMenu() } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::remove_favorite; screen->runNow(); + } else if (selected == TraceRoute) { + if (traceRouteModule) { + traceRouteModule->launch(graphics::UIRenderer::currentFavoriteNodeNum); + } } }; screen->showOverlayBanner(bannerOptions); @@ -491,12 +498,12 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, Verify, Reset }; - static const char *optionsArray[] = {"Back", "Add Favorite", "Key Verification", "Reset NodeDB"}; + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; + static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; + bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -507,6 +514,9 @@ void menuHandler::nodeListMenu() } else if (selected == Reset) { menuQueue = reset_node_db_menu; screen->runNow(); + } else if (selected == TraceRoute) { + menuQueue = trace_route_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -859,6 +869,16 @@ void menuHandler::removeFavoriteMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::traceRouteMenu() +{ + screen->showNodePicker("Node to Trace", 30000, [](uint32_t nodenum) -> void { + LOG_INFO("Menu: Node picker selected node 0x%08x, traceRouteModule=%p", nodenum, traceRouteModule); + if (traceRouteModule) { + traceRouteModule->startTraceRoute(nodenum); + } + }); +} + void menuHandler::testMenu() { @@ -1131,6 +1151,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case remove_favorite: removeFavoriteMenu(); break; + case trace_route_menu: + traceRouteMenu(); + break; case test_menu: testMenu(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1f989be79..2e4923241 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -36,7 +36,8 @@ class menuHandler system_base_menu, key_verification_init, key_verification_final_prompt, - throttle_message + trace_route_menu, + throttle_message, }; static screenMenus menuQueue; @@ -64,6 +65,7 @@ class menuHandler static void shutdownMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); + static void traceRouteMenu(); static void testMenu(); static void numberTest(); static void wifiBaseMenu(); diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41cb35649..bd75c6983 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -1,6 +1,13 @@ #include "TraceRouteModule.h" #include "MeshService.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "mesh/Router.h" #include "meshUtils.h" +#include + +extern graphics::Screen *screen; TraceRouteModule *traceRouteModule; @@ -27,6 +34,123 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti // Set updated route to the payload of the to be flooded packet p.decoded.payload.size = pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, r); + + if (tracingNode != 0) { + // check isResponseFromTarget + bool isResponseFromTarget = (incoming.request_id != 0 && p.from == tracingNode); + bool isRequestToUs = (incoming.request_id == 0 && p.to == nodeDB->getNodeNum() && tracingNode != 0); + + // Check if this is a trace route response containing our target node + bool containsTargetNode = false; + for (uint8_t i = 0; i < r->route_count; i++) { + if (r->route[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + for (uint8_t i = 0; i < r->route_back_count; i++) { + if (r->route_back[i] == tracingNode) { + containsTargetNode = true; + break; + } + } + + // Check if this response contains a complete route to our target + bool hasCompleteRoute = (r->route_count > 0 && r->route_back_count > 0) || + (containsTargetNode && (r->route_count > 0 || r->route_back_count > 0)); + + LOG_INFO("TracRoute packet analysis: tracingNode=0x%08x, p.from=0x%08x, p.to=0x%08x, request_id=0x%08x", tracingNode, + p.from, p.to, incoming.request_id); + LOG_INFO("TracRoute conditions: isResponseFromTarget=%d, isRequestToUs=%d, containsTargetNode=%d, hasCompleteRoute=%d", + isResponseFromTarget, isRequestToUs, containsTargetNode, hasCompleteRoute); + + if (isResponseFromTarget || isRequestToUs || (containsTargetNode && hasCompleteRoute)) { + LOG_INFO("TracRoute result detected: isResponseFromTarget=%d, isRequestToUs=%d", isResponseFromTarget, isRequestToUs); + + LOG_INFO("SNR arrays - towards_count=%d, back_count=%d", r->snr_towards_count, r->snr_back_count); + for (int i = 0; i < r->snr_towards_count; i++) { + LOG_INFO("SNR towards[%d] = %d (%.1fdB)", i, r->snr_towards[i], (float)r->snr_towards[i] / 4.0f); + } + for (int i = 0; i < r->snr_back_count; i++) { + LOG_INFO("SNR back[%d] = %d (%.1fdB)", i, r->snr_back[i], (float)r->snr_back[i] / 4.0f); + } + + String result = ""; + + // Show request path (from initiator to target) + if (r->route_count > 0) { + result += getNodeName(nodeDB->getNodeNum()); + for (uint8_t i = 0; i < r->route_count; i++) { + result += " > "; + const char *name = getNodeName(r->route[i]); + float snr = + (i < r->snr_towards_count && r->snr_towards[i] != INT8_MIN) ? ((float)r->snr_towards[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[r->snr_towards_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[r->snr_towards_count - 1] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } else { + // Direct connection (no intermediate hops) + result += getNodeName(nodeDB->getNodeNum()); + result += " > "; + result += getNodeName(tracingNode); + if (r->snr_towards_count > 0 && r->snr_towards[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_towards[0] / 4.0f, 1); + result += "dB)"; + } + result += "\n"; + } + + // Show response path (from target back to initiator) + if (r->route_back_count > 0) { + result += getNodeName(tracingNode); + for (int8_t i = r->route_back_count - 1; i >= 0; i--) { + result += " > "; + const char *name = getNodeName(r->route_back[i]); + float snr = (i < r->snr_back_count && r->snr_back[i] != INT8_MIN) ? ((float)r->snr_back[i] / 4.0f) : 0.0f; + result += name; + if (snr != 0.0f) { + result += "("; + result += String(snr, 1); + result += "dB)"; + } + } + // add initiator node + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[r->snr_back_count - 1] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[r->snr_back_count - 1] / 4.0f, 1); + result += "dB)"; + } + } else { + // Direct return path (no intermediate hops) + result += getNodeName(tracingNode); + result += " > "; + result += getNodeName(nodeDB->getNodeNum()); + if (r->snr_back_count > 0 && r->snr_back[0] != INT8_MIN) { + result += "("; + result += String((float)r->snr_back[0] / 4.0f, 1); + result += "dB)"; + } + } + + LOG_INFO("Trace route result: %s", result.c_str()); + handleTraceRouteResult(result); + } + } } void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination) @@ -173,8 +297,467 @@ meshtastic_MeshPacket *TraceRouteModule::allocReply() } TraceRouteModule::TraceRouteModule() - : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg) + : ProtobufModule("traceroute", meshtastic_PortNum_TRACEROUTE_APP, &meshtastic_RouteDiscovery_msg), OSThread("TraceRoute") { ourPortNum = meshtastic_PortNum_TRACEROUTE_APP; isPromiscuous = true; // We need to update the route even if it is not destined to us +} + +const char *TraceRouteModule::getNodeName(NodeNum node) +{ + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user) { + if (strlen(info->user.short_name) > 0) { + return info->user.short_name; + } + if (strlen(info->user.long_name) > 0) { + return info->user.long_name; + } + } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; +} + +bool TraceRouteModule::startTraceRoute(NodeNum node) +{ + LOG_INFO("=== TraceRoute startTraceRoute CALLED: node=0x%08x ===", node); + unsigned long now = millis(); + + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return false; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + LOG_INFO("TraceRoute already in progress"); + return false; + } + + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + // Cooldown + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return false; + } + + tracingNode = node; + lastTraceRouteTime = now; + runState = TRACEROUTE_STATE_TRACKING; + bannerText = String("Tracing ") + getNodeName(node); + + LOG_INFO("TraceRoute UI: Starting trace route to node 0x%08x, requesting focus", node); + + // 请求焦点,然后触发UI更新事件 + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + // 设置定时器来处理超时检查 + setIntervalFromNow(1000); // 每秒检查一次状态 + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + // Allocate a packet directly from router like the reference code + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + // Set destination and port + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + // Manually encode the RouteDiscovery payload + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + LOG_INFO("About to call service->sendToMesh..."); + + if (service) { + LOG_INFO("MeshService is available, sending packet..."); + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e2; + e2.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e2); + return false; + } + return true; +} + +void TraceRouteModule::launch(NodeNum node) +{ + if (node == 0 || node == NODENUM_BROADCAST) { + LOG_ERROR("Invalid node number for trace route: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Invalid node"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (node == nodeDB->getNodeNum()) { + LOG_ERROR("Cannot trace route to self: 0x%08x", node); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Cannot trace self"; + resultShowTime = millis(); + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return; + } + + if (!initialized) { + lastTraceRouteTime = 0; + initialized = true; + LOG_INFO("TraceRoute initialized for first time"); + } + + unsigned long now = millis(); + if (initialized && lastTraceRouteTime > 0 && now - lastTraceRouteTime < cooldownMs) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + bannerText = String("Wait for ") + String(wait) + String("s"); + runState = TRACEROUTE_STATE_COOLDOWN; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + LOG_INFO("Cooldown active, please wait %lu seconds before starting a new trace route.", wait); + return; + } + + runState = TRACEROUTE_STATE_TRACKING; + tracingNode = node; + lastTraceRouteTime = now; + bannerText = String("Tracing ") + getNodeName(node); + + requestFocus(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(1000); + + meshtastic_RouteDiscovery req = meshtastic_RouteDiscovery_init_zero; + LOG_INFO("Creating RouteDiscovery protobuf..."); + + meshtastic_MeshPacket *p = router->allocForSending(); + if (p) { + p->to = node; + p->decoded.portnum = meshtastic_PortNum_TRACEROUTE_APP; + p->decoded.want_response = true; + + p->decoded.payload.size = + pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_RouteDiscovery_msg, &req); + + LOG_INFO("Packet allocated successfully: to=0x%08x, portnum=%d, want_response=%d, payload_size=%d", p->to, + p->decoded.portnum, p->decoded.want_response, p->decoded.payload.size); + + if (service) { + service->sendToMesh(p, RX_SRC_USER); + LOG_INFO("sendToMesh called successfully for trace route to node 0x%08x", node); + } else { + LOG_ERROR("MeshService is NULL!"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Service unavailable"; + resultShowTime = millis(); + tracingNode = 0; + } + } else { + LOG_ERROR("Failed to allocate TraceRoute packet from router"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "Failed to send"; + resultShowTime = millis(); + tracingNode = 0; + } +} + +void TraceRouteModule::handleTraceRouteResult(const String &result) +{ + resultText = result; + runState = TRACEROUTE_STATE_RESULT; + resultShowTime = millis(); + tracingNode = 0; + + LOG_INFO("TraceRoute result ready, requesting focus. Result: %s", result.c_str()); + + setIntervalFromNow(1000); + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + LOG_INFO("=== TraceRoute handleTraceRouteResult END ==="); +} + +bool TraceRouteModule::shouldDraw() +{ + bool draw = (runState != TRACEROUTE_STATE_IDLE); + static TraceRouteRunState lastLoggedState = TRACEROUTE_STATE_IDLE; + if (runState != lastLoggedState) { + LOG_INFO("TraceRoute shouldDraw: runState=%d, draw=%d", runState, draw); + lastLoggedState = runState; + } + return draw; +} +#if HAS_SCREEN +void TraceRouteModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + LOG_DEBUG("TraceRoute drawFrame called: runState=%d", runState); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (runState == TRACEROUTE_STATE_TRACKING) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + + } else if (runState == TRACEROUTE_STATE_RESULT) { + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->drawString(x, y, "Route Result"); + + int contentStartY = y + FONT_HEIGHT_MEDIUM + 2; // Add more spacing after title + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + if (resultText.length() > 0) { + std::vector lines; + String currentLine = ""; + int maxWidth = display->getWidth() - 4; + + int start = 0; + int newlinePos = resultText.indexOf('\n', start); + + while (newlinePos != -1 || start < resultText.length()) { + String segment; + if (newlinePos != -1) { + segment = resultText.substring(start, newlinePos); + start = newlinePos + 1; + newlinePos = resultText.indexOf('\n', start); + } else { + segment = resultText.substring(start); + start = resultText.length(); + } + + if (display->getStringWidth(segment) <= maxWidth) { + lines.push_back(segment); + } else { + // Try to break at better positions (space, >, <, -) + String remaining = segment; + + while (remaining.length() > 0) { + String tempLine = ""; + int lastGoodBreak = -1; + bool lineComplete = false; + + for (int i = 0; i < remaining.length(); i++) { + char ch = remaining.charAt(i); + String testLine = tempLine + ch; + + if (display->getStringWidth(testLine) > maxWidth) { + if (lastGoodBreak >= 0) { + // Break at the last good position + lines.push_back(remaining.substring(0, lastGoodBreak + 1)); + remaining = remaining.substring(lastGoodBreak + 1); + lineComplete = true; + break; + } else if (tempLine.length() > 0) { + lines.push_back(tempLine); + remaining = remaining.substring(i); + lineComplete = true; + break; + } else { + // Single character exceeds width + lines.push_back(String(ch)); + remaining = remaining.substring(i + 1); + lineComplete = true; + break; + } + } else { + tempLine = testLine; + // Mark good break positions + if (ch == ' ' || ch == '>' || ch == '<' || ch == '-' || ch == '(' || ch == ')') { + lastGoodBreak = i; + } + } + } + + if (!lineComplete) { + // Reached end of remaining text + if (tempLine.length() > 0) { + lines.push_back(tempLine); + } + break; + } + } + } + } + + int lineHeight = FONT_HEIGHT_SMALL + 1; // Use proper font height with 1px spacing + for (size_t i = 0; i < lines.size(); i++) { + int lineY = contentStartY + (i * lineHeight); + if (lineY + FONT_HEIGHT_SMALL <= display->getHeight()) { + display->drawString(x + 2, lineY, lines[i]); + } + } + } + + } else if (runState == TRACEROUTE_STATE_COOLDOWN) { + display->setFont(FONT_MEDIUM); + int centerY = y + (display->getHeight() / 2) - (FONT_HEIGHT_MEDIUM / 2); + display->drawString(display->getWidth() / 2 + x, centerY, bannerText); + } +} +#endif // HAS_SCREEN +int32_t TraceRouteModule::runOnce() +{ + unsigned long now = millis(); + + if (runState == TRACEROUTE_STATE_IDLE) { + return INT32_MAX; + } + + // Check for tracking timeout + if (runState == TRACEROUTE_STATE_TRACKING && now - lastTraceRouteTime > trackingTimeoutMs) { + LOG_INFO("TraceRoute timeout, no response received"); + runState = TRACEROUTE_STATE_RESULT; + resultText = "No response received"; + resultShowTime = now; + tracingNode = 0; + + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + setIntervalFromNow(resultDisplayMs); + return resultDisplayMs; + } + + // Update cooldown display every second + if (runState == TRACEROUTE_STATE_COOLDOWN) { + unsigned long wait = (cooldownMs - (now - lastTraceRouteTime)) / 1000; + if (wait > 0) { + String newBannerText = String("Wait for ") + String(wait) + String("s"); + bannerText = newBannerText; + LOG_INFO("TraceRoute cooldown: updating banner to %s", bannerText.c_str()); + + // Force flash UI + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + + if (screen) { + screen->forceDisplay(); + } + + return 1000; + } else { + // Cooldown finished + LOG_INFO("TraceRoute cooldown finished, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + bannerText = ""; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } + } + + if (runState == TRACEROUTE_STATE_RESULT) { + if (now - resultShowTime >= resultDisplayMs) { + LOG_INFO("TraceRoute result display timeout, returning to IDLE"); + runState = TRACEROUTE_STATE_IDLE; + resultText = ""; + bannerText = ""; + tracingNode = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return INT32_MAX; + } else { + return 1000; + } + } + + if (runState == TRACEROUTE_STATE_TRACKING) { + return 1000; + } + + return INT32_MAX; } \ No newline at end of file diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index afe2b3871..51d98826e 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -1,16 +1,40 @@ #pragma once #include "ProtobufModule.h" +#include "concurrency/OSThread.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "input/InputBroker.h" +#if HAS_SCREEN +#include "OLEDDisplayUi.h" +#endif #define ROUTE_SIZE sizeof(((meshtastic_RouteDiscovery *)0)->route) / sizeof(((meshtastic_RouteDiscovery *)0)->route[0]) /** * A module that traces the route to a certain destination node */ -class TraceRouteModule : public ProtobufModule +enum TraceRouteRunState { TRACEROUTE_STATE_IDLE, TRACEROUTE_STATE_TRACKING, TRACEROUTE_STATE_RESULT, TRACEROUTE_STATE_COOLDOWN }; + +class TraceRouteModule : public ProtobufModule, + public Observable, + private concurrency::OSThread { public: TraceRouteModule(); + bool startTraceRoute(NodeNum node); + void launch(NodeNum node); + void handleTraceRouteResult(const String &result); + bool shouldDraw(); +#if HAS_SCREEN + virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; +#endif + + const char *getNodeName(NodeNum node); + + virtual bool wantUIFrame() override { return shouldDraw(); } + virtual Observable *getUIFrameObservable() override { return this; } + protected: bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_RouteDiscovery *r) override; @@ -20,6 +44,8 @@ class TraceRouteModule : public ProtobufModule the route array containing the IDs of nodes this packet went through */ void alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) override; + virtual int32_t runOnce() override; + private: // Call to add unknown hops (e.g. when a node couldn't decrypt it) to the route based on hopStart and current hopLimit void insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r, bool isTowardsDestination); @@ -31,6 +57,17 @@ class TraceRouteModule : public ProtobufModule Set origin to where the request came from. Set dest to the ID of its destination, or NODENUM_BROADCAST if it has not yet arrived there. */ void printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination); + + TraceRouteRunState runState = TRACEROUTE_STATE_IDLE; + unsigned long lastTraceRouteTime = 0; + unsigned long resultShowTime = 0; + unsigned long cooldownMs = 30000; + unsigned long resultDisplayMs = 10000; + unsigned long trackingTimeoutMs = 10000; + String bannerText; + String resultText; + NodeNum tracingNode = 0; + bool initialized = false; }; extern TraceRouteModule *traceRouteModule; \ No newline at end of file From d1fbf65c5d7db35e8ea52553d92aa6ba92596a61 Mon Sep 17 00:00:00 2001 From: Pedro <40841971+barbabarros@users.noreply.github.com> Date: Thu, 24 Jul 2025 23:57:40 -0300 Subject: [PATCH 09/25] Fix timezone definition for UTC in TZPicker function (#7442) --- src/graphics/draw/MenuHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 731cae3df..dc6470565 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -188,7 +188,7 @@ void menuHandler::TZPicker() } else if (selected == 7) { // Eastern strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); } else if (selected == 8) { // UTC - strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); + strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); } else if (selected == 9) { // EU/Western strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); } else if (selected == 10) { // EU/Central From bbe548bc980827145114b76260b1b2377dd633e5 Mon Sep 17 00:00:00 2001 From: Pedro <40841971+barbabarros@users.noreply.github.com> Date: Fri, 25 Jul 2025 00:42:42 -0300 Subject: [PATCH 10/25] Add BRT3 timezone option to TZPicker menu (#7438) --- src/graphics/draw/MenuHandler.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index dc6470565..cf19c4825 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -154,6 +154,7 @@ void menuHandler::TZPicker() "US/Mountain", "US/Central", "US/Eastern", + "BR/Brasilia", "UTC", "EU/Western", "EU/" @@ -168,7 +169,7 @@ void menuHandler::TZPicker() BannerOverlayOptions bannerOptions; bannerOptions.message = "Pick Timezone"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 17; + bannerOptions.optionsCount = 19; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { menuHandler::menuQueue = menuHandler::clock_menu; @@ -187,25 +188,27 @@ void menuHandler::TZPicker() strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); } else if (selected == 7) { // Eastern strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 8) { // UTC + } else if (selected == 8) { // Brazil + strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef)); + } else if (selected == 9) { // UTC strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); - } else if (selected == 9) { // EU/Western + } else if (selected == 10) { // EU/Western strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Central + } else if (selected == 11) { // EU/Central strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 11) { // EU/Eastern + } else if (selected == 12) { // EU/Eastern strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 12) { // Asia/Kolkata + } else if (selected == 13) { // Asia/Kolkata strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 13) { // China + } else if (selected == 14) { // China strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 14) { // AU/AWST + } else if (selected == 15) { // AU/AWST strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/ACST + } else if (selected == 16) { // AU/ACST strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 16) { // AU/AEST + } else if (selected == 17) { // AU/AEST strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 17) { // NZ + } else if (selected == 18) { // NZ strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); } if (selected != 0) { From 4c6db2c5bdf04856e31e25668e28b8583e9e0925 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 25 Jul 2025 08:10:35 -0500 Subject: [PATCH 11/25] Fix MHz label (#7455) --- src/graphics/draw/DebugRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5420d1b4b..5d9b5a33b 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -412,9 +412,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { From 4f57a2e2485778d92a98bc2e719a6314cc45539d Mon Sep 17 00:00:00 2001 From: Austin Date: Fri, 25 Jul 2025 16:25:50 -0400 Subject: [PATCH 12/25] Build RP2350 (Pi Pico 2) (#7441) --- .github/workflows/main_matrix.yml | 51 +++++++++++++++++++++--- bin/build-firmware.sh | 2 +- variants/rp2040/rpipicow/platformio.ini | 1 + variants/rp2350/rpipico2w/platformio.ini | 1 + 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index c62d80888..a98bdc011 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -30,7 +30,16 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - check runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -56,6 +65,7 @@ jobs: esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }} nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }} rp2040: ${{ steps.jsonStep.outputs.rp2040 }} + rp2350: ${{ steps.jsonStep.outputs.rp2350 }} stm32: ${{ steps.jsonStep.outputs.stm32 }} check: ${{ steps.jsonStep.outputs.check }} @@ -145,7 +155,7 @@ jobs: pio_env: ${{ matrix.board }} platform: nrf52840 - build-rpi2040: + build-rp2040: needs: [setup, version] strategy: fail-fast: false @@ -156,6 +166,17 @@ jobs: pio_env: ${{ matrix.board }} platform: rp2040 + build-rp2350: + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.rp2350) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2350 + build-stm32: needs: [setup, version] strategy: @@ -243,7 +264,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest needs: [ @@ -253,7 +282,8 @@ jobs: build-esp32c3, build-esp32c6, build-nrf52840, - build-rpi2040, + build-rp2040, + build-rp2350, build-stm32, ] steps: @@ -392,7 +422,15 @@ jobs: strategy: fail-fast: false matrix: - arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32] + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-artifacts, version] @@ -449,7 +487,8 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' }} needs: [release-firmware, version] env: - targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32 + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bin/build-firmware.sh b/bin/build-firmware.sh index c53f1b660..fdd7caa11 100644 --- a/bin/build-firmware.sh +++ b/bin/build-firmware.sh @@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then elif (echo $2 | grep -q "stm32"); then bin/build-stm32.sh $1 elif (echo $2 | grep -q "rpi2040"); then - bin/build-rpi2040.sh $1 + bin/build-rp2xx0.sh $1 else echo "Unknown target $2" exit 1 diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index 658d113d7..60845ba39 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -2,6 +2,7 @@ extends = rp2040_base board = rpipicow board_level = pr +board_check = true upload_protocol = picotool # add our variants files to the include and src paths build_flags = diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 59b1354b4..5dbce533b 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -2,6 +2,7 @@ extends = rp2350_base board = rpipico2w board_level = pr +board_check = true upload_protocol = jlink # debug settings for external openocd with RP2040 support (custom build) debug_tool = custom From bbc638ab82b3834d11325067f38641c0112c2459 Mon Sep 17 00:00:00 2001 From: Iris Date: Fri, 25 Jul 2025 23:36:37 +0300 Subject: [PATCH 13/25] Create Platformio.ini (#7450) --- variants/nrf52840/diy/WashTastic/Platformio.ini | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 variants/nrf52840/diy/WashTastic/Platformio.ini diff --git a/variants/nrf52840/diy/WashTastic/Platformio.ini b/variants/nrf52840/diy/WashTastic/Platformio.ini new file mode 100644 index 000000000..e67c25910 --- /dev/null +++ b/variants/nrf52840/diy/WashTastic/Platformio.ini @@ -0,0 +1,13 @@ +; Promicro + E22900M30S +[env:WashTastic] +extends = nrf52840_base +board = promicro-nrf52840 +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo + -D PRIVATE_HW + -D EBYTE_E22_900M30S +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink \ No newline at end of file From fc1e6ccb8cd90463c9e2647e3fba9c10b7543ad3 Mon Sep 17 00:00:00 2001 From: Iris Date: Sat, 26 Jul 2025 13:13:02 +0300 Subject: [PATCH 14/25] Rename Platformio.ini to platformio.ini (#7468) --- .../nrf52840/diy/WashTastic/{Platformio.ini => platformio.ini} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename variants/nrf52840/diy/WashTastic/{Platformio.ini => platformio.ini} (95%) diff --git a/variants/nrf52840/diy/WashTastic/Platformio.ini b/variants/nrf52840/diy/WashTastic/platformio.ini similarity index 95% rename from variants/nrf52840/diy/WashTastic/Platformio.ini rename to variants/nrf52840/diy/WashTastic/platformio.ini index e67c25910..881b961e1 100644 --- a/variants/nrf52840/diy/WashTastic/Platformio.ini +++ b/variants/nrf52840/diy/WashTastic/platformio.ini @@ -10,4 +10,4 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> lib_deps = ${nrf52840_base.lib_deps} -debug_tool = jlink \ No newline at end of file +debug_tool = jlink From a506dc6b65d8d4dbf5b7106de598e89d8d25484f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 26 Jul 2025 05:38:24 -0500 Subject: [PATCH 15/25] Fix MQTT config bugs (#7446) * Fix mqtt config bugs * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add client notification * Verbiage * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mqtt/MQTT.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove test that is no longer true * This test no longer exists * Fix client notification crap * Suppress false positive * Revert "Suppress false positive" This reverts commit bead96eaee31f11ed631c016eb0424055e923a29. * Try macro exclusion * Derp * Fix --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jason P --- src/mesh/MeshService.h | 2 +- src/modules/AdminModule.cpp | 7 ++++++- src/mqtt/MQTT.cpp | 27 +++++++++++++++++++++------ test/test_mqtt/MQTT.cpp | 23 +++++++++++++---------- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 89d3b15d0..f7d79366e 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -146,7 +146,7 @@ class MeshService virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); /// Send a ClientNotification to the phone - void sendClientNotification(meshtastic_ClientNotification *cn); + virtual void sendClientNotification(meshtastic_ClientNotification *cn); /// Send an error response to the phone void sendRoutingErrorResponse(meshtastic_Routing_Error error, const meshtastic_MeshPacket *mp); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 33d5e1016..4b910a959 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -798,8 +798,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - if (!hasOpenEditTransaction) + // If we are in an open transaction or configuring MQTT, defer disabling Bluetooth + // Otherwise, disable Bluetooth to prevent the phone from interfering with the config + if (!hasOpenEditTransaction && c.which_payload_variant != meshtastic_ModuleConfig_mqtt_tag) disableBluetooth(); + switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: #if MESHTASTIC_EXCLUDE_MQTT @@ -810,6 +813,8 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) if (!MQTT::isValidConfig(c.payload_variant.mqtt)) { return false; } + // Disable Bluetooth to prevent interference during MQTT configuration + disableBluetooth(); moduleConfig.has_mqtt = true; moduleConfig.mqtt = c.payload_variant.mqtt; #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 091612827..21d4a8fa0 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -39,6 +39,7 @@ #include #define ntohl __ntohl #endif +#include MQTT *mqtt; @@ -624,18 +625,32 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); } #else - LOG_ERROR("Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"); + const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif return false; #endif } const bool defaultServer = isDefaultServer(parsed.serverAddr); - if (defaultServer && config.tls_enabled) { - LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS"); - return false; - } if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) { - LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort); + const char *warning = "Invalid MQTT config: default server address must not have a port specified"; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination + service->sendClientNotification(cn); +#endif return false; } return true; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 8047079ba..32d81f6b4 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -27,6 +27,12 @@ #include #include +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + namespace { // Minimal router needed to receive messages from MQTT. @@ -56,7 +62,13 @@ class MockMeshService : public MeshService messages_.emplace_back(*m); releaseMqttClientProxyMessageToPool(m); } - std::list messages_; // Messages received from the MeshService. + void sendClientNotification(meshtastic_ClientNotification *n) override + { + notifications_.emplace_back(*n); + releaseClientNotificationToPool(n); + } + std::list messages_; // Messages received from the MeshService. + std::list notifications_; // Notifications received from the MeshService. }; // Minimal NodeDB needed to return values from getMeshNode. @@ -823,14 +835,6 @@ void test_configWithDefaultServerAndInvalidPort(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } -// Configuration with the default server and tls_enabled = true is invalid. -void test_configWithDefaultServerAndInvalidTLSEnabled(void) -{ - meshtastic_ModuleConfig_MQTTConfig config = {.tls_enabled = true}; - - TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); -} - // isValidConfig connects to a custom host and port. void test_configCustomHostAndPort(void) { @@ -911,7 +915,6 @@ void setup() RUN_TEST(test_configEnabledEmptyIsValid); RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); - RUN_TEST(test_configWithDefaultServerAndInvalidTLSEnabled); RUN_TEST(test_configCustomHostAndPort); RUN_TEST(test_configWithConnectionFailure); RUN_TEST(test_configWithTLSEnabled); From df8b629c2c4c43b5939e7153a427100c1b769a42 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 26 Jul 2025 23:09:26 +1000 Subject: [PATCH 16/25] Clear position on GPS deactivation, unless using fixed position (#7464) * Clear position on GPS deactivation, unless using fixed position As reported by @dreimal8 , and confirmed by @tuxmobil , when using and then subsequently disabling GPS the last position retrieved from the GPS was stored and continued to be broadcast. This change introduces a check to see if we are transitioning from GPS Enabled to GPS Disabled or Not Present. If we are, and fixed position is not in use, then we clear the local position. This will prevent inaccurate and undesired position broadcasts for those who disable their GPS. Fixes https://github.com/meshtastic/firmware/issues/7228 * Update triple click to also clear position --------- Co-authored-by: Ben Meadors --- src/modules/AdminModule.cpp | 9 +++++++++ src/modules/SystemCommandsModule.cpp | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4b910a959..87d423f21 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -638,7 +638,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_position_tag: LOG_INFO("Set config: Position"); config.has_position = true; + // If we have turned off the GPS (disabled or not present) and we're not using fixed position, + // clear the stored position since it may not get updated + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false); + } config.position = c.payload_variant.position; + // Save nodedb as well in case we got a fixed position packet break; case meshtastic_Config_power_tag: diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 2d534bd67..74b9678f4 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -89,6 +89,11 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) #if !MESHTASTIC_EXCLUDE_GPS if (gps) { LOG_WARN("GPS Toggle2"); + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED && + config.position.fixed_position == false) { + nodeDB->clearLocalPosition(); + nodeDB->saveToDisk(); + } gps->toggleGpsMode(); const char *msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; From 7c5e2c539387b178661afa4279f6c5e92a4dec2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:21:49 -0500 Subject: [PATCH 17/25] Update protobufs (#7473) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/protobufs b/protobufs index d31cd890d..9bac2886f 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit d31cd890d58ffa7e3524e0685a8617bbd181a1c6 +Subproject commit 9bac2886f9344f25716921467a82e8b0326107cd diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index e709db6c4..ba1a52b27 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -362,7 +362,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size #define meshtastic_BackupPreferences_size 2271 #define meshtastic_ChannelFile_size 718 -#define meshtastic_DeviceState_size 1724 +#define meshtastic_DeviceState_size 1728 #define meshtastic_NodeInfoLite_size 196 #define meshtastic_PositionLite_size 28 #define meshtastic_UserLite_size 98 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index abc06e635..8e6524042 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -935,6 +935,9 @@ typedef struct _meshtastic_MyNodeInfo { char pio_env[40]; /* The indicator for whether this device is running event firmware and which */ meshtastic_FirmwareEdition firmware_edition; + /* The number of nodes in the nodedb. + This is used by the phone to know how many NodeInfo packets to expect on want_config */ + uint16_t nodedb_count; } meshtastic_MyNodeInfo; /* Debug output from the device. @@ -1322,7 +1325,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_default {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} @@ -1353,7 +1356,7 @@ extern "C" { #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} #define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0, 0, false, 0, 0, 0, 0} -#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN} +#define meshtastic_MyNodeInfo_init_zero {0, 0, 0, {0, {0}}, "", _meshtastic_FirmwareEdition_MIN, 0} #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} @@ -1477,6 +1480,7 @@ extern "C" { #define meshtastic_MyNodeInfo_device_id_tag 12 #define meshtastic_MyNodeInfo_pio_env_tag 13 #define meshtastic_MyNodeInfo_firmware_edition_tag 14 +#define meshtastic_MyNodeInfo_nodedb_count_tag 15 #define meshtastic_LogRecord_message_tag 1 #define meshtastic_LogRecord_time_tag 2 #define meshtastic_LogRecord_source_tag 3 @@ -1710,7 +1714,8 @@ X(a, STATIC, SINGULAR, UINT32, reboot_count, 8) \ X(a, STATIC, SINGULAR, UINT32, min_app_version, 11) \ X(a, STATIC, SINGULAR, BYTES, device_id, 12) \ X(a, STATIC, SINGULAR, STRING, pio_env, 13) \ -X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) +X(a, STATIC, SINGULAR, UENUM, firmware_edition, 14) \ +X(a, STATIC, SINGULAR, UINT32, nodedb_count, 15) #define meshtastic_MyNodeInfo_CALLBACK NULL #define meshtastic_MyNodeInfo_DEFAULT NULL @@ -1993,7 +1998,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_LowEntropyKey_size 0 #define meshtastic_MeshPacket_size 378 #define meshtastic_MqttClientProxyMessage_size 501 -#define meshtastic_MyNodeInfo_size 79 +#define meshtastic_MyNodeInfo_size 83 #define meshtastic_NeighborInfo_size 258 #define meshtastic_Neighbor_size 22 #define meshtastic_NodeInfo_size 323 From 28aeb0f09e9bb633d5ba8b6511050ac778e030c0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 26 Jul 2025 19:55:54 -0500 Subject: [PATCH 18/25] Validate Serial config console override modes (#7470) * Validate serial config console override modes * Update src/modules/SerialModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Disable * Guard serial module * Guards --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/AdminModule.cpp | 32 ++++-- src/modules/SerialModule.cpp | 20 ++++ src/modules/SerialModule.h | 2 + test/test_serial/SerialModule.cpp | 156 ++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 test/test_serial/SerialModule.cpp diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 87d423f21..1a9c3a7a7 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -43,6 +43,10 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C #include "motion/AccelerometerThread.h" #endif +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "SerialModule.h" +#endif AdminModule *adminModule; bool hasOpenEditTransaction; @@ -807,10 +811,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { - // If we are in an open transaction or configuring MQTT, defer disabling Bluetooth + // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && c.which_payload_variant != meshtastic_ModuleConfig_mqtt_tag) + if (!hasOpenEditTransaction && + !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { disableBluetooth(); + } switch (c.which_payload_variant) { case meshtastic_ModuleConfig_mqtt_tag: @@ -830,6 +836,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) break; case meshtastic_ModuleConfig_serial_tag: LOG_INFO("Set module config: Serial"); +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + if (!SerialModule::isValidConfig(c.payload_variant.serial)) { + LOG_ERROR("Invalid serial config"); + return false; + } + disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration +#endif moduleConfig.has_serial = true; moduleConfig.serial = c.payload_variant.serial; break; @@ -985,9 +999,10 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32 // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); @@ -1071,9 +1086,10 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const // So even if we internally use 0 to represent 'use default' we still need to send the value we are // using to the app (so that even old phone apps work with new device loads). // r.get_radio_response.preferences.ls_secs = getPref_ls_secs(); - // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally private - // and useful for users to know current provisioning) hideSecret(r.get_radio_response.preferences.wifi_password); - // r.get_config_response.which_payloadVariant = Config_ModuleConfig_telemetry_tag; + // hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally + // private and useful for users to know current provisioning) + // hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant = + // Config_ModuleConfig_telemetry_tag; res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index f3921ef19..f3091e5bf 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -74,6 +74,26 @@ static Print *serialPrint = &Serial2; char serialBytes[512]; size_t serialPayloadSize; +bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) +{ + if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) { + const char *warning = + "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; + LOG_ERROR(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_ERROR; + cn->time = getValidTime(RTCQualityFromNet); + snprintf(cn->message, sizeof(cn->message), "%s", warning); + service->sendClientNotification(cn); +#endif + return false; + } + + return true; +} + SerialModuleRadio::SerialModuleRadio() : MeshModule("SerialModuleRadio") { switch (moduleConfig.serial.mode) { diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index fa86db28f..1c74c927c 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -20,6 +20,8 @@ class SerialModule : public StreamAPI, private concurrency::OSThread public: SerialModule(); + static bool isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config); + protected: virtual int32_t runOnce() override; diff --git a/test/test_serial/SerialModule.cpp b/test/test_serial/SerialModule.cpp new file mode 100644 index 000000000..1bccf04a7 --- /dev/null +++ b/test/test_serial/SerialModule.cpp @@ -0,0 +1,156 @@ +#include "DebugConfiguration.h" +#include "TestUtil.h" +#include + +#ifdef ARCH_PORTDUINO +#include "configuration.h" + +#if defined(UNIT_TEST) +#define IS_RUNNING_TESTS 1 +#else +#define IS_RUNNING_TESTS 0 +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) +#include "modules/SerialModule.h" +#endif + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + +// Test that empty configuration is valid. +void test_serialConfigEmptyIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that basic enabled configuration is valid. +void test_serialConfigEnabledIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and NMEA mode is valid. +void test_serialConfigWithOverrideConsoleNmeaModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and CalTopo mode is valid. +void test_serialConfigWithOverrideConsoleCalTopoModeIsValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO}; + + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and DEFAULT mode is invalid. +void test_serialConfigWithOverrideConsoleDefaultModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and SIMPLE mode is invalid. +void test_serialConfigWithOverrideConsoleSimpleModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and TEXTMSG mode is invalid. +void test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that configuration with override_console_serial_port and PROTO mode is invalid. +void test_serialConfigWithOverrideConsoleProtoModeIsInvalid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = { + .enabled = true, .override_console_serial_port = true, .mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO}; + + TEST_ASSERT_FALSE(SerialModule::isValidConfig(config)); +} + +// Test that various modes work without override_console_serial_port. +void test_serialConfigVariousModesWithoutOverrideAreValid(void) +{ + meshtastic_ModuleConfig_SerialConfig config = {.enabled = true, .override_console_serial_port = false}; + + // Test DEFAULT mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_DEFAULT; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test SIMPLE mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_SIMPLE; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test TEXTMSG mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test PROTO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_PROTO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test NMEA mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); + + // Test CALTOPO mode + config.mode = meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO; + TEST_ASSERT_TRUE(SerialModule::isValidConfig(config)); +} + +#endif // Architecture check + +void setup() +{ + initializeTestEnvironment(); + +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ + !defined(CONFIG_IDF_TARGET_ESP32C3) + UNITY_BEGIN(); + RUN_TEST(test_serialConfigEmptyIsValid); + RUN_TEST(test_serialConfigEnabledIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleNmeaModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleCalTopoModeIsValid); + RUN_TEST(test_serialConfigWithOverrideConsoleDefaultModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleSimpleModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleTextMsgModeIsInvalid); + RUN_TEST(test_serialConfigWithOverrideConsoleProtoModeIsInvalid); + RUN_TEST(test_serialConfigVariousModesWithoutOverrideAreValid); + exit(UNITY_END()); +#else + LOG_WARN("This test requires ESP32, NRF52, or RP2040 architecture"); + UNITY_BEGIN(); + UNITY_END(); +#endif +} +#else +void setup() +{ + initializeTestEnvironment(); + LOG_WARN("This test requires the ARCH_PORTDUINO variant"); + UNITY_BEGIN(); + UNITY_END(); +} +#endif +void loop() {} From aa3b14ce720b2f52cdd553b1a39477673c2c3783 Mon Sep 17 00:00:00 2001 From: mikecarper <135079168+mikecarper@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:03:01 -0700 Subject: [PATCH 19/25] bugfix Add rssi and snr to the store and forward code. (#7462) * Update StoreForwardModule.cpp * Update StoreForwardModule.h --------- Co-authored-by: Ben Meadors --- src/modules/StoreForwardModule.cpp | 6 +++++- src/modules/StoreForwardModule.h | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 0a6e1b4c4..72ac99118 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -202,6 +202,8 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp) this->packetHistory[this->packetHistoryTotalCount].reply_id = p.reply_id; this->packetHistory[this->packetHistoryTotalCount].emoji = (bool)p.emoji; this->packetHistory[this->packetHistoryTotalCount].payload_size = p.payload.size; + this->packetHistory[this->packetHistoryTotalCount].rx_rssi = mp.rx_rssi; + this->packetHistory[this->packetHistoryTotalCount].rx_snr = mp.rx_snr; memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN); this->packetHistoryTotalCount++; @@ -252,6 +254,8 @@ meshtastic_MeshPacket *StoreForwardModule::preparePayload(NodeNum dest, uint32_t p->decoded.reply_id = this->packetHistory[i].reply_id; p->rx_time = this->packetHistory[i].time; p->decoded.emoji = (uint32_t)this->packetHistory[i].emoji; + p->rx_rssi = this->packetHistory[i].rx_rssi; + p->rx_snr = this->packetHistory[i].rx_snr; // Let's assume that if the server received the S&F request that the client is in range. // TODO: Make this configurable. @@ -623,4 +627,4 @@ StoreForwardModule::StoreForwardModule() disable(); } #endif -} \ No newline at end of file +} diff --git a/src/modules/StoreForwardModule.h b/src/modules/StoreForwardModule.h index 30db1625c..25836eded 100644 --- a/src/modules/StoreForwardModule.h +++ b/src/modules/StoreForwardModule.h @@ -19,6 +19,8 @@ struct PacketHistoryStruct { bool emoji; uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN]; pb_size_t payload_size; + int32_t rx_rssi; + float rx_snr; }; class StoreForwardModule : private concurrency::OSThread, public ProtobufModule @@ -108,4 +110,4 @@ class StoreForwardModule : private concurrency::OSThread, public ProtobufModule< virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_StoreAndForward *p); }; -extern StoreForwardModule *storeForwardModule; \ No newline at end of file +extern StoreForwardModule *storeForwardModule; From 3ecff48722d3a60e8c84f6d2573cd3688915a279 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 28 Jul 2025 07:31:33 -0500 Subject: [PATCH 20/25] Set firmware edition (for events) from userprefs (#7488) * Set firmware edition (for events) from userprefs * Spaces in the right places --- src/mesh/NodeDB.cpp | 3 +++ userPrefs.jsonc | 1 + 2 files changed, 4 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index b7120a064..881dc6ab7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -406,6 +406,9 @@ NodeDB::NodeDB() config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; config.position.gps_enabled = 0; } +#ifdef USERPREFS_FIRMWARE_EDITION + myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION; +#endif #ifdef USERPREFS_FIXED_GPS if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset. meshtastic_Position fixedGPS = meshtastic_Position_init_default; diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 3da8e7ba6..f6f3ef995 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -23,6 +23,7 @@ // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, LOST AND FOUND, and REPEATER roles are restricted. // "USERPREFS_EVENT_MODE": "1", + // "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", // "USERPREFS_FIXED_GPS_ALT": "0", From 1d8638b47d8ec97f485b240a464de720f7f56310 Mon Sep 17 00:00:00 2001 From: rradillen Date: Mon, 28 Jul 2025 16:23:04 +0200 Subject: [PATCH 21/25] [7353] Add all telemetry fields to json output (#7363) * Serializer bugfix * Remove duplicate test * fix tests * fix float precision issues --------- Co-authored-by: Ben Meadors --- src/serialization/MeshPacketSerializer.cpp | 24 + .../ports/test_encrypted.cpp | 50 ++ .../ports/test_nodeinfo.cpp | 51 ++ .../ports/test_position.cpp | 57 ++ .../ports/test_telemetry.cpp | 528 ++++++++++++++++++ .../ports/test_text_message.cpp | 42 ++ .../ports/test_waypoint.cpp | 53 ++ .../test_meshpacket_serializer/test_helpers.h | 44 ++ .../test_serializer.cpp | 51 ++ 9 files changed, 900 insertions(+) create mode 100644 test/test_meshpacket_serializer/ports/test_encrypted.cpp create mode 100644 test/test_meshpacket_serializer/ports/test_nodeinfo.cpp create mode 100644 test/test_meshpacket_serializer/ports/test_position.cpp create mode 100644 test/test_meshpacket_serializer/ports/test_telemetry.cpp create mode 100644 test/test_meshpacket_serializer/ports/test_text_message.cpp create mode 100644 test/test_meshpacket_serializer/ports/test_waypoint.cpp create mode 100644 test/test_meshpacket_serializer/test_helpers.h create mode 100644 test/test_meshpacket_serializer/test_serializer.cpp diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 29a9b6840..5a1f8ed7e 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -100,6 +100,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.environment_metrics.has_iaq) { msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); } + if (decoded->variant.environment_metrics.has_distance) { + msgPayload["distance"] = new JSONValue(decoded->variant.environment_metrics.distance); + } if (decoded->variant.environment_metrics.has_wind_speed) { msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); } @@ -115,6 +118,27 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.environment_metrics.has_radiation) { msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); } + if (decoded->variant.environment_metrics.has_ir_lux) { + msgPayload["ir_lux"] = new JSONValue(decoded->variant.environment_metrics.ir_lux); + } + if (decoded->variant.environment_metrics.has_uv_lux) { + msgPayload["uv_lux"] = new JSONValue(decoded->variant.environment_metrics.uv_lux); + } + if (decoded->variant.environment_metrics.has_weight) { + msgPayload["weight"] = new JSONValue(decoded->variant.environment_metrics.weight); + } + if (decoded->variant.environment_metrics.has_rainfall_1h) { + msgPayload["rainfall_1h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_1h); + } + if (decoded->variant.environment_metrics.has_rainfall_24h) { + msgPayload["rainfall_24h"] = new JSONValue(decoded->variant.environment_metrics.rainfall_24h); + } + if (decoded->variant.environment_metrics.has_soil_moisture) { + msgPayload["soil_moisture"] = new JSONValue((uint)decoded->variant.environment_metrics.soil_moisture); + } + if (decoded->variant.environment_metrics.has_soil_temperature) { + msgPayload["soil_temperature"] = new JSONValue(decoded->variant.environment_metrics.soil_temperature); + } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { if (decoded->variant.air_quality_metrics.has_pm10_standard) { msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp new file mode 100644 index 000000000..557ee7a49 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp @@ -0,0 +1,50 @@ +#include "../test_helpers.h" + +// Test encrypted packet serialization +void test_encrypted_packet_serialization() +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = 0x11223344; + packet.to = 0x55667788; + packet.id = 0x9999; + packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + + // Add some dummy encrypted data + const char *encrypted_data = "encrypted_payload_data"; + packet.encrypted.size = strlen(encrypted_data); + memcpy(packet.encrypted.bytes, encrypted_data, packet.encrypted.size); + + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check basic packet fields + TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber()); + + // Check that it has encrypted data fields (not "payload" but "bytes" and "size") + TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["bytes"]->IsString()); + + TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); + TEST_ASSERT_EQUAL(22, (int)jsonObj["size"]->AsNumber()); // strlen("encrypted_payload_data") = 22 + + // The encrypted data should be hex-encoded + std::string encrypted_hex = jsonObj["bytes"]->AsString(); + TEST_ASSERT_TRUE(encrypted_hex.length() > 0); + // Should be twice the size of the original data (hex encoding) + TEST_ASSERT_EQUAL(44, encrypted_hex.length()); // 22 * 2 = 44 + + delete root; +} diff --git a/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp new file mode 100644 index 000000000..febda9950 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_nodeinfo.cpp @@ -0,0 +1,51 @@ +#include "../test_helpers.h" + +static size_t encode_user_info(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_User user = meshtastic_User_init_zero; + strcpy(user.short_name, "TEST"); + strcpy(user.long_name, "Test User"); + strcpy(user.id, "!12345678"); + user.hw_model = meshtastic_HardwareModel_HELTEC_V3; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_User_msg, &user); + return stream.bytes_written; +} + +// Test NODEINFO_APP port +void test_nodeinfo_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_user_info(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_NODEINFO_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("nodeinfo", jsonObj["type"]->AsString().c_str()); + + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Verify user data + TEST_ASSERT_TRUE(payload.find("shortname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("TEST", payload["shortname"]->AsString().c_str()); + + TEST_ASSERT_TRUE(payload.find("longname") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test User", payload["longname"]->AsString().c_str()); + + delete root; +} diff --git a/test/test_meshpacket_serializer/ports/test_position.cpp b/test/test_meshpacket_serializer/ports/test_position.cpp new file mode 100644 index 000000000..f0dcc0709 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_position.cpp @@ -0,0 +1,57 @@ +#include "../test_helpers.h" + +static size_t encode_position(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Position position = meshtastic_Position_init_zero; + position.latitude_i = 374208000; // 37.4208 degrees * 1e7 + position.longitude_i = -1221981000; // -122.1981 degrees * 1e7 + position.altitude = 123; + position.time = 1609459200; + position.has_altitude = true; + position.has_latitude_i = true; + position.has_longitude_i = true; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Position_msg, &position); + return stream.bytes_written; +} + +// Test POSITION_APP port +void test_position_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_position(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_POSITION_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("position", jsonObj["type"]->AsString().c_str()); + + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Verify position data + TEST_ASSERT_TRUE(payload.find("latitude_i") != payload.end()); + TEST_ASSERT_EQUAL(374208000, (int)payload["latitude_i"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("longitude_i") != payload.end()); + TEST_ASSERT_EQUAL(-1221981000, (int)payload["longitude_i"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("altitude") != payload.end()); + TEST_ASSERT_EQUAL(123, (int)payload["altitude"]->AsNumber()); + + delete root; +} diff --git a/test/test_meshpacket_serializer/ports/test_telemetry.cpp b/test/test_meshpacket_serializer/ports/test_telemetry.cpp new file mode 100644 index 000000000..a813aaab5 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_telemetry.cpp @@ -0,0 +1,528 @@ +#include "../test_helpers.h" + +// Helper function to create and encode device metrics +static size_t encode_telemetry_device_metrics(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_device_metrics_tag; + telemetry.variant.device_metrics.battery_level = 85; + telemetry.variant.device_metrics.has_battery_level = true; + telemetry.variant.device_metrics.voltage = 3.72f; + telemetry.variant.device_metrics.has_voltage = true; + telemetry.variant.device_metrics.channel_utilization = 15.56f; + telemetry.variant.device_metrics.has_channel_utilization = true; + telemetry.variant.device_metrics.air_util_tx = 8.23f; + telemetry.variant.device_metrics.has_air_util_tx = true; + telemetry.variant.device_metrics.uptime_seconds = 12345; + telemetry.variant.device_metrics.has_uptime_seconds = true; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; +} + +// Helper function to create and encode empty environment metrics (no fields set) +static size_t encode_telemetry_environment_metrics_empty(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; + + // NO fields are set - all has_* flags remain false + // This tests that empty environment metrics don't produce any JSON fields + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; +} + +// Helper function to create environment metrics with ALL possible fields set +// This function should be updated whenever new fields are added to the protobuf +static size_t encode_telemetry_environment_metrics_all_fields(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; + + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; + + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; + + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; + + // Light measurements (ALL 4 types) + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; + + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; + + // Wind measurements (ALL 4 types) + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; + + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; + + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; + + // Rainfall measurements (BOTH types) + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; + + // Soil measurements (BOTH types) + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; + + // IMPORTANT: When new environment fields are added to the protobuf, + // they MUST be added here too, or the coverage test will fail! + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; +} + +// Helper function to create and encode environment metrics with all current fields +static size_t encode_telemetry_environment_metrics(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Telemetry telemetry = meshtastic_Telemetry_init_zero; + telemetry.time = 1609459200; + telemetry.which_variant = meshtastic_Telemetry_environment_metrics_tag; + + // Basic environment metrics + telemetry.variant.environment_metrics.temperature = 23.56f; + telemetry.variant.environment_metrics.has_temperature = true; + telemetry.variant.environment_metrics.relative_humidity = 65.43f; + telemetry.variant.environment_metrics.has_relative_humidity = true; + telemetry.variant.environment_metrics.barometric_pressure = 1013.27f; + telemetry.variant.environment_metrics.has_barometric_pressure = true; + + // Gas and air quality + telemetry.variant.environment_metrics.gas_resistance = 50.58f; + telemetry.variant.environment_metrics.has_gas_resistance = true; + telemetry.variant.environment_metrics.iaq = 120; + telemetry.variant.environment_metrics.has_iaq = true; + + // Power measurements + telemetry.variant.environment_metrics.voltage = 3.34f; + telemetry.variant.environment_metrics.has_voltage = true; + telemetry.variant.environment_metrics.current = 0.53f; + telemetry.variant.environment_metrics.has_current = true; + + // Light measurements + telemetry.variant.environment_metrics.lux = 450.12f; + telemetry.variant.environment_metrics.has_lux = true; + telemetry.variant.environment_metrics.white_lux = 380.95f; + telemetry.variant.environment_metrics.has_white_lux = true; + telemetry.variant.environment_metrics.ir_lux = 25.37f; + telemetry.variant.environment_metrics.has_ir_lux = true; + telemetry.variant.environment_metrics.uv_lux = 15.68f; + telemetry.variant.environment_metrics.has_uv_lux = true; + + // Distance measurement + telemetry.variant.environment_metrics.distance = 150.29f; + telemetry.variant.environment_metrics.has_distance = true; + + // Wind measurements + telemetry.variant.environment_metrics.wind_direction = 180; + telemetry.variant.environment_metrics.has_wind_direction = true; + telemetry.variant.environment_metrics.wind_speed = 5.52f; + telemetry.variant.environment_metrics.has_wind_speed = true; + telemetry.variant.environment_metrics.wind_gust = 8.24f; + telemetry.variant.environment_metrics.has_wind_gust = true; + telemetry.variant.environment_metrics.wind_lull = 2.13f; + telemetry.variant.environment_metrics.has_wind_lull = true; + + // Weight measurement + telemetry.variant.environment_metrics.weight = 75.56f; + telemetry.variant.environment_metrics.has_weight = true; + + // Radiation measurement + telemetry.variant.environment_metrics.radiation = 0.13f; + telemetry.variant.environment_metrics.has_radiation = true; + + // Rainfall measurements + telemetry.variant.environment_metrics.rainfall_1h = 2.57f; + telemetry.variant.environment_metrics.has_rainfall_1h = true; + telemetry.variant.environment_metrics.rainfall_24h = 15.89f; + telemetry.variant.environment_metrics.has_rainfall_24h = true; + + // Soil measurements + telemetry.variant.environment_metrics.soil_moisture = 85; + telemetry.variant.environment_metrics.has_soil_moisture = true; + telemetry.variant.environment_metrics.soil_temperature = 18.54f; + telemetry.variant.environment_metrics.has_soil_temperature = true; + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Telemetry_msg, &telemetry); + return stream.bytes_written; +} + +// Test TELEMETRY_APP port with device metrics +void test_telemetry_device_metrics_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_device_metrics(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("telemetry", jsonObj["type"]->AsString().c_str()); + + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Verify telemetry data + TEST_ASSERT_TRUE(payload.find("battery_level") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["battery_level"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.72f, payload["voltage"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("channel_utilization") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.56f, payload["channel_utilization"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("uptime_seconds") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["uptime_seconds"]->AsNumber()); + + // Note: JSON serialization may not preserve exact 2-decimal formatting due to float precision + // We verify the numeric values are correct within tolerance + + delete root; +} + +// Test that telemetry environment metrics are properly serialized +void test_telemetry_environment_metrics_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Test key fields that should be present in the serializer + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + + // Note: JSON serialization may have float precision limitations + // We focus on verifying numeric accuracy rather than exact string formatting + + delete root; +} + +// Test comprehensive environment metrics coverage +void test_telemetry_environment_metrics_comprehensive() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Check all 15 originally supported fields + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + + delete root; +} + +// Test for the 7 environment fields that were added to complete coverage +void test_telemetry_environment_metrics_missing_fields() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Check the 7 fields that were previously missing + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + + // Note: JSON float serialization may not preserve exact decimal formatting + // We verify the values are numerically correct within tolerance + + delete root; +} + +// Test that ALL environment fields are serialized (canary test for forgotten fields) +// This test will FAIL if a new environment field is added to the protobuf but not to the serializer +void test_telemetry_environment_metrics_complete_coverage() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_all_fields(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // ✅ ALL 22 environment fields MUST be present and correct + // If this test fails, it means either: + // 1. A new field was added to the protobuf but not to the serializer + // 2. The encode_telemetry_environment_metrics_all_fields() function wasn't updated + + // Basic environment (3 fields) + TEST_ASSERT_TRUE(payload.find("temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 23.56f, payload["temperature"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 65.43f, payload["relative_humidity"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 1013.27f, payload["barometric_pressure"]->AsNumber()); + + // Gas and air quality (2 fields) + TEST_ASSERT_TRUE(payload.find("gas_resistance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 50.58f, payload["gas_resistance"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("iaq") != payload.end()); + TEST_ASSERT_EQUAL(120, (int)payload["iaq"]->AsNumber()); + + // Power measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("voltage") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 3.34f, payload["voltage"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("current") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.53f, payload["current"]->AsNumber()); + + // Light measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 450.12f, payload["lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("white_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 380.95f, payload["white_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("ir_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 25.37f, payload["ir_lux"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("uv_lux") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.68f, payload["uv_lux"]->AsNumber()); + + // Distance measurement (1 field) + TEST_ASSERT_TRUE(payload.find("distance") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 150.29f, payload["distance"]->AsNumber()); + + // Wind measurements (4 fields) + TEST_ASSERT_TRUE(payload.find("wind_direction") != payload.end()); + TEST_ASSERT_EQUAL(180, (int)payload["wind_direction"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_speed") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 5.52f, payload["wind_speed"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_gust") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 8.24f, payload["wind_gust"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("wind_lull") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.13f, payload["wind_lull"]->AsNumber()); + + // Weight measurement (1 field) + TEST_ASSERT_TRUE(payload.find("weight") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 75.56f, payload["weight"]->AsNumber()); + + // Radiation measurement (1 field) + TEST_ASSERT_TRUE(payload.find("radiation") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 0.13f, payload["radiation"]->AsNumber()); + + // Rainfall measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("rainfall_1h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 2.57f, payload["rainfall_1h"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 15.89f, payload["rainfall_24h"]->AsNumber()); + + // Soil measurements (2 fields) + TEST_ASSERT_TRUE(payload.find("soil_moisture") != payload.end()); + TEST_ASSERT_EQUAL(85, (int)payload["soil_moisture"]->AsNumber()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") != payload.end()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 18.54f, payload["soil_temperature"]->AsNumber()); + + // Total: 22 environment fields + // This test ensures 100% coverage of environment metrics + + // Note: JSON float serialization precision may vary due to the underlying library + // The important aspect is that all values are numerically accurate within tolerance + + delete root; +} + +// Test that unset environment fields are not present in JSON +void test_telemetry_environment_metrics_unset_fields() +{ + uint8_t buffer[256]; + size_t payload_size = encode_telemetry_environment_metrics_empty(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TELEMETRY_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check payload exists + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // With completely empty environment metrics, NO fields should be present + // Only basic telemetry fields like "time" might be present + + // All 22 environment fields should be absent (none were set) + TEST_ASSERT_TRUE(payload.find("temperature") == payload.end()); + TEST_ASSERT_TRUE(payload.find("relative_humidity") == payload.end()); + TEST_ASSERT_TRUE(payload.find("barometric_pressure") == payload.end()); + TEST_ASSERT_TRUE(payload.find("gas_resistance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("iaq") == payload.end()); + TEST_ASSERT_TRUE(payload.find("voltage") == payload.end()); + TEST_ASSERT_TRUE(payload.find("current") == payload.end()); + TEST_ASSERT_TRUE(payload.find("lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("white_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("ir_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("uv_lux") == payload.end()); + TEST_ASSERT_TRUE(payload.find("distance") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_direction") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_speed") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_gust") == payload.end()); + TEST_ASSERT_TRUE(payload.find("wind_lull") == payload.end()); + TEST_ASSERT_TRUE(payload.find("weight") == payload.end()); + TEST_ASSERT_TRUE(payload.find("radiation") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_1h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("rainfall_24h") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_moisture") == payload.end()); + TEST_ASSERT_TRUE(payload.find("soil_temperature") == payload.end()); + + delete root; +} diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp new file mode 100644 index 000000000..de3f34541 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp @@ -0,0 +1,42 @@ +#include "../test_helpers.h" + +// Test TEXT_MESSAGE_APP port +void test_text_message_serialization() +{ + const char *test_text = "Hello Meshtastic!"; + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, (const uint8_t *)test_text, strlen(test_text)); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check basic packet fields + TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber()); + + TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); + TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber()); + + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("text", jsonObj["type"]->AsString().c_str()); + + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + TEST_ASSERT_TRUE(payload.find("text") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Hello Meshtastic!", payload["text"]->AsString().c_str()); + + delete root; +} diff --git a/test/test_meshpacket_serializer/ports/test_waypoint.cpp b/test/test_meshpacket_serializer/ports/test_waypoint.cpp new file mode 100644 index 000000000..b7e811d70 --- /dev/null +++ b/test/test_meshpacket_serializer/ports/test_waypoint.cpp @@ -0,0 +1,53 @@ +#include "../test_helpers.h" + +static size_t encode_waypoint(uint8_t *buffer, size_t buffer_size) +{ + meshtastic_Waypoint waypoint = meshtastic_Waypoint_init_zero; + waypoint.id = 12345; + waypoint.latitude_i = 374208000; + waypoint.longitude_i = -1221981000; + waypoint.expire = 1609459200 + 3600; // 1 hour from now + strcpy(waypoint.name, "Test Point"); + strcpy(waypoint.description, "Test waypoint description"); + + pb_ostream_t stream = pb_ostream_from_buffer(buffer, buffer_size); + pb_encode(&stream, &meshtastic_Waypoint_msg, &waypoint); + return stream.bytes_written; +} + +// Test WAYPOINT_APP port +void test_waypoint_serialization() +{ + uint8_t buffer[256]; + size_t payload_size = encode_waypoint(buffer, sizeof(buffer)); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_WAYPOINT_APP, buffer, payload_size); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); + + JSONValue *root = JSON::Parse(json.c_str()); + TEST_ASSERT_NOT_NULL(root); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check message type + TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("waypoint", jsonObj["type"]->AsString().c_str()); + + // Check payload + TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); + TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); + + JSONObject payload = jsonObj["payload"]->AsObject(); + + // Verify waypoint data + TEST_ASSERT_TRUE(payload.find("id") != payload.end()); + TEST_ASSERT_EQUAL(12345, (int)payload["id"]->AsNumber()); + + TEST_ASSERT_TRUE(payload.find("name") != payload.end()); + TEST_ASSERT_EQUAL_STRING("Test Point", payload["name"]->AsString().c_str()); + + delete root; +} diff --git a/test/test_meshpacket_serializer/test_helpers.h b/test/test_meshpacket_serializer/test_helpers.h new file mode 100644 index 000000000..630e059bc --- /dev/null +++ b/test/test_meshpacket_serializer/test_helpers.h @@ -0,0 +1,44 @@ +#pragma once + +#include "serialization/JSON.h" +#include "serialization/MeshPacketSerializer.h" +#include +#include +#include +#include +#include +#include +#include + +// Helper function to create a test packet with the given port and payload +static meshtastic_MeshPacket create_test_packet(meshtastic_PortNum port, const uint8_t *payload, size_t payload_size) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + + packet.id = 0x9999; + packet.from = 0x11223344; + packet.to = 0x55667788; + packet.channel = 0; + packet.hop_limit = 3; + packet.want_ack = false; + packet.priority = meshtastic_MeshPacket_Priority_UNSET; + packet.rx_time = 1609459200; + packet.rx_snr = 10.5f; + packet.hop_start = 3; + packet.rx_rssi = -85; + packet.delayed = meshtastic_MeshPacket_Delayed_NO_DELAY; + + // Set decoded variant + packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.decoded.portnum = port; + memcpy(packet.decoded.payload.bytes, payload, payload_size); + packet.decoded.payload.size = payload_size; + packet.decoded.want_response = false; + packet.decoded.dest = 0x55667788; + packet.decoded.source = 0x11223344; + packet.decoded.request_id = 0; + packet.decoded.reply_id = 0; + packet.decoded.emoji = 0; + + return packet; +} diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp new file mode 100644 index 000000000..d74031fa4 --- /dev/null +++ b/test/test_meshpacket_serializer/test_serializer.cpp @@ -0,0 +1,51 @@ +#include "test_helpers.h" +#include +#include + +// Forward declarations for test functions +void test_text_message_serialization(); +void test_position_serialization(); +void test_nodeinfo_serialization(); +void test_waypoint_serialization(); +void test_telemetry_device_metrics_serialization(); +void test_telemetry_environment_metrics_serialization(); +void test_telemetry_environment_metrics_comprehensive(); +void test_telemetry_environment_metrics_missing_fields(); +void test_telemetry_environment_metrics_complete_coverage(); +void test_telemetry_environment_metrics_unset_fields(); +void test_encrypted_packet_serialization(); + +void setup() +{ + UNITY_BEGIN(); + + // Text message tests + RUN_TEST(test_text_message_serialization); + + // Position tests + RUN_TEST(test_position_serialization); + + // Nodeinfo tests + RUN_TEST(test_nodeinfo_serialization); + + // Waypoint tests + RUN_TEST(test_waypoint_serialization); + + // Telemetry tests + RUN_TEST(test_telemetry_device_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_serialization); + RUN_TEST(test_telemetry_environment_metrics_comprehensive); + RUN_TEST(test_telemetry_environment_metrics_missing_fields); + RUN_TEST(test_telemetry_environment_metrics_complete_coverage); + RUN_TEST(test_telemetry_environment_metrics_unset_fields); + + // Encrypted packet test + RUN_TEST(test_encrypted_packet_serialization); + + UNITY_END(); +} + +void loop() +{ + delay(1000); +} From 608fdc6f52d7e4d1588fad4b92712e5290b00ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 28 Jul 2025 16:47:46 +0200 Subject: [PATCH 22/25] Santa may be checking his list twice, but we only need this in the platformio.ini (#7490) --- variants/nrf52840/gat562_mesh_trial_tracker/variant.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h index 2af0bc76d..6337ac70c 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/variant.h +++ b/variants/nrf52840/gat562_mesh_trial_tracker/variant.h @@ -19,8 +19,6 @@ #ifndef _VARIANT_GAT562_MESH_TRIAL_TRACKER_ #define _VARIANT_GAT562_MESH_TRIAL_TRACKER_ -#define GAT562_MESH_TRIAL_TRACKER - // led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 #define RAK4630 From 1a8ab2aadc6adac01bbf5f5b186e0ca3d3382975 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 28 Jul 2025 12:23:59 -0500 Subject: [PATCH 23/25] NodeDB count on MyNodeInfo for client progress reporting (#7489) --- src/mesh/PhoneAPI.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index e0b81bedd..83becb037 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -205,6 +205,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) // app not to send locations on our behalf. fromRadioScratch.which_payload_variant = meshtastic_FromRadio_my_info_tag; strncpy(myNodeInfo.pio_env, optstr(APP_ENV), sizeof(myNodeInfo.pio_env)); + myNodeInfo.nodedb_count = static_cast(nodeDB->getNumMeshNodes()); fromRadioScratch.my_info = myNodeInfo; state = STATE_SEND_UIDATA; From cc5d00e2116a458f84078c24c51a99b17f9f2774 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 28 Jul 2025 12:37:37 -0500 Subject: [PATCH 24/25] Core portnums rebroadcast mode whitelist instead of blacklist (#7487) --- src/mesh/Router.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 02968513c..993c59dd6 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -651,11 +651,12 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) shouldIgnoreNonstandardPorts = true; #endif if (shouldIgnoreNonstandardPorts && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_ATAK_FORWARDER, meshtastic_PortNum_ATAK_PLUGIN, - meshtastic_PortNum_PAXCOUNTER_APP, meshtastic_PortNum_IP_TUNNEL_APP, meshtastic_PortNum_AUDIO_APP, - meshtastic_PortNum_PRIVATE_APP, meshtastic_PortNum_DETECTION_SENSOR_APP, meshtastic_PortNum_RANGE_TEST_APP, - meshtastic_PortNum_REMOTE_HARDWARE_APP)) { - LOG_DEBUG("Ignore packet on blacklisted portnum for CORE_PORTNUMS_ONLY"); + !IS_ONE_OF(p->decoded.portnum, meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP, + meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, + meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, + meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, + meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP)) { + LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); cancelSending(p->from, p->id); skipHandle = true; } From b5a8e8f51ba87d873e4eb3e714267b4f4bf39719 Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Mon, 28 Jul 2025 23:51:38 +0100 Subject: [PATCH 25/25] DEBUG_MUTE correctness (#7492) * treewide: make 'ifdef DEBUG_PORT' guards also take into account DEBUG_MUTE * stm32wl: Add a guard against having debug prints turned on without PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF defined --------- Co-authored-by: Ben Meadors --- src/mesh/Router.cpp | 5 +++-- src/modules/ReplyModule.cpp | 2 +- src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- src/modules/Telemetry/DeviceTelemetry.cpp | 2 +- src/modules/Telemetry/EnvironmentTelemetry.cpp | 2 +- src/modules/Telemetry/HealthTelemetry.cpp | 2 +- src/modules/Telemetry/HostMetrics.cpp | 2 +- src/modules/Telemetry/PowerTelemetry.cpp | 2 +- src/modules/TextMessageModule.cpp | 2 +- src/modules/TraceRouteModule.cpp | 2 +- src/modules/WaypointModule.cpp | 2 +- src/platform/stm32wl/architecture.h | 5 +++++ src/sleep.cpp | 2 +- 13 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 993c59dd6..a7508423a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -224,9 +224,10 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) if (!config.lora.override_duty_cycle && myRegion->dutyCycle < 100) { float hourlyTxPercent = airTime->utilizationTXPercent(); if (hourlyTxPercent > myRegion->dutyCycle) { -#ifdef DEBUG_PORT uint8_t silentMinutes = airTime->getSilentMinutes(hourlyTxPercent, myRegion->dutyCycle); + LOG_WARN("Duty cycle limit exceeded. Aborting send for now, you can send again in %d mins", silentMinutes); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->has_reply_id = true; cn->reply_id = p->id; @@ -234,7 +235,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) cn->time = getValidTime(RTCQualityFromNet); sprintf(cn->message, "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); service->sendClientNotification(cn); -#endif + meshtastic_Routing_Error err = meshtastic_Routing_Error_DUTY_CYCLE_LIMIT; if (isFromUs(p)) { // only send NAK to API, not to the mesh abortSendAndNak(err, p); diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index 8892aaa97..434441d49 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -8,7 +8,7 @@ meshtastic_MeshPacket *ReplyModule::allocReply() { assert(currentRequest); // should always be !NULL -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto req = *currentRequest; auto &p = req.decoded; // The incoming message is in p.payload diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 2472b95b1..21a563b9d 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -121,7 +121,7 @@ int32_t AirQualityTelemetryModule::runOnce() bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 43c2dd84c..08fd09db0 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -49,7 +49,7 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket & return false; if (t->which_variant == meshtastic_Telemetry_device_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): air_util_tx=%f, channel_utilization=%f, battery_level=%i, voltage=%f", sender, diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d1b10fa82..8926b171c 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -502,7 +502,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_environment_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): barometric_pressure=%f, current=%f, gas_resistance=%f, relative_humidity=%f, " diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 3a735b1fa..215e49c7a 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -149,7 +149,7 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * bool HealthTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_health_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): temperature=%f, heart_bpm=%d, spO2=%d,", sender, t->variant.health_metrics.temperature, diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index 6a92b15f8..8f10b9228 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -27,7 +27,7 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, return false; if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); if (t->variant.host_metrics.has_user_string) t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index a92013d01..35409edef 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -168,7 +168,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { if (t->which_variant == meshtastic_Telemetry_power_metrics_tag) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); LOG_INFO("(Received from %s): ch1_voltage=%.1f, ch1_current=%.1f, ch2_voltage=%.1f, ch2_current=%.1f, " diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index 970f4429c..72df330c5 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -9,7 +9,7 @@ TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto &p = mp.decoded; LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index bd75c6983..f4eccd667 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -232,7 +232,7 @@ void TraceRouteModule::appendMyIDandSNR(meshtastic_RouteDiscovery *updated, floa void TraceRouteModule::printRoute(meshtastic_RouteDiscovery *r, uint32_t origin, uint32_t dest, bool isTowardsDestination) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) std::string route = "Route traced:\n"; route += vformat("0x%x --> ", origin); for (uint8_t i = 0; i < r->route_count; i++) { diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index aab3ed6bc..4b05d5fa1 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -16,7 +16,7 @@ WaypointModule *waypointModule; ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) auto &p = mp.decoded; LOG_INFO("Received waypoint msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif diff --git a/src/platform/stm32wl/architecture.h b/src/platform/stm32wl/architecture.h index ac2bbe5d1..e131a0a32 100644 --- a/src/platform/stm32wl/architecture.h +++ b/src/platform/stm32wl/architecture.h @@ -32,3 +32,8 @@ #define SX126X_DIO1 1001 #define SX126X_RESET 1003 #define SX126X_BUSY 1004 + +#if !defined(DEBUG_MUTE) && !defined(PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF) +#error \ + "You MUST enable PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF if debug prints are enabled. printf will print uninitialized garbage instead of floats." +#endif \ No newline at end of file diff --git a/src/sleep.cpp b/src/sleep.cpp index 09484f46e..1a5f246c5 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -131,7 +131,7 @@ void initDeepSleep() support busted boards, assume button one was pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0]; */ -#ifdef DEBUG_PORT +#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) // If we booted because our timer ran out or the user pressed reset, send those as fake events RESET_REASON hwReason = rtc_get_reset_reason(0);