From a7f63d5783fe9b84a534191903d77f1e3a93a59e Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 17:04:24 +1000 Subject: [PATCH 01/62] add merge queue --- .github/workflows/merge_queue.yml | 508 ++++++++++++++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 .github/workflows/merge_queue.yml diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml new file mode 100644 index 000000000..9e34727ad --- /dev/null +++ b/.github/workflows/merge_queue.yml @@ -0,0 +1,508 @@ + +name: Merge Queue +concurrency: + group: merge-queue-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +on: + # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. + merge_group: + +env: + FAIL_FAST_PER_ARCH: true + + +jobs: + setup: + strategy: + fail-fast: true + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - check + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: pip + - run: pip install -U platformio + - name: Generate matrix + id: jsonStep + run: | + if [[ "$GITHUB_HEAD_REF" == "" ]]; then + TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) + else + 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 + outputs: + esp32: ${{ steps.jsonStep.outputs.esp32 }} + esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }} + esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }} + 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 }} + + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT + id: version + env: + BUILD_LOCATION: local + outputs: + long: ${{ steps.version.outputs.long }} + deb: ${{ steps.version.outputs.deb }} + + check: + needs: setup + strategy: + fail-fast: true + matrix: ${{ fromJson(needs.setup.outputs.check) }} + + runs-on: ubuntu-latest + if: ${{ github.event_name != 'workflow_dispatch' }} + steps: + - uses: actions/checkout@v4 + - name: Build base + id: base + uses: ./.github/actions/setup-base + - name: Check ${{ matrix.board }} + run: bin/check-all.sh ${{ matrix.board }} + + build-esp32: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.esp32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32 + + build-esp32s3: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32s3 + + build-esp32c3: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c3 + + build-esp32c6: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c6 + + build-nrf52840: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: nrf52840 + + build-rp2040: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2040 + + build-rp2350: + needs: [setup, version] + strategy: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + 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: + fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + matrix: ${{ fromJson(needs.setup.outputs.stm32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: stm32 + + build-debian-src: + if: github.repository == 'meshtastic/firmware' + uses: ./.github/workflows/build_debian_src.yml + with: + series: UNRELEASED + build_location: local + secrets: inherit + + package-pio-deps-native-tft: + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native-tft + secrets: inherit + + test-native: + if: ${{ !contains(github.ref_name, 'event/') }} + uses: ./.github/workflows/test_native.yml + + docker-deb-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-deb-amd64-tft: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alp-amd64-tft: + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-deb-armv7: + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false + + gather-artifacts: + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + needs: + [ + version, + build-esp32, + build-esp32s3, + build-esp32c3, + build-esp32c6, + build-nrf52840, + build-rp2040, + build-rp2350, + build-stm32, + ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - uses: actions/download-artifact@v4 + with: + path: ./ + pattern: firmware-${{matrix.arch}}-* + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Move files up + run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat + + - name: Repackage in single firmware zip + uses: actions/upload-artifact@v4 + with: + name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} + overwrite: true + path: | + ./firmware-*.bin + ./firmware-*.uf2 + ./firmware-*.hex + ./firmware-*-ota.zip + ./device-*.sh + ./device-*.bat + ./littlefs-*.bin + ./bleota*bin + ./Meshtastic_nRF52_factory_erase*.uf2 + retention-days: 30 + + - uses: actions/download-artifact@v4 + with: + name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + # For diagnostics + - name: Show artifacts + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - name: Repackage in single elfs zip + uses: actions/upload-artifact@v4 + with: + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + overwrite: true + path: ./*.elf + retention-days: 30 + + - uses: scruplelesswizard/comment-artifact@main + if: ${{ github.event_name == 'pull_request' }} + with: + name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} + description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + release-artifacts: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + needs: + - version + - gather-artifacts + - build-debian-src + - package-pio-deps-native-tft + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Create release + uses: softprops/action-gh-release@v2 + id: create_release + with: + draft: true + prerelease: true + name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha + tag_name: v${{ needs.version.outputs.long }} + body: | + Autogenerated by github action, developer should edit as required before publishing... + + - name: Download source deb + uses: actions/download-artifact@v4 + with: + pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src + merge-multiple: true + path: ./output/debian-src + + - name: Download `native-tft` pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native-tft + + - name: Zip Linux sources + working-directory: output + run: | + zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src + zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip + gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-firmware: + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-artifacts, version] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + - name: Display structure of downloaded files + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - uses: actions/download-artifact@v4 + with: + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + merge-multiple: true + path: ./elfs + + - name: Zip debug elfs + run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-firmware, version] + env: + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ needs.version.outputs.long }} + enable_jekyll: true From e39b56547e1721dd53c471fc51e145542302a3f0 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 17:08:03 +1000 Subject: [PATCH 02/62] try vars --- .github/workflows/merge_queue.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 9e34727ad..e24d7e906 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -1,8 +1,9 @@ name: Merge Queue -concurrency: - group: merge-queue-${{ github.head_ref || github.run_id }} - cancel-in-progress: true +# Not sure how concu +# concurrency: +# group: merge-queue-${{ github.head_ref || github.run_id }} +# cancel-in-progress: true on: # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. merge_group: @@ -89,7 +90,7 @@ jobs: build-esp32: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.esp32) }} uses: ./.github/workflows/build_firmware.yml with: @@ -100,7 +101,7 @@ jobs: build-esp32s3: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} uses: ./.github/workflows/build_firmware.yml with: @@ -111,7 +112,7 @@ jobs: build-esp32c3: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} uses: ./.github/workflows/build_firmware.yml with: @@ -122,7 +123,7 @@ jobs: build-esp32c6: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} uses: ./.github/workflows/build_firmware.yml with: @@ -133,7 +134,7 @@ jobs: build-nrf52840: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} uses: ./.github/workflows/build_firmware.yml with: @@ -144,7 +145,7 @@ jobs: build-rp2040: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} uses: ./.github/workflows/build_firmware.yml with: @@ -155,7 +156,7 @@ jobs: build-rp2350: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.rp2350) }} uses: ./.github/workflows/build_firmware.yml with: @@ -166,7 +167,7 @@ jobs: build-stm32: needs: [setup, version] strategy: - fail-fast: ${{ env.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.stm32) }} uses: ./.github/workflows/build_firmware.yml with: From 40d728a14b98b75bfa2666e2210fba5c43260b68 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 17:11:13 +1000 Subject: [PATCH 03/62] kerning in yaml. --- .github/workflows/merge_queue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index e24d7e906..66eaeac6a 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -167,7 +167,7 @@ jobs: build-stm32: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} matrix: ${{ fromJson(needs.setup.outputs.stm32) }} uses: ./.github/workflows/build_firmware.yml with: From ea1d968777fe30cbba4a888e5cb34cd96fffd32c Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 17:51:45 +1000 Subject: [PATCH 04/62] update comment --- .github/workflows/merge_queue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 66eaeac6a..1863582fc 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -1,6 +1,6 @@ name: Merge Queue -# Not sure how concu +# Not sure how concurrency works in merge_queue, removing for now. # concurrency: # group: merge-queue-${{ github.head_ref || github.run_id }} # cancel-in-progress: true From 590db89643cf48aa5a60102e6990235ce277ad13 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 18:06:44 +1000 Subject: [PATCH 05/62] lint etc --- .github/workflows/merge_queue.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 1863582fc..d3df35ac2 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -1,4 +1,3 @@ - name: Merge Queue # Not sure how concurrency works in merge_queue, removing for now. # concurrency: @@ -11,7 +10,6 @@ on: env: FAIL_FAST_PER_ARCH: true - jobs: setup: strategy: @@ -90,7 +88,7 @@ jobs: build-esp32: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.esp32) }} uses: ./.github/workflows/build_firmware.yml with: @@ -101,7 +99,7 @@ jobs: build-esp32s3: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} uses: ./.github/workflows/build_firmware.yml with: @@ -112,7 +110,7 @@ jobs: build-esp32c3: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} uses: ./.github/workflows/build_firmware.yml with: @@ -123,7 +121,7 @@ jobs: build-esp32c6: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} uses: ./.github/workflows/build_firmware.yml with: @@ -134,7 +132,7 @@ jobs: build-nrf52840: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} uses: ./.github/workflows/build_firmware.yml with: @@ -145,7 +143,7 @@ jobs: build-rp2040: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} uses: ./.github/workflows/build_firmware.yml with: @@ -156,7 +154,7 @@ jobs: build-rp2350: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.rp2350) }} uses: ./.github/workflows/build_firmware.yml with: @@ -167,7 +165,7 @@ jobs: build-stm32: needs: [setup, version] strategy: - fail-fast: ${{ vars.FAIL_FAST_PER_ARCH }} + fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }} matrix: ${{ fromJson(needs.setup.outputs.stm32) }} uses: ./.github/workflows/build_firmware.yml with: From 8791cd7851c467dc48d43860e3a31de9d2942f86 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 18:11:57 +1000 Subject: [PATCH 06/62] touching to check grandfathering --- .github/workflows/main_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index ed14907dc..0a72bdc3e 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -3,7 +3,7 @@ concurrency: group: ci-${{ github.head_ref || github.run_id }} cancel-in-progress: true on: - # # Triggers the workflow on push but only for the master branch + # # Triggers the workflow on push but only for the main branches push: branches: - master From 18d005d7e6805b95f50eebf02a43fa289fc795c8 Mon Sep 17 00:00:00 2001 From: Dane Evans Date: Fri, 1 Aug 2025 18:17:34 +1000 Subject: [PATCH 07/62] explicit ignores --- .github/workflows/main_matrix.yml | 1 + .github/workflows/merge_queue.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 0a72bdc3e..815b03c86 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -258,6 +258,7 @@ jobs: push: false gather-artifacts: + # trunk-ignore(checkov/CKV2_GHA_1) permissions: contents: write pull-requests: write diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index d3df35ac2..e2264e250 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -243,6 +243,7 @@ jobs: push: false gather-artifacts: + # trunk-ignore(checkov/CKV2_GHA_1) permissions: contents: write pull-requests: write From 5d3c92f1a28654c7e3bf885113663b1469d1ca76 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Mon, 15 Sep 2025 12:50:38 -0700 Subject: [PATCH 08/62] When DEBUG_HEAP is defined, add free heap bytes to every log line in RedirectablePrint::log_to_serial --- platformio.ini | 1 + src/RedirectablePrint.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/platformio.ini b/platformio.ini index 47b5f823d..30e8b1aa6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -55,6 +55,7 @@ build_flags = -Wno-missing-field-initializers -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 + #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs monitor_speed = 115200 monitor_filters = direct diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index efab84399..9624a4593 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -4,6 +4,7 @@ #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" +#include "memGet.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include #include @@ -166,6 +167,16 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, print(thread->ThreadName); print("] "); } + +#ifdef DEBUG_HEAP + // Add heap free space bytes prefix before every log message +#ifdef ARCH_PORTDUINO + ::printf("[heap %u] ", memGet.getFreeHeap()); +#else + printf("[heap %u] ", memGet.getFreeHeap()); +#endif +#endif // DEBUG_HEAP + r += vprintf(logLevel, format, arg); } From b9d53d667ee9e6b8eeede142c0e9c6c92dfeab97 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Sep 2025 02:29:47 +0200 Subject: [PATCH 09/62] Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753) * Added compatibility between nodes on different Presets through `Mesh via UDP` * Optimize multicast handling and channel mapping - FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression. - Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets to the local default channel via isDefaultChannel() - UdpMulticastHandler: set transport_mechanism only after successful decode * trunk fmt * Move setting transport mechanism. --------- Co-authored-by: GUVWAF --- src/mesh/Channels.cpp | 26 +++++++++++++++++++++++ src/mesh/Channels.h | 2 ++ src/mesh/Router.cpp | 30 +++++++++++++++++++++++++++ src/mesh/udp/UdpMulticastHandler.h | 2 +- variants/esp32s3/t-deck-pro/variant.h | 15 +++++++------- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4ef41ddfb..4c0a0edad 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -423,6 +423,32 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) } } +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) +{ + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; + ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; + } + } + return false; +} + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) * * This method is called before encoding outbound packets diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 7873a306a..b53f552fa 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -94,6 +94,8 @@ class Channels bool ensureLicensedOperation(); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + private: /** Given a channel index, change to use the crypto key specified by that index * diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 37b70dab6..09fb079c5 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -430,6 +430,36 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) } } } + +#if HAS_UDP_MULTICAST + // Fallback: for UDP multicast, try default preset names with default PSK if normal channel match failed + if (!decrypted && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + if (channels.setDefaultPresetCryptoForHash(p->channel)) { + memcpy(bytes, p->encrypted.bytes, rawSize); + crypto->decrypt(p->from, p->id, rawSize, bytes); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && + decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + // Map to our local default channel index (name+PSK default), not necessarily primary + ChannelIndex defaultIndex = channels.getPrimaryIndex(); + for (ChannelIndex i = 0; i < channels.getNumChannels(); ++i) { + if (channels.isDefaultChannel(i)) { + defaultIndex = i; + break; + } + } + chIndex = defaultIndex; + decrypted = true; + } else { + LOG_WARN("UDP fallback decode attempted but failed for hash 0x%x", p->channel); + } + } + } +#endif if (decrypted) { // parsing was successful p->channel = chIndex; // change to store the index instead of the hash diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 9650668a8..2df8686a3 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -50,10 +50,10 @@ class UdpMulticastHandler final LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif meshtastic_MeshPacket mp; - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; mp.pki_encrypted = false; mp.public_key.size = 0; memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index abe0a772a..35cb99435 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -93,11 +93,10 @@ // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) -#define MODEM_POWER_EN 41 -#define MODEM_PWRKEY 40 -#define MODEM_RST 9 -#define MODEM_RI 7 -#define MODEM_DTR 8 -#define MODEM_RX 10 -#define MODEM_TX 11 - +#define MODEM_POWER_EN 41 +#define MODEM_PWRKEY 40 +#define MODEM_RST 9 +#define MODEM_RI 7 +#define MODEM_DTR 8 +#define MODEM_RX 10 +#define MODEM_TX 11 From d31e3839fbbcd13b92f8d499add265c5c4c1d441 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 16 Sep 2025 06:11:29 -0500 Subject: [PATCH 10/62] Use long name --- src/mesh/Channels.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4c0a0edad..aec112a3e 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -428,7 +428,8 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + const char *name = + DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, false); if (!name) continue; if (strcmp(name, "Invalid") == 0) From 22fcd102a081bf1fda6977e67c8ce44e8d40ebad Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:41:22 +0100 Subject: [PATCH 11/62] (resubmission) Manual GitHub actions to allow building one target or arch (#7997) * Reset the modified files * Fix some changes * Fix some changes * Trunk. That is all. --------- Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 500 +++++++++++++++++++++++++ .github/workflows/build_one_target.yml | 395 +++++++++++++++++++ .github/workflows/daily_packaging.yml | 4 + .github/workflows/main_matrix.yml | 17 +- 4 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build_one_arch.yml create mode 100644 .github/workflows/build_one_target.yml diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml new file mode 100644 index 000000000..5901a335c --- /dev/null +++ b/.github/workflows/build_one_arch.yml @@ -0,0 +1,500 @@ +name: Build One Arch + +on: + workflow_dispatch: + inputs: + arch: + type: choice + options: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - native + +jobs: + setup: + strategy: + fail-fast: false + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: pip + - run: pip install -U platformio + - name: Generate matrix + id: jsonStep + run: | + if [[ "$GITHUB_HEAD_REF" == "" ]]; then + TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} extra) + else + TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} pr) + fi + echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS" + echo "${{inputs.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT + outputs: + esp32: ${{ steps.jsonStep.outputs.esp32 }} + esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }} + esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }} + 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 }} + + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT + id: version + env: + BUILD_LOCATION: local + outputs: + long: ${{ steps.version.outputs.long }} + deb: ${{ steps.version.outputs.deb }} + + build-esp32: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32 + + build-esp32s3: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32s3'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32s3 + + build-esp32c3: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c3'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c3 + + build-esp32c6: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c6'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c6 + + build-nrf52840: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'nrf52840'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: nrf52840 + + build-rp2040: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'rp2040'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2040 + + build-rp2350: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == '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: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'stm32' }} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.stm32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: stm32 + + build-debian-src: + if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/build_debian_src.yml + with: + series: UNRELEASED + build_location: local + secrets: inherit + + package-pio-deps-native-tft: + if: ${{ inputs.arch == 'native' }} + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native-tft + secrets: inherit + + test-native: + if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }} + uses: ./.github/workflows/test_native.yml + + docker-deb-amd64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-deb-amd64-tft: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alp-amd64-tft: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-deb-armv7: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false + + gather-artifacts: + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + needs: + [ + version, + build-esp32, + build-esp32s3, + build-esp32c3, + build-esp32c6, + build-nrf52840, + build-rp2040, + build-rp2350, + build-stm32, + ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - uses: actions/download-artifact@v4 + with: + path: ./ + pattern: firmware-${{inputs.arch}}-* + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Move files up + run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat + + - name: Repackage in single firmware zip + uses: actions/upload-artifact@v4 + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + overwrite: true + path: | + ./firmware-*.bin + ./firmware-*.uf2 + ./firmware-*.hex + ./firmware-*-ota.zip + ./device-*.sh + ./device-*.bat + ./littlefs-*.bin + ./bleota*bin + ./Meshtastic_nRF52_factory_erase*.uf2 + retention-days: 30 + + - uses: actions/download-artifact@v4 + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + # For diagnostics + - name: Show artifacts + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - name: Repackage in single elfs zip + uses: actions/upload-artifact@v4 + with: + name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + overwrite: true + path: ./*.elf + retention-days: 30 + + - uses: scruplelesswizard/comment-artifact@main + if: ${{ github.event_name == 'pull_request' }} + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + release-artifacts: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + needs: + - version + - gather-artifacts + - build-debian-src + - package-pio-deps-native-tft + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Create release + uses: softprops/action-gh-release@v2 + id: create_release + with: + draft: true + prerelease: true + name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha + tag_name: v${{ needs.version.outputs.long }} + body: | + Autogenerated by github action, developer should edit as required before publishing... + + - name: Download source deb + uses: actions/download-artifact@v4 + with: + pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src + merge-multiple: true + path: ./output/debian-src + + - name: Download `native-tft` pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native-tft + + - name: Zip Linux sources + working-directory: output + run: | + zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src + zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip + gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-firmware: + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-artifacts, version] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + - name: Display structure of downloaded files + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - uses: actions/download-artifact@v4 + with: + name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + merge-multiple: true + path: ./elfs + + - name: Zip debug elfs + run: zip -j -9 -r ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./elfs + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-firmware, version] + env: + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ needs.version.outputs.long }} + enable_jekyll: true diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml new file mode 100644 index 000000000..07478ff93 --- /dev/null +++ b/.github/workflows/build_one_target.yml @@ -0,0 +1,395 @@ +name: Build One Target + +on: + workflow_dispatch: + inputs: + arch: + type: choice + options: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - native + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. + + # find-target: + # type: boolean + # default: true + # description: 'Find the available targets' +jobs: + find-targets: + if: ${{ inputs.target == '' }} + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: pip + - run: pip install -U platformio + - name: Generate matrix + id: jsonStep + run: | + TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} extra) + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "Targets:" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | sed 's/[][]//g; s/", "/\n- /g; s/"//g; s/^/- /' >> $GITHUB_STEP_SUMMARY + + version: + if: ${{ inputs.target != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT + id: version + env: + BUILD_LOCATION: local + outputs: + long: ${{ steps.version.outputs.long }} + deb: ${{ steps.version.outputs.deb }} + + build-arch: + if: ${{ inputs.target != '' && inputs.arch != 'native' }} + needs: [version] + strategy: + fail-fast: false + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ inputs.target }} + platform: ${{ inputs.arch }} + + build-debian-src: + if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }} + uses: ./.github/workflows/build_debian_src.yml + with: + series: UNRELEASED + build_location: local + secrets: inherit + + package-pio-deps-native-tft: + if: ${{ inputs.arch == 'native' }} + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native-tft + secrets: inherit + + test-native: + if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }} + uses: ./.github/workflows/test_native.yml + + docker-deb-amd64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-deb-amd64-tft: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alp-amd64-tft: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-deb-armv7: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false + + gather-artifacts: + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + runs-on: ubuntu-latest + needs: [version, build-arch] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - uses: actions/download-artifact@v4 + with: + path: ./ + pattern: firmware-*-* + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Move files up + run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat + + - name: Repackage in single firmware zip + uses: actions/upload-artifact@v4 + with: + name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} + overwrite: true + path: | + ./firmware-*.bin + ./firmware-*.uf2 + ./firmware-*.hex + ./firmware-*-ota.zip + ./device-*.sh + ./device-*.bat + ./littlefs-*.bin + ./bleota*bin + ./Meshtastic_nRF52_factory_erase*.uf2 + retention-days: 30 + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-*-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + # For diagnostics + - name: Show artifacts + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output + + - name: Repackage in single elfs zip + uses: actions/upload-artifact@v4 + with: + name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + overwrite: true + path: ./*.elf + retention-days: 30 + + - uses: scruplelesswizard/comment-artifact@main + if: ${{ github.event_name == 'pull_request' }} + with: + name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} + description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + release-artifacts: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}} + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + needs: + - version + - gather-artifacts + - build-debian-src + - package-pio-deps-native-tft + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Create release + uses: softprops/action-gh-release@v2 + id: create_release + with: + draft: true + prerelease: true + name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha + tag_name: v${{ needs.version.outputs.long }} + body: | + Autogenerated by github action, developer should edit as required before publishing... + + - name: Download source deb + uses: actions/download-artifact@v4 + with: + pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src + merge-multiple: true + path: ./output/debian-src + + - name: Download `native-tft` pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native-tft + + - name: Zip Linux sources + working-directory: output + run: | + zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src + zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip + gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-firmware: + strategy: + fail-fast: false + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}} + needs: [release-artifacts, version] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-*-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + - name: Display structure of downloaded files + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output + + - uses: actions/download-artifact@v4 + with: + pattern: debug-elfs-*-${{ needs.version.outputs.long }}.zip + merge-multiple: true + path: ./elfs + + - name: Zip debug elfs + run: zip -j -9 -r ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./elfs + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' && inputs.target != '' }} + needs: [release-firmware, version] + env: + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ needs.version.outputs.long }} + enable_jekyll: true diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index df5ed27d5..392faeb8a 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -21,12 +21,14 @@ permissions: jobs: docker-multiarch: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_manifest.yml with: release_channel: daily secrets: inherit package-ppa: + if: github.repository == 'meshtastic/firmware' strategy: fail-fast: false matrix: @@ -42,6 +44,7 @@ jobs: secrets: inherit package-obs: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/package_obs.yml with: obs_project: network:Meshtastic:daily @@ -49,6 +52,7 @@ jobs: secrets: inherit hook-copr: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/hook_copr.yml with: copr_project: daily diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 6ff12221b..9b4199223 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -27,6 +27,7 @@ on: jobs: setup: + if: github.repository == 'meshtastic/firmware' strategy: fail-fast: false matrix: @@ -74,6 +75,7 @@ jobs: check: ${{ steps.jsonStep.outputs.check }} version: + if: github.repository == 'meshtastic/firmware' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -95,7 +97,7 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.check) }} runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} + if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - uses: actions/checkout@v5 - name: Build base @@ -208,10 +210,11 @@ jobs: secrets: inherit test-native: - if: ${{ !contains(github.ref_name, 'event/') }} + if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} uses: ./.github/workflows/test_native.yml docker-deb-amd64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -220,6 +223,7 @@ jobs: push: false docker-deb-amd64-tft: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -229,6 +233,7 @@ jobs: pio_env: native-tft docker-alp-amd64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: alpine @@ -237,6 +242,7 @@ jobs: push: false docker-alp-amd64-tft: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: alpine @@ -246,6 +252,7 @@ jobs: pio_env: native-tft docker-deb-arm64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -254,6 +261,7 @@ jobs: push: false docker-deb-armv7: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -262,6 +270,7 @@ jobs: push: false gather-artifacts: + if: github.repository == 'meshtastic/firmware' permissions: contents: write pull-requests: write @@ -361,7 +370,7 @@ jobs: release-artifacts: runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} needs: @@ -436,7 +445,7 @@ jobs: - rp2350 - stm32 runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}} needs: [release-artifacts, version] steps: - name: Checkout From 13ebceb3bc5d01a4b3d82187e82ae9d45461a700 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 06:42:08 -0500 Subject: [PATCH 12/62] Update meshtastic/device-ui digest to 9ed5355 (#7987) 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 47b5f823d..e2eb55dce 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,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/3677476c8a823ee85056b5fb1d146a3e193f8276.zip + https://github.com/meshtastic/device-ui/archive/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From cc3c568501cb258ffc97487fab731610f6e5120b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:20:44 -0500 Subject: [PATCH 13/62] Update protobufs (#8005) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/protobufs b/protobufs index 8caf42396..945b796a9 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 8caf42396438f0d8a0305143485fd671c1fc7126 +Subproject commit 945b796a982f38171a9e0d28b5c8b1f7d53c5cd1 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 2a4e77870..294f0beac 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -259,8 +259,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_DECK_PRO = 102, /* Lilygo TLora Pager */ meshtastic_HardwareModel_T_LORA_PAGER = 103, - /* GAT562 Mesh Trial Tracker */ - meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104, + /* M5Stack Reserved */ + meshtastic_HardwareModel_M5STACK_RESERVED = 104, /* 0x68 */ /* RAKwireless WisMesh Tag */ meshtastic_HardwareModel_WISMESH_TAG = 105, /* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */ @@ -274,6 +274,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_ECHO_LITE = 109, /* New Heltec LoRA32 with ESP32-S3 CPU */ meshtastic_HardwareModel_HELTEC_V4 = 110, + /* M5Stack C6L */ + meshtastic_HardwareModel_M5STACK_C6L = 111, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From 6f5bdd73cb7dd05ee70584f28b9378ef87b9ac48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 06:09:18 -0500 Subject: [PATCH 14/62] Upgrade trunk (#7868) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index e10e20a04..c1fde9602 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,15 +8,15 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.469 - - renovate@41.94.0 + - checkov@3.2.471 + - renovate@41.115.2 - prettier@3.6.2 - - trufflehog@3.90.5 + - trufflehog@3.90.6 - yamllint@1.37.1 - bandit@1.8.6 - trivy@0.66.0 - taplo@0.10.0 - - ruff@0.12.11 + - ruff@0.13.0 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 From ba18467bd1459aae0aca9546a2f94571b8ed5f1c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 17 Sep 2025 08:37:51 -0500 Subject: [PATCH 15/62] Auto-favorite remote admin node --- src/modules/AdminModule.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 78c101765..441a6e12b 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -104,6 +104,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { LOG_INFO("PKC admin payload with authorized sender key"); + auto remoteNode = nodeDB->getMeshNode(mp.from); + if (remoteNode && !remoteNode->is_favorite) { + remoteNode->is_favorite = true; + } } else { myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); From 71d84404c62af883eb73c261eddb31968d202bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 17 Sep 2025 22:40:55 +0200 Subject: [PATCH 16/62] add WIP for Unit C6L (#7433) * add WIP for Unit C6L * adapt to new config structure * Add c6l BLE and screen support (#7991) * Minor c6l fix * Move out of PRIVATE_HW --------- Co-authored-by: Austin Co-authored-by: Ben Meadors Co-authored-by: Jonathan Bennett Co-authored-by: Jason P Co-authored-by: Markus --- src/AmbientLightingThread.h | 6 +- src/detect/ScanI2CTwoWire.cpp | 2 + src/graphics/Screen.cpp | 40 +++++- src/graphics/Screen.h | 2 + src/graphics/ScreenFonts.h | 4 + src/graphics/SharedUIDisplay.cpp | 4 +- src/graphics/draw/DebugRenderer.cpp | 42 +++++- src/graphics/draw/MenuHandler.cpp | 83 ++++++++++- src/graphics/draw/MenuHandler.h | 1 + src/graphics/draw/MessageRenderer.cpp | 70 +++++++++- src/graphics/draw/NodeListRenderer.cpp | 32 ++++- src/graphics/draw/NotificationRenderer.cpp | 130 ++++++++++++++++++ src/graphics/draw/UIRenderer.cpp | 88 ++++++++++-- src/graphics/images.h | 5 +- src/graphics/img/icon_small.xbm | 30 ++++ src/input/i2cButton.cpp | 95 +++++++++++++ src/input/i2cButton.h | 18 +++ src/main.cpp | 13 +- src/mesh/NodeDB.cpp | 2 +- src/modules/CannedMessageModule.cpp | 20 ++- src/modules/Modules.cpp | 4 + src/nimble/NimbleBluetooth.cpp | 108 ++++++++++++++- src/nimble/NimbleBluetooth.h | 5 + src/platform/esp32/architecture.h | 2 + .../esp32c6/m5stack_unitc6l/pins_arduino.h | 28 ++++ .../esp32c6/m5stack_unitc6l/platformio.ini | 35 +++++ variants/esp32c6/m5stack_unitc6l/variant.cpp | 74 ++++++++++ variants/esp32c6/m5stack_unitc6l/variant.h | 52 +++++++ 28 files changed, 952 insertions(+), 43 deletions(-) create mode 100644 src/graphics/img/icon_small.xbm create mode 100644 src/input/i2cButton.cpp create mode 100644 src/input/i2cButton.h create mode 100644 variants/esp32c6/m5stack_unitc6l/pins_arduino.h create mode 100644 variants/esp32c6/m5stack_unitc6l/platformio.ini create mode 100644 variants/esp32c6/m5stack_unitc6l/variant.cpp create mode 100644 variants/esp32c6/m5stack_unitc6l/variant.h diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index e4ef3b443..947b1e054 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -183,9 +183,9 @@ class AmbientLightingThread : public concurrency::OSThread #endif #endif pixels.show(); - LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 5cb4fca32..01a630b52 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -294,6 +294,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = AHT10; break; #endif +#if !defined(M5STACK_UNITC6L) case INA_ADDR: case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: @@ -340,6 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) // else: probably a RAK12500/UBLOX GPS on I2C } break; +#endif case MCP9808_ADDR: // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index dea08d5ba..0a2229d0e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -317,6 +317,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_SPISSD1306) + dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); + if (!dispdev->init()) { + LOG_DEBUG("Error: SSD1306 not detected!"); + } else { + static_cast(dispdev)->setHorizontalOffset(32); + LOG_INFO("SSD1306 init success"); + } #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, @@ -507,7 +515,7 @@ void Screen::setup() // === Apply loaded brightness === #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); -#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) +#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) dispdev->setBrightness(brightness); #endif LOG_INFO("Applied screen brightness: %d", brightness); @@ -554,7 +562,7 @@ void Screen::setup() static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); -#else +#elif !defined(M5STACK_UNITC6L) dispdev->flipScreenVertically(); #endif } @@ -692,7 +700,11 @@ int32_t Screen::runOnce() #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { +#if defined(M5STACK_UNITC6L) + menuHandler::LoraRegionPicker(); +#else menuHandler::OnboardMessage(); +#endif } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { @@ -890,8 +902,12 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) fsi.positions.clock = numframes; +#if defined(M5STACK_UNITC6L) + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; +#else normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame; +#endif indicatorIcons.push_back(digital_icon_clock); #endif @@ -1226,6 +1242,10 @@ void Screen::handleShowNextFrame() void Screen::setFastFramerate() { +#if defined(M5STACK_UNITC6L) + dispdev->clear(); + dispdev->display(); +#endif // We are about to start a transition so speed up fps targetFramerate = SCREEN_TRANSITION_FRAMERATE; @@ -1297,13 +1317,23 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) } } else { if (longName && longName[0]) { +#if defined(M5STACK_UNITC6L) + strcpy(banner, "New Message"); +#else snprintf(banner, sizeof(banner), "New Message from\n%s", longName); +#endif + } else { strcpy(banner, "New Message"); } } - +#if defined(M5STACK_UNITC6L) + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + playLongBeep(); +#else screen->showSimpleBanner(banner, 3000); +#endif } } @@ -1386,7 +1416,11 @@ int Screen::handleInputEvent(const InputEvent *event) if (devicestate.rx_text_message.from) { menuHandler::messageResponseMenu(); } else { +#if defined(M5STACK_UNITC6L) + menuHandler::textMessageMenu(); +#else menuHandler::textMessageBaseMenu(); +#endif } } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..ecc39ac60 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -81,6 +81,8 @@ class Screen #include #elif defined(USE_ST7789) #include +#elif defined(USE_SPISSD1306) +#include #else // the SH1106/SSD1306 variant is auto-detected #include diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index a25417b05..c497a27b2 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -79,6 +79,10 @@ #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 +#elif defined(M5STACK_UNITC6L) +#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 +#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13 +#define FONT_LARGE FONT_SMALL_LOCAL // Height: 13 #else #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index b458e54e4..13691665a 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -124,7 +124,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; - +#if !defined(M5STACK_UNITC6L) // === Battery Icons === if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; @@ -337,7 +337,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } } } - +#endif display->setColor(WHITE); // Reset for other UI } diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index d5835a335..6137ddef8 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -277,12 +277,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); // Line 1 (Still) +#if !defined(M5STACK_UNITC6L) display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); if (config.display.heading_bold) display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->setColor(WHITE); - +#endif // Setup string to assemble analogClock string std::string analogClock = ""; @@ -386,17 +387,24 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char shortnameble[35]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); +#if defined(M5STACK_UNITC6L) + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); +#else snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); +#endif int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Second Row: Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { +#if defined(M5STACK_UNITC6L) + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); +#else snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); +#endif } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -408,9 +416,17 @@ 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) { +#if defined(M5STACK_UNITC6L) + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); +#else snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); +#endif } else { +#if defined(M5STACK_UNITC6L) + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); +#else snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); +#endif } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { @@ -420,6 +436,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); +#if !defined(M5STACK_UNITC6L) // === Fourth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; @@ -476,6 +493,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], chUtilPercentage); +#endif } // **************************** @@ -501,8 +519,11 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, #ifdef USE_EINK barsOffset -= 12; #endif +#if defined(M5STACK_UNITC6L) + const int barX = x + 45 + barsOffset; +#else const int barX = x + 40 + barsOffset; - +#endif auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; @@ -527,7 +548,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Label display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(labelX, getTextPositions(display)[line], label); - +#if !defined(M5STACK_UNITC6L) // Bar int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); @@ -535,7 +556,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); - +#endif // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); @@ -588,10 +609,16 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, line += 1; } line += 1; + char appversionstr[35]; snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION)); char appversionstr_formatted[40]; char *lastDot = strrchr(appversionstr, '.'); +#if defined(M5STACK_UNITC6L) + if (lastDot != nullptr) { + *lastDot = '\0'; // truncate string + } +#else if (lastDot) { size_t prefixLen = lastDot - appversionstr; strncpy(appversionstr_formatted, appversionstr, prefixLen); @@ -602,10 +629,12 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); appversionstr[sizeof(appversionstr) - 1] = '\0'; } +#endif int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line], appversionstr); + display->drawString(nameX, getTextPositions(display)[line], appversionstr); +#if !defined(M5STACK_UNITC6L) if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it line += 1; char uptimeStr[32] = ""; @@ -624,6 +653,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line], uptimeStr); } +#endif } } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index bcd8d8ee8..ba554dbd6 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -79,7 +79,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration) "NP_865", "BR_902"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "LoRa Region"; +#else bannerOptions.message = "Set the LoRa region"; +#endif bannerOptions.durationMs = duration; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 27; @@ -260,7 +264,11 @@ void menuHandler::TZPicker() void menuHandler::clockMenu() { +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; +#else static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; +#endif enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "Clock Action"; @@ -284,8 +292,11 @@ void menuHandler::clockMenu() void menuHandler::messageResponseMenu() { enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; - +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"}; +#else static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; +#endif static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; int options = 3; @@ -299,7 +310,11 @@ void menuHandler::messageResponseMenu() optionsEnumArray[options++] = Aloud; #endif BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Message"; +#else bannerOptions.message = "Message Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -349,7 +364,11 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Send Position"; optionsEnumArray[options++] = Position; +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "New Preset"; +#else optionsArray[options] = "New Preset Msg"; +#endif optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; @@ -357,7 +376,11 @@ void menuHandler::homeBaseMenu() } BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Home"; +#else bannerOptions.message = "Home Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -396,6 +419,11 @@ void menuHandler::homeBaseMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::textMessageMenu() +{ + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); +} + void menuHandler::textMessageBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, enumEnd }; @@ -439,11 +467,17 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif - +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "Bluetooth"; +#else optionsArray[options] = "Bluetooth Toggle"; +#endif optionsEnumArray[options++] = Bluetooth; - +#if defined(M5STACK_UNITC6L) + optionsArray[options] = "Power"; +#else optionsArray[options] = "Reboot/Shutdown"; +#endif optionsEnumArray[options++] = PowerMenu; if (test_enabled) { @@ -452,7 +486,11 @@ void menuHandler::systemBaseMenu() } BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "System"; +#else bannerOptions.message = "System Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; @@ -485,7 +523,11 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[enumEnd] = {"Back", "New Preset"}; +#else static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; +#endif static int optionsEnumArray[enumEnd] = {Back, Preset}; int options = 2; @@ -493,13 +535,19 @@ void menuHandler::favoriteBaseMenu() optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } +#if !defined(M5STACK_UNITC6L) optionsArray[options] = "Trace Route"; optionsEnumArray[options++] = TraceRoute; +#endif optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Favorites"; +#else bannerOptions.message = "Favorites Action"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -554,11 +602,19 @@ void menuHandler::positionBaseMenu() void menuHandler::nodeListMenu() { enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; +#if defined(M5STACK_UNITC6L) + static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"}; +#else static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; +#if defined(M5STACK_UNITC6L) + bannerOptions.optionsCount = 3; +#else bannerOptions.optionsCount = 5; +#endif bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -665,7 +721,11 @@ void menuHandler::BluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Bluetooth"; +#else bannerOptions.message = "Toggle Bluetooth"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { @@ -857,7 +917,11 @@ void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Reboot"; +#else bannerOptions.message = "Reboot Device?"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -877,7 +941,11 @@ void menuHandler::shutdownMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Shutdown"; +#else bannerOptions.message = "Shutdown Device?"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -894,7 +962,12 @@ void menuHandler::shutdownMenu() void menuHandler::addFavoriteMenu() { +#if defined(M5STACK_UNITC6L) + screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void { +#else screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { + +#endif LOG_WARN("Nodenum: %u", nodenum); nodeDB->set_favorite(true, nodenum); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); @@ -1090,7 +1163,11 @@ void menuHandler::powerMenu() #endif BannerOverlayOptions bannerOptions; +#if defined(M5STACK_UNITC6L) + bannerOptions.message = "Power"; +#else bannerOptions.message = "Reboot / Shutdown"; +#endif bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index b15cf237d..ed49a89fb 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -76,6 +76,7 @@ class menuHandler static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); + static void textMessageMenu(); private: static void saveUIConfig(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 117829167..6971826de 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -181,12 +181,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - +#if defined(M5STACK_UNITC6L) + const int fixedTopHeight = 24; + const int windowX = 0; + const int windowY = fixedTopHeight; + const int windowWidth = 64; + const int windowHeight = SCREEN_HEIGHT - fixedTopHeight; +#else const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; const int textWidth = SCREEN_WIDTH; +#endif bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; @@ -201,7 +208,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 graphics::drawCommonHeader(display, x, y, titleStr); const char *messageString = "No messages"; int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); +#if defined(M5STACK_UNITC6L) + display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString); +#else display->drawString(center_text, getTextPositions(display)[2], messageString); +#endif return; } @@ -209,6 +220,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); char headerStr[80]; const char *sender = "???"; +#if defined(M5STACK_UNITC6L) + if (node && node->has_user) + sender = node->user.short_name; +#else if (node && node->has_user) { if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { sender = node->user.long_name; @@ -216,6 +231,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 sender = node->user.short_name; } } +#endif uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint8_t timestampHours, timestampMinutes; int32_t daysAgo; @@ -235,10 +251,61 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 sender); } } else { +#if defined(M5STACK_UNITC6L) + snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); +#else snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), sender); +#endif } +#if defined(M5STACK_UNITC6L) + graphics::drawCommonHeader(display, x, y, titleStr); + int headerY = getTextPositions(display)[1]; + display->drawString(x, headerY, headerStr); + for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) { + display->setPixel(separatorX, fixedTopHeight - 1); + } + cachedLines.clear(); + std::string fullMsg(messageBuf); + std::string currentLine; + for (size_t i = 0; i < fullMsg.size();) { + unsigned char c = fullMsg[i]; + size_t charLen = 1; + if ((c & 0xE0) == 0xC0) + charLen = 2; + else if ((c & 0xF0) == 0xE0) + charLen = 3; + else if ((c & 0xF8) == 0xF0) + charLen = 4; + std::string nextChar = fullMsg.substr(i, charLen); + std::string testLine = currentLine + nextChar; + if (display->getStringWidth(testLine.c_str()) > windowWidth) { + cachedLines.push_back(currentLine); + currentLine = nextChar; + } else { + currentLine = testLine; + } + i += charLen; + } + if (!currentLine.empty()) + cachedLines.push_back(currentLine); + cachedHeights = calculateLineHeights(cachedLines, emotes); + int yOffset = windowY; + int linesDrawn = 0; + for (size_t i = 0; i < cachedLines.size(); ++i) { + if (linesDrawn >= 2) + break; + int lineHeight = cachedHeights[i]; + if (yOffset + lineHeight > windowY + windowHeight) + break; + drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes); + yOffset += lineHeight; + linesDrawn++; + } + screen->forceDisplay(); +#else uint32_t now = millis(); #ifndef EXCLUDE_EMOJI // === Bounce animation setup === @@ -355,6 +422,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Draw header at the end to sort out overlapping elements graphics::drawCommonHeader(display, x, y, titleStr); +#endif } std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index d8746fb69..7d6a38dd3 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -21,6 +21,10 @@ extern bool haveGlyphs(const char *str); // Global screen instance extern graphics::Screen *screen; +#if defined(M5STACK_UNITC6L) +static uint32_t lastSwitchTime = 0; +#else +#endif namespace graphics { namespace NodeListRenderer @@ -393,9 +397,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; - +#if defined(M5STACK_UNITC6L) + int columnWidth = display->getWidth(); +#else int columnWidth = display->getWidth() / 2; - +#endif display->clear(); // Draw the battery/time header @@ -408,8 +414,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int visibleNodeRows = totalRowsAvailable; +#if defined(M5STACK_UNITC6L) + int totalColumns = 1; +#else int totalColumns = 2; - +#endif int startIndex = scrollIndex * visibleNodeRows * totalColumns; if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { startIndex++; // skip own node @@ -445,12 +454,14 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } } +#if !defined(M5STACK_UNITC6L) // Draw column separator if (shownCount > 0) { const int firstNodeY = y + 3; drawColumnSeparator(display, x, firstNodeY, lastNodeY); } +#endif const int scrollStartY = y + 3; drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } @@ -468,6 +479,13 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, unsigned long now = millis(); +#if defined(M5STACK_UNITC6L) + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } +#endif // On very first call (on boot or state enter) if (lastRenderedMode == MODE_COUNT) { currentMode = MODE_LAST_HEARD; @@ -522,6 +540,14 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); +#if defined(M5STACK_UNITC6L) + display->clear(); + uint32_t now = millis(); + if (now - lastSwitchTime >= 2000) { + display->display(); + lastSwitchTime = now; + } +#endif if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { #if HAS_GPS if (screen->hasHeading()) { diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3d635e588..c2bd1ba66 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -459,6 +459,135 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay // count lines uint16_t boxWidth = hPadding * 2 + maxWidth; +#if defined(M5STACK_UNITC6L) + if (needs_bell) { + if (isHighResolution && boxWidth <= 150) + boxWidth += 26; + if (!isHighResolution && boxWidth <= 100) + boxWidth += 20; + } + + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + if (visibleTotalLines == 1) + boxHeight += (isHighResolution ? 4 : 3); + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + if (totalLines > visibleTotalLines) + boxWidth += (isHighResolution ? 4 : 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + + if (visibleTotalLines == 1) { + boxTop += 25; + } + if (alertBannerOptions < 3) { + int missingLines = 3 - alertBannerOptions; + int moveUp = missingLines * (effectiveLineHeight / 2); + boxTop -= moveUp; + if (boxTop < 0) + boxTop = 0; + } + + // === Draw Box === + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + int16_t lineY = boxTop + vPadding; + int swingRange = 8; + static int swingOffset = 0; + static bool swingRight = true; + static unsigned long lastSwingTime = 0; + unsigned long now = millis(); + int swingSpeedMs = 10 / (swingRange * 2); + if (now - lastSwingTime >= (unsigned long)swingSpeedMs) { + lastSwingTime = now; + if (swingRight) { + swingOffset++; + if (swingOffset >= swingRange) + swingRight = false; + } else { + swingOffset--; + if (swingOffset <= 0) + swingRight = true; + } + } + for (int i = 0; i < lineCount; i++) { + bool isTitle = (i == 0); + int globalOptionIndex = (i - 1) + firstOptionToShow; + bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected); + + uint16_t visibleWidth = 64 - hPadding * 2; + if (totalLines > visibleTotalLines) + visibleWidth -= 6; + char lineBuffer[lineLengths[i] + 1]; + strncpy(lineBuffer, lines[i], lineLengths[i]); + lineBuffer[lineLengths[i]] = '\0'; + + if (isTitle) { + if (visibleTotalLines == 1) { + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); + display->setColor(WHITE); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); + } else { + display->setColor(WHITE); + display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); + display->setColor(BLACK); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); + display->setColor(WHITE); + if (needs_bell) { + int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert); + display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert); + } + } + lineY = boxTop + effectiveLineHeight + 1; + } else if (isSelectedOption) { + display->setColor(WHITE); + display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); + display->setColor(BLACK); + if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) { + int textX = boxLeft + hPadding + swingOffset; + display->drawString(textX, lineY - 1, lineBuffer); + } else { + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer); + } + display->setColor(WHITE); + lineY += effectiveLineHeight; + } else { + display->setColor(BLACK); + display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); + display->setColor(WHITE); + display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer); + lineY += effectiveLineHeight; + } + } + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); + } +#else if (needs_bell) { if (isHighResolution && boxWidth <= 150) boxWidth += 26; @@ -547,6 +676,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } +#endif } /// Draw the last text message we received diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 049722df8..e00b19b2f 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -20,7 +20,7 @@ // External variables extern graphics::Screen *screen; - +static uint32_t lastSwitchTime = 0; namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; @@ -218,7 +218,6 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes // ********************** void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (favoritedNodes.empty()) return; @@ -230,8 +229,15 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; - + uint32_t now = millis(); display->clear(); +#if defined(M5STACK_UNITC6L) + if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 + { + display->display(); + lastSwitchTime = now; + } +#endif currentFavoriteNodeNum = node->num; // === Create the shortName and title string === const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; @@ -250,9 +256,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st // List of available macro Y positions in order, from top to bottom. int line = 1; // which slot to use next std::string usernameStr; - // === 1. Long Name (always try to show first) === +#if defined(M5STACK_UNITC6L) + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; +#else const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; +#endif + if (username) { usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case // Print node's long name (e.g. "Backpack Node") @@ -307,7 +317,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (seenStr[0] && line < 5) { display->drawString(x, getTextPositions(display)[line++], seenStr); } - +#if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { @@ -479,6 +489,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st } // else show nothing } +#endif } // **************************** @@ -492,7 +503,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int line = 1; // === Header === +#if defined(M5STACK_UNITC6L) + graphics::drawCommonHeader(display, x, y, "Home"); +#else graphics::drawCommonHeader(display, x, y, ""); +#endif // === Content below header === @@ -507,20 +522,25 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta config.display.heading_bold = false; // Display Region and Channel Utilization +#if defined(M5STACK_UNITC6L) + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); +#else drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); - +#endif char uptimeStr[32] = ""; uint32_t uptime = millis() / 1000; uint32_t days = uptime / 86400; uint32_t hours = (uptime % 86400) / 3600; uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" +#if !defined(M5STACK_UNITC6L) if (days) snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); else if (hours) snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); else snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); +#endif display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); // === Second Row: Satellites and Voltage === @@ -549,6 +569,21 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } #endif +#if defined(M5STACK_UNITC6L) + line += 1; + + // === Node Identity === + int textWidth = 0; + int nameX = 0; + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); +#else if (powerStatus->getHasBattery()) { char batStr[20]; int batV = powerStatus->getBatteryVoltageMv() / 1000; @@ -674,6 +709,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], shortnameble); } +#endif } // Start Functions to write date/time to the screen @@ -832,6 +868,28 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED // needs to be drawn relative to x and y // draw centered icon left to right and centered above the one line of app text +#if defined(M5STACK_UNITC6L) + display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // Draw region in upper left + if (upperMsg) { + int msgWidth = display->getStringWidth(upperMsg); + int msgX = x + (SCREEN_WIDTH - msgWidth) / 2; + int msgY = y; + display->drawString(msgX, msgY, upperMsg); + } + // Draw version and short name in bottom middle + char buf[25]; + snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +#else display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, icon_width, icon_height, icon_bits); @@ -840,7 +898,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED const char *title = "meshtastic.org"; display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); - // Draw region in upper left if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); @@ -855,6 +912,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +#endif } // **************************** @@ -930,15 +988,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); char fullLine[40]; snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); +#if !defined(M5STACK_UNITC6L) display->drawString(0, getTextPositions(display)[line++], fullLine); +#endif // === Third Row: Latitude === char latStr[32]; +#if defined(M5STACK_UNITC6L) + snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++] + 2, latStr); +#else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); +#endif // === Fourth Row: Longitude === char lonStr[32]; +#if defined(M5STACK_UNITC6L) + snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); +#else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); @@ -950,8 +1019,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); +#endif } - +#if !defined(M5STACK_UNITC6L) // === Draw Compass if heading is valid === if (validHeading) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- @@ -1034,6 +1104,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } } #endif +#endif } #ifdef USERPREFS_OEM_TEXT @@ -1190,7 +1261,6 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(WHITE); } } - // Knock the corners off the square display->setColor(BLACK); display->drawRect(rectX, y - 2, 1, 1); diff --git a/src/graphics/images.h b/src/graphics/images.h index c66e4b992..4a58edb3b 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -287,6 +287,9 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101 #define analog_icon_clock_height 8 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; - +#ifdef M5STACK_UNITC6L +#include "img/icon_small.xbm" +#else #include "img/icon.xbm" +#endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/graphics/img/icon_small.xbm b/src/graphics/img/icon_small.xbm new file mode 100644 index 000000000..e320a1fea --- /dev/null +++ b/src/graphics/img/icon_small.xbm @@ -0,0 +1,30 @@ +#ifndef USERPREFS_HAS_SPLASH +#define icon_width 50 +#define icon_height 20 +static uint8_t icon_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x80, + 0x07, 0xc0, 0x07, 0x00, 0x00, 0x00, 0xc0, 0x0f, + 0xc0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xe0, + 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xf0, 0x1f, + 0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x3f, 0x00, + 0x00, 0x00, 0xf8, 0x03, 0xf8, 0x7f, 0x00, 0x00, + 0x00, 0xf8, 0x01, 0xfc, 0x7e, 0x00, 0x00, 0x00, + 0xfc, 0x00, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0xfe, + 0x00, 0x7e, 0xf8, 0x00, 0x00, 0x00, 0x7e, 0x00, + 0x3f, 0xf8, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x1f, + 0xf0, 0x01, 0x00, 0x00, 0x1f, 0x80, 0x1f, 0xe0, + 0x03, 0x00, 0x80, 0x1f, 0xc0, 0x0f, 0xe0, 0x03, + 0x00, 0x80, 0x0f, 0xc0, 0x07, 0xc0, 0x07, 0x00, + 0xc0, 0x0f, 0xe0, 0x07, 0x80, 0x0f, 0x00, 0xe0, + 0x07, 0xf0, 0x03, 0x80, 0x1f, 0x00, 0xe0, 0x03, + 0xf8, 0x03, 0x00, 0x1f, 0x00, 0xf0, 0x03, 0xf8, + 0x01, 0x00, 0x3f, 0x00, 0xf8, 0x01, 0xfc, 0x00, + 0x00, 0x7e, 0x00, 0xfc, 0x00, 0xfe, 0x00, 0x00, + 0x7e, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x00, 0xfc, + 0x00, 0x7e, 0x00, 0x3f, 0x00, 0x00, 0xf8, 0x00, + 0x7e, 0x00, 0x3e, 0x00, 0x00, 0xf8, 0x00, 0x38, + 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; +#endif \ No newline at end of file diff --git a/src/input/i2cButton.cpp b/src/input/i2cButton.cpp new file mode 100644 index 000000000..d874146cd --- /dev/null +++ b/src/input/i2cButton.cpp @@ -0,0 +1,95 @@ +#include "i2cButton.h" +#include "meshUtils.h" + +#include "configuration.h" +#if defined(M5STACK_UNITC6L) + +#include "MeshService.h" +#include "RadioLibInterface.h" +#include "buzz.h" +#include "input/InputBroker.h" +#include "main.h" +#include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" +#include "power.h" +#include "sleep.h" +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +i2cButtonThread *i2cButton; + +using namespace concurrency; + +extern void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value); + +extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value); + +#define PI4IO_M_ADDR 0x43 +#define getbit(x, y) ((x) >> (y)&0x01) +#define PI4IO_REG_IRQ_STA 0x13 +#define PI4IO_REG_IN_STA 0x0F +#define PI4IO_REG_CHIP_RESET 0x01 + +i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name) +{ + _originName = name; + if (inputBroker) + inputBroker->registerSource(this); +} + +int32_t i2cButtonThread::runOnce() +{ + static bool btn1_pressed = false; + static uint32_t press_start_time = 0; + const uint32_t LONG_PRESS_TIME = 1000; + static bool long_press_triggered = false; + + uint8_t in_data; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data); + if (getbit(in_data, 0)) { + uint8_t input_state; + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state); + + if (!getbit(input_state, 0)) { + if (!btn1_pressed) { + btn1_pressed = true; + press_start_time = millis(); + long_press_triggered = false; + } + } else { + if (btn1_pressed) { + btn1_pressed = false; + uint32_t press_duration = millis() - press_start_time; + if (long_press_triggered) { + long_press_triggered = false; + return 50; + } + + if (press_duration < LONG_PRESS_TIME) { + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_USER_PRESS; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + } + } + } + + if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) { + long_press_triggered = true; + InputEvent evt; + evt.source = "UserButton"; + evt.inputEvent = INPUT_BROKER_SELECT; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + this->notifyObservers(&evt); + } + return 50; +} +#endif \ No newline at end of file diff --git a/src/input/i2cButton.h b/src/input/i2cButton.h new file mode 100644 index 000000000..1ad908606 --- /dev/null +++ b/src/input/i2cButton.h @@ -0,0 +1,18 @@ +#pragma once + +#include "InputBroker.h" +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" +#if defined(M5STACK_UNITC6L) + +class i2cButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + explicit i2cButtonThread(const char *name); + int32_t runOnce() override; +}; + +extern i2cButtonThread *i2cButton; +#endif diff --git a/src/main.cpp b/src/main.cpp index 401ea7592..d7e866a2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -385,7 +385,6 @@ void setup() io.digitalWrite(EXPANDS_GPIO_EN, HIGH); io.pinMode(EXPANDS_SD_PULLEN, INPUT); #endif - concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0); @@ -544,6 +543,12 @@ void setup() #endif #endif +#if defined(M5STACK_UNITC6L) + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, 1); + c6l_init(); +#endif + #ifdef PIN_LCD_RESET // FIXME - move this someplace better, LCD is at address 0x3F pinMode(PIN_LCD_RESET, OUTPUT); @@ -877,7 +882,8 @@ void setup() if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_SPISSD1306) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && @@ -1140,7 +1146,8 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(USE_SPISSD1306) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index c8eba1b2e..6473722d7 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index e9165e57c..2fc0bf4a6 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1432,10 +1432,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node) { if (node->is_favorite) { +#if defined(M5STACK_UNITC6L) + snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name); + } else { + snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); + } +#else snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); } else { snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); } +#endif } } } @@ -1606,7 +1613,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int yOffset = y + 10; #else display->setFont(FONT_MEDIUM); +#if defined(M5STACK_UNITC6L) + int yOffset = y; +#else int yOffset = y + 10; +#endif #endif // --- Delivery Status Message --- @@ -1631,13 +1642,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } display->drawString(display->getWidth() / 2 + x, yOffset, buffer); +#if defined(M5STACK_UNITC6L) + yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding +#else yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding - +#endif #ifndef USE_EINK // --- SNR + RSSI Compact Line --- if (this->ack) { display->setFont(FONT_SMALL); +#if defined(M5STACK_UNITC6L) + snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi); +#else snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); +#endif display->drawString(display->getWidth() / 2 + x, yOffset, buffer); } #endif diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index d4beb6824..757753d45 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -7,6 +7,7 @@ #include "input/RotaryEncoderInterruptImpl1.h" #include "input/SerialKeyboardImpl.h" #include "input/UpDownInterruptImpl1.h" +#include "input/i2cButton.h" #include "modules/SystemCommandsModule.h" #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" @@ -196,6 +197,9 @@ void setupModules() #endif cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); +#if defined(M5STACK_UNITC6L) + i2cButton = new i2cButtonThread("i2cButtonThread"); +#endif #ifdef INPUTBROKER_MATRIX_TYPE kbMatrixImpl = new KbMatrixImpl(); kbMatrixImpl->init(); diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index ee95168c3..0eb8e9bdd 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -11,6 +11,12 @@ #include #include +#ifdef NIMBLE_TWO +#include "NimBLEAdvertising.h" +#include "NimBLEExtAdvertising.h" +#include "PowerStatus.h" +#endif + NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *BatteryCharacteristic; NimBLECharacteristic *logRadioCharacteristic; @@ -56,13 +62,18 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { PhoneAPI::onNowHasData(fromRadioNum); - LOG_DEBUG("BLE notify fromNum"); + uint8_t cc = bleServer->getConnectedCount(); + LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc); uint8_t val[4]; put_le32(val, fromRadioNum); fromNumCharacteristic->setValue(val, sizeof(val)); +#ifdef NIMBLE_TWO + fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); +#else fromNumCharacteristic->notify(); +#endif } /// Check the current underlying physical link to see if the client is currently connected @@ -79,7 +90,12 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks { +#ifdef NIMBLE_TWO + virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) +#else virtual void onWrite(NimBLECharacteristic *pCharacteristic) + +#endif { auto val = pCharacteristic->getValue(); @@ -97,7 +113,11 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { +#ifdef NIMBLE_TWO + virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) +#else virtual void onRead(NimBLECharacteristic *pCharacteristic) +#endif { int tries = 0; bluetoothPhoneAPI->phoneWants = true; @@ -107,9 +127,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks tries++; } std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); - std::string fromRadioByteString(bluetoothPhoneAPI->fromRadioBytes, - bluetoothPhoneAPI->fromRadioBytes + bluetoothPhoneAPI->numBytes); - pCharacteristic->setValue(fromRadioByteString); + pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes); if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload bluetoothPhoneAPI->setIntervalFromNow(0); @@ -121,7 +139,17 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { +#ifdef NIMBLE_TWO + public: + NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; } + + private: + NimbleBluetooth *ble; + + virtual uint32_t onPassKeyDisplay() +#else virtual uint32_t onPassKeyRequest() +#endif { uint32_t passkey = config.bluetooth.fixed_pin; @@ -170,7 +198,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks return passkey; } +#ifdef NIMBLE_TWO + virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) +#else virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) +#endif { LOG_INFO("BLE authentication complete"); @@ -185,9 +217,20 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks } } +#ifdef NIMBLE_TWO + virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) + { + LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); + } + + virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) + { + LOG_INFO("BLE disconnect reason: %d", reason); +#else virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) { LOG_INFO("BLE disconnect"); +#endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); @@ -200,6 +243,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothPhoneAPI->numBytes = 0; bluetoothPhoneAPI->queue_size = 0; } +#ifdef NIMBLE_TWO + // Restart Advertising + ble->startAdvertising(); +#endif } }; @@ -251,7 +298,11 @@ int NimbleBluetooth::getRssi() if (bleServer && isConnected()) { auto service = bleServer->getServiceByUUID(MESH_SERVICE_UUID); uint16_t handle = service->getHandle(); +#ifdef NIMBLE_TWO + return NimBLEDevice::getClientByHandle(handle)->getRssi(); +#else return NimBLEDevice::getClientByID(handle)->getRssi(); +#endif } return 0; // FIXME figure out where to source this } @@ -273,8 +324,11 @@ void NimbleBluetooth::setup() NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); } bleServer = NimBLEDevice::createServer(); - +#ifdef NIMBLE_TWO + NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this); +#else NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(); +#endif bleServer->setCallbacks(serverCallbacks, true); setupService(); startAdvertising(); @@ -318,8 +372,11 @@ void NimbleBluetooth::setupService() NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) (uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1); - +#ifdef NIMBLE_TWO + NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904(); +#else NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904); +#endif batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8); batteryLevelDescriptor->setNamespace(1); batteryLevelDescriptor->setUnit(0x27ad); @@ -329,11 +386,40 @@ void NimbleBluetooth::setupService() void NimbleBluetooth::startAdvertising() { +#ifdef NIMBLE_TWO + NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + NimBLEExtAdvertisement legacyAdvertising; + + legacyAdvertising.setLegacyAdvertising(true); + legacyAdvertising.setScannable(true); + legacyAdvertising.setConnectable(true); + legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN); + if (powerStatus->getHasBattery() == 1) { + legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f)); + } + legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID)); + legacyAdvertising.setMinInterval(500); + legacyAdvertising.setMaxInterval(1000); + + NimBLEExtAdvertisement legacyScanResponse; + legacyScanResponse.setLegacyAdvertising(true); + legacyScanResponse.setConnectable(true); + legacyScanResponse.setName(getDeviceName()); + + if (!pAdvertising->setInstanceData(0, legacyAdvertising)) { + LOG_ERROR("BLE failed to set legacyAdvertising"); + } else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) { + LOG_ERROR("BLE failed to set legacyScanResponse"); + } else if (!pAdvertising->start(0, 0, 0)) { + LOG_ERROR("BLE failed to start legacyAdvertising"); + } +#else NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->reset(); pAdvertising->addServiceUUID(MESH_SERVICE_UUID); pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service pAdvertising->start(0); +#endif } /// Given a level between 0-100, update the BLE attribute @@ -341,7 +427,11 @@ void updateBatteryLevel(uint8_t level) { if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) { BatteryCharacteristic->setValue(&level, 1); +#ifdef NIMBLE_TWO + BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE); +#else BatteryCharacteristic->notify(); +#endif } } @@ -356,7 +446,11 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length) if (!bleServer || !isConnected() || length > 512) { return; } +#ifdef NIMBLE_TWO + logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE); +#else logRadioCharacteristic->notify(logMessage, length, true); +#endif } void clearNVS() @@ -366,4 +460,4 @@ void clearNVS() ESP.restart(); #endif } -#endif +#endif \ No newline at end of file diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index 45602e088..899355b4d 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -12,10 +12,15 @@ class NimbleBluetooth : BluetoothApi bool isConnected(); int getRssi(); void sendLog(const uint8_t *logMessage, size_t length); +#if defined(NIMBLE_TWO) + void startAdvertising(); +#endif private: void setupService(); +#if !defined(NIMBLE_TWO) void startAdvertising(); +#endif }; void setBluetoothEnable(bool enable); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index cb0f0dab3..22ce6487f 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -194,6 +194,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER +#elif defined(M5STACK_UNITC6L) +#define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L #endif // ----------------------------------------------------------------------------- diff --git a/variants/esp32c6/m5stack_unitc6l/pins_arduino.h b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h new file mode 100644 index 000000000..5b169a2d4 --- /dev/null +++ b/variants/esp32c6/m5stack_unitc6l/pins_arduino.h @@ -0,0 +1,28 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x2886 +#define USB_PID 0x0048 + +static const uint8_t TX = 16; +static const uint8_t RX = 17; + +static const uint8_t SDA = 10; +static const uint8_t SCL = 8; + +// Default SPI will be mapped to Radio +static const uint8_t MISO = 22; +static const uint8_t SCK = 20; +static const uint8_t MOSI = 21; +static const uint8_t SS = 6; + +// #define SPI_MOSI (11) +// #define SPI_SCK (14) +// #define SPI_MISO (2) +// #define SPI_CS (13) + +// #define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini new file mode 100644 index 000000000..da1c70c0a --- /dev/null +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -0,0 +1,35 @@ +[env:m5stack-unitc6l] +extends = esp32c6_base +board = esp32-c6-devkitc-1 +;OpenOCD flash method +;upload_protocol = esp-builtin +;Normal method +upload_protocol = esptool +;upload_port = /dev/ttyACM2 +build_unflags = + -D HAS_BLUETOOTH + -D MESHTASTIC_EXCLUDE_BLUETOOTH + -D HAS_WIFI +lib_deps = + ${esp32c6_base.lib_deps} + adafruit/Adafruit NeoPixel@^1.12.3 + h2zero/NimBLE-Arduino@^2.3.6 +build_flags = + ${esp32c6_base.build_flags} + -D M5STACK_UNITC6L + -I variants/esp32c6/m5stack_unitc6l + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -D HAS_BLUETOOTH=1 + -D MESHTASTIC_EXCLUDE_WEBSERVER + -D MESHTASTIC_EXCLUDE_MQTT + -DCONFIG_BT_NIMBLE_EXT_ADV=1 + -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 + -D NIMBLE_TWO +monitor_speed=115200 +lib_ignore = + NonBlockingRTTTL + libpax +build_src_filter = + ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> \ No newline at end of file diff --git a/variants/esp32c6/m5stack_unitc6l/variant.cpp b/variants/esp32c6/m5stack_unitc6l/variant.cpp new file mode 100644 index 000000000..8e26b4ab7 --- /dev/null +++ b/variants/esp32c6/m5stack_unitc6l/variant.cpp @@ -0,0 +1,74 @@ +#include "driver/gpio.h" +#include +#include +// I2C device addr +#define PI4IO_M_ADDR 0x43 + +// PI4IO registers +#define PI4IO_REG_CHIP_RESET 0x01 +#define PI4IO_REG_IO_DIR 0x03 +#define PI4IO_REG_OUT_SET 0x05 +#define PI4IO_REG_OUT_H_IM 0x07 +#define PI4IO_REG_IN_DEF_STA 0x09 +#define PI4IO_REG_PULL_EN 0x0B +#define PI4IO_REG_PULL_SEL 0x0D +#define PI4IO_REG_IN_STA 0x0F +#define PI4IO_REG_INT_MASK 0x11 +#define PI4IO_REG_IRQ_STA 0x13 +// PI4IO + +#define setbit(x, y) x |= (0x01 << y) +#define clrbit(x, y) x &= ~(0x01 << y) +#define reversebit(x, y) x ^= (0x01 << y) +#define getbit(x, y) ((x) >> (y)&0x01) + +void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) +{ + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom(addr, 1); + *value = Wire.read(); +} + +/*******************************************************************/ +void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) +{ + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + Wire.endTransmission(); +} +/*******************************************************************/ +void c6l_init() +{ + // P7 LoRa Reset + // P6 RF Switch + // P5 LNA Enable + + printf("pi4io_init\n"); + uint8_t in_data; + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_CHIP_RESET, &in_data); + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IO_DIR, 0b11000000); // 0: input 1: output + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_H_IM, 0b00111100); // 使用到的引脚关闭High-Impedance + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_SEL, 0b11000011); // pull up/down select, 0 down, 1 up + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_PULL_EN, 0b11000011); // pull up/down enable, 0 disable, 1 enable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); // P0 P1 默认高电平, 按键按下触发中断 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_INT_MASK, 0b11111100); // P0 P1 中断使能 0 enable, 1 disable + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, 0b10000000); // 默认输出为0 + vTaskDelay(10 / portTICK_PERIOD_MS); + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data); // 读取IRQ_STA清除标志 + + i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, &in_data); + setbit(in_data, 6); // HIGH + i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_OUT_SET, in_data); +} diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h new file mode 100644 index 000000000..d973aa281 --- /dev/null +++ b/variants/esp32c6/m5stack_unitc6l/variant.h @@ -0,0 +1,52 @@ +void c6l_init(); + +#define HAS_GPS 1 +#define GPS_RX_PIN 4 +#define GPS_TX_PIN 5 + +#define I2C_SDA 10 +#define I2C_SCL 8 + +#define PIN_BUZZER 11 + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA 2 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use +#define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting + +// #define BUTTON_PIN 9 +#define BUTTON_EXTENDER + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +// WaveShare Core1262-868M OK +// https://www.waveshare.com/wiki/Core1262-868M +#define USE_SX1262 + +#define LORA_MISO 22 +#define LORA_SCK 20 +#define LORA_MOSI 21 +#define LORA_CS 23 +#define LORA_RESET RADIOLIB_NC +#define LORA_DIO1 7 +#define LORA_BUSY 19 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 + +#define USE_SPISSD1306 +#ifdef USE_SPISSD1306 +#define SSD1306_NSS 6 // CS +#define SSD1306_RS 18 // DC +#define SSD1306_RESET 15 +// #define OLED_DG 1 +#endif +#define SCREEN_TRANSITION_FRAMERATE 10 +#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness From 6a8732bbaa0e7a6b741bc8874197229acc4bf73d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:51:02 -0500 Subject: [PATCH 17/62] Update Adafruit BusIO to v1.17.3 (#8018) 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 e2eb55dce..42453a33e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -124,7 +124,7 @@ lib_deps = [environmental_base] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO - adafruit/Adafruit BusIO@1.17.2 + adafruit/Adafruit BusIO@1.17.3 # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor adafruit/Adafruit Unified Sensor@1.1.15 # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library From a70ffae82ca53c7e10395e85ae23a641ddc46d16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:51:22 -0500 Subject: [PATCH 18/62] Update actions/checkout action to v5 (#8020) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/merge_queue.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index e2264e250..3243d5867 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -27,7 +27,7 @@ jobs: - check runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -57,7 +57,7 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name != 'workflow_dispatch' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build base id: base uses: ./.github/actions/setup-base @@ -274,7 +274,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -353,7 +353,7 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -422,7 +422,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -477,7 +477,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 From ec7415b3fd8ab8b44c3b2ee642045da3d0314870 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:55:24 -0500 Subject: [PATCH 19/62] Update actions/setup-python action to v6 (#8023) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/merge_queue.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 3243d5867..472ec2a9e 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -356,7 +356,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -425,7 +425,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -480,7 +480,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x From 953fdc9eedfc5b8042df6e23ae5fd9300e86c7f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:55:44 -0500 Subject: [PATCH 20/62] Upgrade trunk (#8025) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index c1fde9602..f23c41810 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,7 +9,7 @@ plugins: lint: enabled: - checkov@3.2.471 - - renovate@41.115.2 + - renovate@41.115.6 - prettier@3.6.2 - trufflehog@3.90.6 - yamllint@1.37.1 From 188283b38290b94dccafd3c1bf753961ac658895 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 05:56:05 -0500 Subject: [PATCH 21/62] Update actions/download-artifact action to v5 (#8021) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/merge_queue.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 472ec2a9e..7f397ce18 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -279,7 +279,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -308,7 +308,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -372,14 +372,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -429,7 +429,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -446,7 +446,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -484,7 +484,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true From d8381aa905dbe48daa3569fecde137547c9664eb Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:32:56 +0200 Subject: [PATCH 22/62] Fix init for InputEvent (#8015) --- src/input/ExpressLRSFiveWay.cpp | 2 +- src/input/LinuxInput.cpp | 2 +- src/input/RotaryEncoderInterruptBase.cpp | 2 +- src/input/SeesawRotary.cpp | 2 +- src/input/SerialKeyboard.cpp | 4 ++-- src/input/TouchScreenImpl1.cpp | 2 +- src/input/TrackballInterruptBase.cpp | 2 +- src/input/UpDownInterruptBase.cpp | 2 +- src/input/kbI2cBase.cpp | 10 +++++----- src/input/kbMatrixBase.cpp | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 776b9001d..01712ad2a 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -188,7 +188,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) // Feed input to the canned messages module void ExpressLRSFiveWay::sendKey(input_broker_event key) { - InputEvent e; + InputEvent e = {}; e.source = inputSourceName; e.inputEvent = key; notifyObservers(&e); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 1f80fd5d3..fee7c8ded 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -73,7 +73,7 @@ int32_t LinuxInput::runOnce() int rd = read(events[i].data.fd, ev, sizeof(ev)); assert(rd > ((signed int)sizeof(struct input_event))); for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; e.kbchar = 0; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 204a0fbf0..016727cd7 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -45,7 +45,7 @@ void RotaryEncoderInterruptBase::init( int32_t RotaryEncoderInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; unsigned long now = millis(); diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index c212773c4..0a6e6e974 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -49,7 +49,7 @@ bool SeesawRotary::init() int32_t SeesawRotary::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; bool currentlyPressed = !ss.digitalRead(SS_SWITCH); diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 63501bda5..2df1ace70 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -29,7 +29,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) void SerialKeyboard::erase() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; e.source = this->_originName; @@ -80,7 +80,7 @@ int32_t SerialKeyboard::runOnce() if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but // shouldn't be a limitation - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index c0e220941..69dcab04e 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -47,7 +47,7 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) */ void TouchScreenImpl1::onEvent(const TouchEvent &event) { - InputEvent e; + InputEvent e = {}; e.source = event.source; e.kbchar = 0; e.touchX = event.x; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index 4c8ce6409..4ddaf7064 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -51,7 +51,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef int32_t TrackballInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; // Handle long press detection for press button diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 0bf0f5cd4..d597c8d8f 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -48,7 +48,7 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, int32_t UpDownInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; unsigned long now = millis(); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 5db1e39a9..3f5d4d184 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -90,7 +90,7 @@ int32_t KbI2cBase::runOnce() while (keyCount--) { const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key.key) { @@ -187,7 +187,7 @@ int32_t KbI2cBase::runOnce() } case 0x37: { // MPR121 MPRkeyboard.trigger(); - InputEvent e; + InputEvent e = {}; while (MPRkeyboard.hasEvent()) { char nextEvent = MPRkeyboard.dequeueEvent(); @@ -250,7 +250,7 @@ int32_t KbI2cBase::runOnce() } case 0x84: { // Adafruit TCA8418 TCAKeyboard.trigger(); - InputEvent e; + InputEvent e = {}; while (TCAKeyboard.hasEvent()) { char nextEvent = TCAKeyboard.dequeueEvent(); e.inputEvent = INPUT_BROKER_ANYKEY; @@ -350,7 +350,7 @@ int32_t KbI2cBase::runOnce() } if (PrintDataBuf != 0) { LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_MATRIXKEY; e.source = this->_originName; e.kbchar = PrintDataBuf; @@ -365,7 +365,7 @@ int32_t KbI2cBase::runOnce() if (i2cBus->available()) { char c = i2cBus->read(); - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (c) { diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index 05f4d8177..18243f3bf 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -72,7 +72,7 @@ int32_t KbMatrixBase::runOnce() if (key != 0) { LOG_DEBUG("Key 0x%x pressed", key); // reset shift now that we have a keypress - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key) { From 2567c03a3fbd30a7a1d3223328820dd60c8861fe Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:32:56 +0200 Subject: [PATCH 23/62] Fix init for InputEvent (#8015) --- src/input/ExpressLRSFiveWay.cpp | 2 +- src/input/LinuxInput.cpp | 2 +- src/input/RotaryEncoderInterruptBase.cpp | 2 +- src/input/SeesawRotary.cpp | 2 +- src/input/SerialKeyboard.cpp | 4 ++-- src/input/TouchScreenImpl1.cpp | 2 +- src/input/TrackballInterruptBase.cpp | 2 +- src/input/UpDownInterruptBase.cpp | 2 +- src/input/kbI2cBase.cpp | 10 +++++----- src/input/kbMatrixBase.cpp | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 776b9001d..01712ad2a 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -188,7 +188,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) // Feed input to the canned messages module void ExpressLRSFiveWay::sendKey(input_broker_event key) { - InputEvent e; + InputEvent e = {}; e.source = inputSourceName; e.inputEvent = key; notifyObservers(&e); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 90f06ecc9..7620080f0 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -73,7 +73,7 @@ int32_t LinuxInput::runOnce() int rd = read(events[i].data.fd, ev, sizeof(ev)); assert(rd > ((signed int)sizeof(struct input_event))); for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; e.kbchar = 0; diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 88b07a389..3511a990a 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -43,7 +43,7 @@ void RotaryEncoderInterruptBase::init( int32_t RotaryEncoderInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index c212773c4..0a6e6e974 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -49,7 +49,7 @@ bool SeesawRotary::init() int32_t SeesawRotary::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; bool currentlyPressed = !ss.digitalRead(SS_SWITCH); diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 63501bda5..2df1ace70 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -29,7 +29,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) void SerialKeyboard::erase() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; e.source = this->_originName; @@ -80,7 +80,7 @@ int32_t SerialKeyboard::runOnce() if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but // shouldn't be a limitation - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index cea47faeb..50a70d66e 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -47,7 +47,7 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) */ void TouchScreenImpl1::onEvent(const TouchEvent &event) { - InputEvent e; + InputEvent e = {}; e.source = event.source; e.kbchar = 0; e.touchX = event.x; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index d41ad2fd6..a11628e1d 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -48,7 +48,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef int32_t TrackballInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball if (this->action == TB_ACTION_PRESSED) { diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 26b281aaf..87b34b92f 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -40,7 +40,7 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, int32_t UpDownInterruptBase::runOnce() { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; unsigned long now = millis(); if (this->action == UPDOWN_ACTION_PRESSED) { diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 5db1e39a9..3f5d4d184 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -90,7 +90,7 @@ int32_t KbI2cBase::runOnce() while (keyCount--) { const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key.key) { @@ -187,7 +187,7 @@ int32_t KbI2cBase::runOnce() } case 0x37: { // MPR121 MPRkeyboard.trigger(); - InputEvent e; + InputEvent e = {}; while (MPRkeyboard.hasEvent()) { char nextEvent = MPRkeyboard.dequeueEvent(); @@ -250,7 +250,7 @@ int32_t KbI2cBase::runOnce() } case 0x84: { // Adafruit TCA8418 TCAKeyboard.trigger(); - InputEvent e; + InputEvent e = {}; while (TCAKeyboard.hasEvent()) { char nextEvent = TCAKeyboard.dequeueEvent(); e.inputEvent = INPUT_BROKER_ANYKEY; @@ -350,7 +350,7 @@ int32_t KbI2cBase::runOnce() } if (PrintDataBuf != 0) { LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_MATRIXKEY; e.source = this->_originName; e.kbchar = PrintDataBuf; @@ -365,7 +365,7 @@ int32_t KbI2cBase::runOnce() if (i2cBus->available()) { char c = i2cBus->read(); - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (c) { diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index 05f4d8177..18243f3bf 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -72,7 +72,7 @@ int32_t KbMatrixBase::runOnce() if (key != 0) { LOG_DEBUG("Key 0x%x pressed", key); // reset shift now that we have a keypress - InputEvent e; + InputEvent e = {}; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key) { From c71c1f2d15e65aa883b8ac954bafb89d6c5cd74f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:17:14 -0500 Subject: [PATCH 24/62] Automated version bumps (#8028) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 + debian/changelog | 150 ++++++-------------- version.properties | 2 +- 3 files changed, 49 insertions(+), 106 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 108ca4910..090c141fa 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 diff --git a/debian/changelog b/debian/changelog index 286349dd2..76e390001 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,109 +1,49 @@ -meshtasticd (2.7.9.0) unstable; urgency=medium - - * Version 2.7.9 - - -- GitHub Actions Wed, 03 Sep 2025 23:39:17 +0000 - -meshtasticd (2.7.8.0) unstable; urgency=medium - - * Version 2.7.8 - - -- GitHub Actions Sat, 30 Aug 2025 00:26:04 +0000 - -meshtasticd (2.7.7.0) unstable; urgency=medium - - * Version 2.7.7 - - -- GitHub Actions Thu, 28 Aug 2025 10:33:25 +0000 - -meshtasticd (2.7.6.0) unstable; urgency=medium - - * Version 2.7.6 - - -- GitHub Actions Tue, 12 Aug 2025 23:48:48 +0000 - -meshtasticd (2.7.5.0) unstable; urgency=medium - - * Version 2.7.5 - - -- GitHub Actions Sat, 09 Aug 2025 12:46:53 +0000 - -meshtasticd (2.7.4.0) unstable; urgency=medium - - * Version 2.7.4 - - -- GitHub Actions Sat, 19 Jul 2025 11:36:55 +0000 - -meshtasticd (2.7.3.0) unstable; urgency=medium - - * Version 2.7.3 - - -- GitHub Actions Thu, 10 Jul 2025 16:29:27 +0000 - -meshtasticd (2.7.2.0) unstable; urgency=medium - - * Version 2.7.2 - - -- GitHub Actions Fri, 04 Jul 2025 11:58:01 +0000 - -meshtasticd (2.7.1.0) unstable; urgency=medium - - * Version 2.7.1 - - -- GitHub Actions Fri, 27 Jun 2025 20:12:21 +0000 - -meshtasticd (2.6.13) unstable; urgency=medium - - * Version 2.6.13 - - -- GitHub Actions Mon, 16 Jun 2025 02:10:49 +0000 - -meshtasticd (2.6.11) unstable; urgency=medium - - * Version 2.6.11 - - -- GitHub Actions Mon, 02 Jun 2025 20:00:55 +0000 - -meshtasticd (2.6.10) unstable; urgency=medium - - * Version 2.6.10 - - -- GitHub Actions Sun, 25 May 2025 20:46:49 +0000 - -meshtasticd (2.6.9) unstable; urgency=medium - - * Version 2.6.9 - * Run as non-root user, 'meshtasticd' - - -- GitHub Actions Thu, 15 May 2025 11:13:30 +0000 - -meshtasticd (2.6.8) unstable; urgency=medium - - * Version 2.6.8 - - -- GitHub Actions Tue, 06 May 2025 01:32:49 +0000 - -meshtasticd (2.5.22) unstable; urgency=medium - - * Version 2.5.22 - - -- GitHub Actions Wed, 05 Feb 2025 01:10:33 +0000 - -meshtasticd (2.5.21) unstable; urgency=medium - - * Version 2.5.21 - - -- GitHub Actions Sat, 25 Jan 2025 01:39:16 +0000 - -meshtasticd (2.5.20) unstable; urgency=medium - - * Version 2.5.20 - - -- GitHub Actions Mon, 13 Jan 2025 19:24:14 +0000 - -meshtasticd (2.5.19) unstable; urgency=medium +meshtasticd (2.7.10.0) UNRELEASED; urgency=medium * Initial packaging * Version 2.5.19 - -- Austin Lane Thu, 02 Jan 2025 12:00:00 +0000 + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ Ubuntu ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + -- Thu, 18 Sep 2025 22:11:37 +0000 diff --git a/version.properties b/version.properties index cbf8265d9..cc0449a72 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 9 +build = 10 From e3772858b319199bd8f0e0a82a8c6f27342e7024 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:17:14 -0500 Subject: [PATCH 25/62] Automated version bumps (#8028) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 ++ debian/changelog | 50 ++------------------- version.properties | 2 +- 3 files changed, 7 insertions(+), 48 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 108ca4910..090c141fa 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9 diff --git a/debian/changelog b/debian/changelog index 29841d0db..d4ba6b7a0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,50 +1,6 @@ -meshtasticd (2.7.9.0) UNRELEASED; urgency=medium +meshtasticd (2.7.10.0) UNRELEASED; urgency=medium - [ Austin Lane ] * Initial packaging - * GitHub Actions Automatic version bump - * GitHub Actions Automatic version bump - * GitHub Actions Automatic version bump - * GitHub Actions Automatic version bump + * Version 2.5.19 - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ Ubuntu ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - [ ] - * GitHub Actions Automatic version bump - - -- Wed, 03 Sep 2025 23:39:17 +0000 + -- Austin Lane Thu, 02 Jan 2025 12:00:00 +0000 diff --git a/version.properties b/version.properties index cbf8265d9..cc0449a72 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 9 +build = 10 From 89cccdbbe27f0e4259e1ef3ad5f05db8efcf8cdc Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Fri, 19 Sep 2025 02:25:58 +0200 Subject: [PATCH 26/62] Allow Left / Right Events for selection and improve encoder responsives (#8016) * Allow Left / Right Events for selection and improve encoder responsives * add define for ROTARY_DELAY --- src/configuration.h | 7 +++++++ src/graphics/draw/NotificationRenderer.cpp | 12 ++++++++---- src/input/RotaryEncoderInterruptBase.cpp | 2 +- src/modules/CannedMessageModule.cpp | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index d5adba028..515380cfb 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -262,6 +262,13 @@ along with this program. If not, see . #define VEXT_ON_VALUE LOW #endif +// ----------------------------------------------------------------------------- +// Rotary encoder +// ----------------------------------------------------------------------------- +#ifndef ROTARY_DELAY +#define ROTARY_DELAY 5 +#endif + // ----------------------------------------------------------------------------- // GPS // ----------------------------------------------------------------------------- diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 8c8ea800b..26bfe8447 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -250,9 +250,11 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { alertBannerCallback(selectedNodenum); @@ -365,9 +367,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // Handle input if (alertBannerOptions > 0) { - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { if (optionsEnumPtr != nullptr) { diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 016727cd7..980057ce4 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -151,7 +151,7 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool // Logic to prevent bouncing. newState = ROTARY_EVENT_CLEARED; } - setIntervalFromNow(50); // TODO: this modifies a non-volatile variable! + setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! return newState; } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index ffbc6640e..e9f52bb7d 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -404,14 +404,14 @@ bool CannedMessageModule::isUpEvent(const InputEvent *event) return event->inputEvent == INPUT_BROKER_UP || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - event->inputEvent == INPUT_BROKER_ALT_PRESS); + (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); } bool CannedMessageModule::isDownEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_DOWN || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - event->inputEvent == INPUT_BROKER_USER_PRESS); + (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); } bool CannedMessageModule::isSelectEvent(const InputEvent *event) { From 017d07e1082dbfcf77305169db5e6e306dd9dd42 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 18 Sep 2025 19:28:10 -0500 Subject: [PATCH 27/62] Extra chirpy --- src/graphics/draw/DebugRenderer.cpp | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index aa0e7cf1b..5fe10a1b8 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -693,32 +693,6 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1 display->drawString(textX, getTextPositions(display)[line++], "World!"); } -// **************************** -// * Chirpy Screen * -// **************************** -void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - int line = 1; - int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); - int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; - int textX_offset = 10; - if (isHighResolution) { - iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); - iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; - textX_offset = textX_offset * 4; - display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); - } else { - display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); - } - - int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); - display->drawString(textX, getTextPositions(display)[line++], "Hello"); - textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); - display->drawString(textX, getTextPositions(display)[line++], "World!"); -} } // namespace DebugRenderer } // namespace graphics #endif \ No newline at end of file From ec29100a88915cb33bfa1d9b430bf67e3b1cc786 Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Fri, 19 Sep 2025 02:25:58 +0200 Subject: [PATCH 28/62] Allow Left / Right Events for selection and improve encoder responsives (#8016) * Allow Left / Right Events for selection and improve encoder responsives * add define for ROTARY_DELAY --- src/configuration.h | 7 +++++++ src/graphics/draw/NotificationRenderer.cpp | 12 ++++++++---- src/input/RotaryEncoderInterruptBase.cpp | 2 +- src/modules/CannedMessageModule.cpp | 4 ++-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 81632c89e..1b386ec17 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -262,6 +262,13 @@ along with this program. If not, see . #define VEXT_ON_VALUE LOW #endif +// ----------------------------------------------------------------------------- +// Rotary encoder +// ----------------------------------------------------------------------------- +#ifndef ROTARY_DELAY +#define ROTARY_DELAY 5 +#endif + // ----------------------------------------------------------------------------- // GPS // ----------------------------------------------------------------------------- diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index c2bd1ba66..710609f60 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -216,9 +216,11 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { alertBannerCallback(selectedNodenum); @@ -333,9 +335,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // Handle input if (alertBannerOptions > 0) { - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { curSelected--; - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || + inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { if (optionsEnumPtr != nullptr) { diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 3511a990a..598353f98 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -120,7 +120,7 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool // Logic to prevent bouncing. newState = ROTARY_EVENT_CLEARED; } - setIntervalFromNow(50); // TODO: this modifies a non-volatile variable! + setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable! return newState; } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 2fc0bf4a6..1af1dd7d3 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -394,14 +394,14 @@ bool CannedMessageModule::isUpEvent(const InputEvent *event) return event->inputEvent == INPUT_BROKER_UP || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - event->inputEvent == INPUT_BROKER_ALT_PRESS); + (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS)); } bool CannedMessageModule::isDownEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_DOWN || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && - event->inputEvent == INPUT_BROKER_USER_PRESS); + (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS)); } bool CannedMessageModule::isSelectEvent(const InputEvent *event) { From 902405a985d0d58f317a68f669769149a63df3be Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 18 Sep 2025 19:47:16 -0500 Subject: [PATCH 29/62] Extra endif --- src/graphics/draw/UIRenderer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 4326d3fd4..53ed6e737 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1154,7 +1154,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } } #endif -#endif } #ifdef USERPREFS_OEM_TEXT From 9345bdcb224c4e42dd75470a1631a6e9ca03dddc Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 13 Sep 2025 13:50:02 +0200 Subject: [PATCH 30/62] T-Lora Pager: Support LR1121 and SX1280 models (#7956) * T-Lora Pager: Support LR1121 and SX1280 models * Remove ifdefs --- variants/esp32s3/tlora-pager/rfswitch.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/variants/esp32s3/tlora-pager/rfswitch.h b/variants/esp32s3/tlora-pager/rfswitch.h index 337346ec5..0fba5a305 100644 --- a/variants/esp32s3/tlora-pager/rfswitch.h +++ b/variants/esp32s3/tlora-pager/rfswitch.h @@ -4,12 +4,8 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 static const Module::RfSwitchMode_t rfswitch_table[] = { // mode DIO5 DIO6 - {LR11x0::MODE_STBY, {LOW, LOW}}, - {LR11x0::MODE_RX, {LOW, HIGH}}, - {LR11x0::MODE_TX, {HIGH, LOW}}, - {LR11x0::MODE_TX_HP, {HIGH, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW}}, - {LR11x0::MODE_GNSS, {LOW, LOW}}, - {LR11x0::MODE_WIFI, {LOW, LOW}}, - END_OF_MODE_TABLE, + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH}}, + {LR11x0::MODE_TX, {HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, }; \ No newline at end of file From c73fe85ec8b02ea057f8b104f88afa56f501c01f Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:41:22 +0100 Subject: [PATCH 31/62] (resubmission) Manual GitHub actions to allow building one target or arch (#7997) * Reset the modified files * Fix some changes * Fix some changes * Trunk. That is all. --------- Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 500 +++++++++++++++++++++++++ .github/workflows/build_one_target.yml | 395 +++++++++++++++++++ .github/workflows/daily_packaging.yml | 4 + .github/workflows/main_matrix.yml | 17 +- 4 files changed, 912 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build_one_arch.yml create mode 100644 .github/workflows/build_one_target.yml diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml new file mode 100644 index 000000000..5901a335c --- /dev/null +++ b/.github/workflows/build_one_arch.yml @@ -0,0 +1,500 @@ +name: Build One Arch + +on: + workflow_dispatch: + inputs: + arch: + type: choice + options: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - native + +jobs: + setup: + strategy: + fail-fast: false + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: pip + - run: pip install -U platformio + - name: Generate matrix + id: jsonStep + run: | + if [[ "$GITHUB_HEAD_REF" == "" ]]; then + TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} extra) + else + TARGETS=$(./bin/generate_ci_matrix.py ${{inputs.arch}} pr) + fi + echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS" + echo "${{inputs.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT + outputs: + esp32: ${{ steps.jsonStep.outputs.esp32 }} + esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }} + esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }} + 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 }} + + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT + id: version + env: + BUILD_LOCATION: local + outputs: + long: ${{ steps.version.outputs.long }} + deb: ${{ steps.version.outputs.deb }} + + build-esp32: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32 + + build-esp32s3: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32s3'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32s3 + + build-esp32c3: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c3'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c3 + + build-esp32c6: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'esp32c6'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: esp32c6 + + build-nrf52840: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'nrf52840'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: nrf52840 + + build-rp2040: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'rp2040'}} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: rp2040 + + build-rp2350: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == '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: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'stm32' }} + needs: [setup, version] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.setup.outputs.stm32) }} + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ matrix.board }} + platform: stm32 + + build-debian-src: + if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/build_debian_src.yml + with: + series: UNRELEASED + build_location: local + secrets: inherit + + package-pio-deps-native-tft: + if: ${{ inputs.arch == 'native' }} + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native-tft + secrets: inherit + + test-native: + if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }} + uses: ./.github/workflows/test_native.yml + + docker-deb-amd64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-deb-amd64-tft: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alp-amd64-tft: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-deb-armv7: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false + + gather-artifacts: + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + needs: + [ + version, + build-esp32, + build-esp32s3, + build-esp32c3, + build-esp32c6, + build-nrf52840, + build-rp2040, + build-rp2350, + build-stm32, + ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - uses: actions/download-artifact@v4 + with: + path: ./ + pattern: firmware-${{inputs.arch}}-* + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Move files up + run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat + + - name: Repackage in single firmware zip + uses: actions/upload-artifact@v4 + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + overwrite: true + path: | + ./firmware-*.bin + ./firmware-*.uf2 + ./firmware-*.hex + ./firmware-*-ota.zip + ./device-*.sh + ./device-*.bat + ./littlefs-*.bin + ./bleota*bin + ./Meshtastic_nRF52_factory_erase*.uf2 + retention-days: 30 + + - uses: actions/download-artifact@v4 + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + # For diagnostics + - name: Show artifacts + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - name: Repackage in single elfs zip + uses: actions/upload-artifact@v4 + with: + name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + overwrite: true + path: ./*.elf + retention-days: 30 + + - uses: scruplelesswizard/comment-artifact@main + if: ${{ github.event_name == 'pull_request' }} + with: + name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + release-artifacts: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + needs: + - version + - gather-artifacts + - build-debian-src + - package-pio-deps-native-tft + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Create release + uses: softprops/action-gh-release@v2 + id: create_release + with: + draft: true + prerelease: true + name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha + tag_name: v${{ needs.version.outputs.long }} + body: | + Autogenerated by github action, developer should edit as required before publishing... + + - name: Download source deb + uses: actions/download-artifact@v4 + with: + pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src + merge-multiple: true + path: ./output/debian-src + + - name: Download `native-tft` pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native-tft + + - name: Zip Linux sources + working-directory: output + run: | + zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src + zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip + gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-firmware: + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-artifacts, version] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + - name: Display structure of downloaded files + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output + + - uses: actions/download-artifact@v4 + with: + name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + merge-multiple: true + path: ./elfs + + - name: Zip debug elfs + run: zip -j -9 -r ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./elfs + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' }} + needs: [release-firmware, version] + env: + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ needs.version.outputs.long }} + enable_jekyll: true diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml new file mode 100644 index 000000000..07478ff93 --- /dev/null +++ b/.github/workflows/build_one_target.yml @@ -0,0 +1,395 @@ +name: Build One Target + +on: + workflow_dispatch: + inputs: + arch: + type: choice + options: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + - native + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. + + # find-target: + # type: boolean + # default: true + # description: 'Find the available targets' +jobs: + find-targets: + if: ${{ inputs.target == '' }} + strategy: + fail-fast: false + matrix: + arch: + - esp32 + - esp32s3 + - esp32c3 + - esp32c6 + - nrf52840 + - rp2040 + - rp2350 + - stm32 + + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: pip + - run: pip install -U platformio + - name: Generate matrix + id: jsonStep + run: | + TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} extra) + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "Targets:" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | sed 's/[][]//g; s/", "/\n- /g; s/"//g; s/^/- /' >> $GITHUB_STEP_SUMMARY + + version: + if: ${{ inputs.target != '' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get release version string + run: | + echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT + id: version + env: + BUILD_LOCATION: local + outputs: + long: ${{ steps.version.outputs.long }} + deb: ${{ steps.version.outputs.deb }} + + build-arch: + if: ${{ inputs.target != '' && inputs.arch != 'native' }} + needs: [version] + strategy: + fail-fast: false + uses: ./.github/workflows/build_firmware.yml + with: + version: ${{ needs.version.outputs.long }} + pio_env: ${{ inputs.target }} + platform: ${{ inputs.arch }} + + build-debian-src: + if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }} + uses: ./.github/workflows/build_debian_src.yml + with: + series: UNRELEASED + build_location: local + secrets: inherit + + package-pio-deps-native-tft: + if: ${{ inputs.arch == 'native' }} + uses: ./.github/workflows/package_pio_deps.yml + with: + pio_env: native-tft + secrets: inherit + + test-native: + if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }} + uses: ./.github/workflows/test_native.yml + + docker-deb-amd64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-deb-amd64-tft: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-alp-amd64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + + docker-alp-amd64-tft: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: alpine + platform: linux/amd64 + runs-on: ubuntu-24.04 + push: false + pio_env: native-tft + + docker-deb-arm64: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm64 + runs-on: ubuntu-24.04-arm + push: false + + docker-deb-armv7: + if: ${{ inputs.target != '' && inputs.arch == 'native' }} + uses: ./.github/workflows/docker_build.yml + with: + distro: debian + platform: linux/arm/v7 + runs-on: ubuntu-24.04-arm + push: false + + gather-artifacts: + permissions: + contents: write + pull-requests: write + strategy: + fail-fast: false + runs-on: ubuntu-latest + needs: [version, build-arch] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + + - uses: actions/download-artifact@v4 + with: + path: ./ + pattern: firmware-*-* + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Move files up + run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat + + - name: Repackage in single firmware zip + uses: actions/upload-artifact@v4 + with: + name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} + overwrite: true + path: | + ./firmware-*.bin + ./firmware-*.uf2 + ./firmware-*.hex + ./firmware-*-ota.zip + ./device-*.sh + ./device-*.bat + ./littlefs-*.bin + ./bleota*bin + ./Meshtastic_nRF52_factory_erase*.uf2 + retention-days: 30 + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-*-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + # For diagnostics + - name: Show artifacts + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output + + - name: Repackage in single elfs zip + uses: actions/upload-artifact@v4 + with: + name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + overwrite: true + path: ./*.elf + retention-days: 30 + + - uses: scruplelesswizard/comment-artifact@main + if: ${{ github.event_name == 'pull_request' }} + with: + name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} + description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" + github-token: ${{ secrets.GITHUB_TOKEN }} + + release-artifacts: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}} + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + needs: + - version + - gather-artifacts + - build-debian-src + - package-pio-deps-native-tft + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - name: Create release + uses: softprops/action-gh-release@v2 + id: create_release + with: + draft: true + prerelease: true + name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha + tag_name: v${{ needs.version.outputs.long }} + body: | + Autogenerated by github action, developer should edit as required before publishing... + + - name: Download source deb + uses: actions/download-artifact@v4 + with: + pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src + merge-multiple: true + path: ./output/debian-src + + - name: Download `native-tft` pio deps + uses: actions/download-artifact@v4 + with: + pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output/pio-deps-native-tft + + - name: Zip Linux sources + working-directory: output + run: | + zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src + zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add Linux sources to GtiHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip + gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release-firmware: + strategy: + fail-fast: false + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' && inputs.target != ''}} + needs: [release-artifacts, version] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-*-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./output + + - name: Display structure of downloaded files + run: ls -lR + + - name: Device scripts permissions + run: | + chmod +x ./output/device-install.sh + chmod +x ./output/device-update.sh + + - name: Zip firmware + run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output + + - uses: actions/download-artifact@v4 + with: + pattern: debug-elfs-*-${{ needs.version.outputs.long }}.zip + merge-multiple: true + path: ./elfs + + - name: Zip debug elfs + run: zip -j -9 -r ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./elfs + + # For diagnostics + - name: Display structure of downloaded files + run: ls -lR + + - name: Add bins and debug elfs to GitHub Release + # Only run when targeting master branch with workflow_dispatch + if: ${{ github.ref_name == 'master' }} + run: | + gh release upload v${{ needs.version.outputs.long }} ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-firmware: + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' && inputs.target != '' }} + needs: [release-firmware, version] + env: + targets: |- + esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: actions/download-artifact@v4 + with: + pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} + merge-multiple: true + path: ./publish + + - name: Publish firmware to meshtastic.github.io + uses: peaceiris/actions-gh-pages@v4 + env: + # On event/* branches, use the event name as the destination prefix + DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }} + with: + deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }} + external_repository: meshtastic/meshtastic.github.io + publish_branch: master + publish_dir: ./publish + destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} + keep_files: true + user_name: github-actions[bot] + user_email: github-actions[bot]@users.noreply.github.com + commit_message: ${{ needs.version.outputs.long }} + enable_jekyll: true diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index eb61554f2..7e2316e3d 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -21,12 +21,14 @@ permissions: jobs: docker-multiarch: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_manifest.yml with: release_channel: daily secrets: inherit package-ppa: + if: github.repository == 'meshtastic/firmware' strategy: fail-fast: false matrix: @@ -42,6 +44,7 @@ jobs: secrets: inherit package-obs: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/package_obs.yml with: obs_project: network:Meshtastic:daily @@ -49,6 +52,7 @@ jobs: secrets: inherit hook-copr: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/hook_copr.yml with: copr_project: daily diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 562ce00f9..4f2317bd0 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -27,6 +27,7 @@ on: jobs: setup: + if: github.repository == 'meshtastic/firmware' strategy: fail-fast: false matrix: @@ -70,6 +71,7 @@ jobs: check: ${{ steps.jsonStep.outputs.check }} version: + if: github.repository == 'meshtastic/firmware' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -91,7 +93,7 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.check) }} runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} + if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - uses: actions/checkout@v5 - name: Build base @@ -204,10 +206,11 @@ jobs: secrets: inherit test-native: - if: ${{ !contains(github.ref_name, 'event/') }} + if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} uses: ./.github/workflows/test_native.yml docker-deb-amd64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -216,6 +219,7 @@ jobs: push: false docker-deb-amd64-tft: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -225,6 +229,7 @@ jobs: pio_env: native-tft docker-alp-amd64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: alpine @@ -233,6 +238,7 @@ jobs: push: false docker-alp-amd64-tft: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: alpine @@ -242,6 +248,7 @@ jobs: pio_env: native-tft docker-deb-arm64: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -250,6 +257,7 @@ jobs: push: false docker-deb-armv7: + if: github.repository == 'meshtastic/firmware' uses: ./.github/workflows/docker_build.yml with: distro: debian @@ -259,6 +267,7 @@ jobs: gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) + if: github.repository == 'meshtastic/firmware' permissions: contents: write pull-requests: write @@ -358,7 +367,7 @@ jobs: release-artifacts: runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} needs: @@ -433,7 +442,7 @@ jobs: - rp2350 - stm32 runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}} needs: [release-artifacts, version] steps: - name: Checkout From 953fcca304ed37c94a0c20d19dcc8df86a853b00 Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 28 Aug 2025 11:23:24 -0500 Subject: [PATCH 32/62] BaseUI Show/Hide Frame Functionality (#7382) * Rename System Frame (from Memory) in code base * Create menu options to Show/Hide frames: Node Lists, Bearings, Position, LoRa, Clock and Favorites frames * Move Region Picker into submenu * Tweak wording for Send Position vs Node Info if the device has GPS --- src/graphics/Screen.cpp | 223 +++++++++++++++++++--------- src/graphics/Screen.h | 30 +++- src/graphics/draw/DebugRenderer.cpp | 2 +- src/graphics/draw/DebugRenderer.h | 4 +- src/graphics/draw/MenuHandler.cpp | 149 ++++++++++++++++++- src/graphics/draw/MenuHandler.h | 3 + src/graphics/images.h | 4 +- 7 files changed, 331 insertions(+), 84 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0a2229d0e..5bddb6cc8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -901,75 +901,90 @@ void Screen::setFrames(FrameFocus focus) } #if defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); + indicatorIcons.push_back(digital_icon_clock); + } #endif // Declare this early so it’s available in FOCUS_PRESERVE block bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } fsi.positions.textMessage = numframes; normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; - indicatorIcons.push_back(icon_nodes); + if (!hiddenFrames.nodelist) { + fsi.positions.nodelist = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance) { + if (RadioLibInterface::instance && !hiddenFrames.lora) { fsi.positions.lora = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; indicatorIcons.push_back(icon_radio); } - if (!dismissedFrames.memory) { - fsi.positions.memory = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; - indicatorIcons.push_back(icon_memory); + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); } #if !defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!dismissedFrames.wifi && isWifiAvailable()) { + if (!hiddenFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); @@ -1011,27 +1026,29 @@ void Screen::setFrames(FrameFocus focus) if (numMeshNodes > 0) numMeshNodes--; - // Temporary array to hold favorite node frames - std::vector favoriteFrames; + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } } - } - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE @@ -1070,7 +1087,7 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.clock); break; case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.memory); + ui->switchToFrame(fsi.positions.system); break; case FOCUS_PRESERVE: @@ -1098,30 +1115,96 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) setFastFramerate(); } +void Screen::toggleFrameVisibility(const std::string &frameName) +{ +#ifndef USE_EINK + if (frameName == "nodelist") { + hiddenFrames.nodelist = !hiddenFrames.nodelist; + } +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } +#endif + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } +} + +bool Screen::isFrameHidden(const std::string &frameName) const +{ +#ifndef USE_EINK + if (frameName == "nodelist") + return hiddenFrames.nodelist; +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; + if (frameName == "gps") + return hiddenFrames.gps; +#endif + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + + return false; +} + // Dismisses the currently displayed screen frame, if possible // Relevant for text message, waypoint, others in future? // Triggered with a CardKB keycombo -void Screen::dismissCurrentFrame() +void Screen::hideCurrentFrame() { uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Dismiss Text Message"); + LOG_INFO("Hide Text Message"); devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Dismiss Waypoint"); + LOG_DEBUG("Hide Waypoint"); devicestate.has_rx_waypoint = false; - dismissedFrames.waypoint = true; + hiddenFrames.waypoint = true; dismissed = true; } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Dismiss WiFi Screen"); - dismissedFrames.wifi = true; + LOG_DEBUG("Hide WiFi Screen"); + hiddenFrames.wifi = true; dismissed = true; - } else if (currentFrame == framesetInfo.positions.memory) { - LOG_INFO("Dismiss Memory"); - dismissedFrames.memory = true; + } else if (currentFrame == framesetInfo.positions.lora) { + LOG_INFO("Hide LoRa"); + hiddenFrames.lora = true; dismissed = true; } @@ -1277,7 +1360,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Outgoing message (likely sent from phone) devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - dismissedFrames.textMessage = true; + hiddenFrames.textMessage = true; hasUnreadMessage = false; // Clear unread state when user replies setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list @@ -1402,7 +1485,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { menuHandler::systemBaseMenu(); #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { @@ -1411,7 +1494,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { menuHandler::clockMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::LoraRegionPicker(); + menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { if (devicestate.rx_text_message.from) { menuHandler::messageResponseMenu(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ecc39ac60..85dcfaf69 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -593,7 +593,11 @@ class Screen : public concurrency::OSThread void setSSLFrames(); // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) - void dismissCurrentFrame(); + void hideCurrentFrame(); + + // Menu-driven Show / Hide Toggle + void toggleFrameVisibility(const std::string &frameName); + bool isFrameHidden(const std::string &frameName) const; #ifdef USE_EINK /// Draw an image to remain on E-Ink display after screen off @@ -655,7 +659,7 @@ class Screen : public concurrency::OSThread uint8_t settings = 255; uint8_t wifi = 255; uint8_t deviceFocused = 255; - uint8_t memory = 255; + uint8_t system = 255; uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; @@ -673,12 +677,28 @@ class Screen : public concurrency::OSThread uint8_t frameCount = 0; } framesetInfo; - struct DismissedFrames { + struct hiddenFrames { bool textMessage = false; bool waypoint = false; bool wifi = false; - bool memory = false; - } dismissedFrames; + bool system = false; + bool home = false; + bool clock = false; +#ifndef USE_EINK + bool nodelist = false; +#endif +#ifdef USE_EINK + bool nodelist_lastheard = false; + bool nodelist_hopsignal = false; + bool nodelist_distance = false; +#endif +#if HAS_GPS + bool nodelist_bearings = false; + bool gps = false; +#endif + bool lora = false; + bool show_favorites = false; + } hiddenFrames; /// Try to start drawing ASAP void setFastFramerate(); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 6137ddef8..61e919208 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -499,7 +499,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // **************************** // * System Screen * // **************************** -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setFont(FONT_SMALL); diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index f4d484f58..3382e931d 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -31,8 +31,8 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state // LoRa information display void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -// Memory screen display -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +// System screen display +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index ba554dbd6..48e7e808b 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -26,6 +26,24 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; +void menuHandler::loraMenu() +{ + static const char *optionsArray[] = {"Back", "Region Picker"}; + enum optionsNumbers { Back = 0, lora_picker = 1 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "LoRa Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } else if (selected == lora_picker) { + menuHandler::menuQueue = menuHandler::lora_picker; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::OnboardMessage() { static const char *optionsArray[] = {"OK", "Got it!"}; @@ -320,7 +338,7 @@ void menuHandler::messageResponseMenu() bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Dismiss) { - screen->dismissCurrentFrame(); + screen->hideCurrentFrame(); } else if (selected == Preset) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); @@ -361,8 +379,11 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Sleep Screen"; optionsEnumArray[options++] = Sleep; #endif - - optionsArray[options] = "Send Position"; + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + optionsArray[options] = "Send Position"; + } else { + optionsArray[options] = "Send Node Info"; + } optionsEnumArray[options++] = Position; #if defined(M5STACK_UNITC6L) optionsArray[options] = "New Preset"; @@ -455,7 +476,7 @@ void menuHandler::textMessageBaseMenu() void menuHandler::systemBaseMenu() { - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd }; + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; @@ -467,6 +488,9 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif + + optionsArray[options] = "Frame Visiblity Toggle"; + optionsEnumArray[options++] = FrameToggles; #if defined(M5STACK_UNITC6L) optionsArray[options] = "Bluetooth"; #else @@ -504,6 +528,9 @@ void menuHandler::systemBaseMenu() } else if (selected == PowerMenu) { menuHandler::menuQueue = menuHandler::power_menu; screen->runNow(); + } else if (selected == FrameToggles) { + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == Test) { menuHandler::menuQueue = menuHandler::test_menu; screen->runNow(); @@ -580,6 +607,7 @@ void menuHandler::positionBaseMenu() optionsArray[options] = "Compass Calibrate"; optionsEnumArray[options++] = CompassCalibrate; } + BannerOverlayOptions bannerOptions; bannerOptions.message = "Position Action"; bannerOptions.optionsArrayPtr = optionsArray; @@ -1220,6 +1248,116 @@ void menuHandler::keyVerificationFinalPrompt() } } +void menuHandler::FrameToggles_menu() +{ + enum optionsNumbers { + Finish, + nodelist, + nodelist_lastheard, + nodelist_hopsignal, + nodelist_distance, + nodelist_bearings, + gps, + lora, + clock, + show_favorites, + enumEnd + }; + static const char *optionsArray[enumEnd] = {"Finish"}; + static int optionsEnumArray[enumEnd] = {Finish}; + int options = 1; + + // Track last selected index (not enum value!) + static int lastSelectedIndex = 0; + +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; + optionsEnumArray[options++] = nodelist; +#endif +#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; + optionsEnumArray[options++] = nodelist_lastheard; + optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; + optionsEnumArray[options++] = nodelist_hopsignal; + optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; + optionsEnumArray[options++] = nodelist_distance; +#endif +#if HAS_GPS + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsEnumArray[options++] = nodelist_bearings; + + optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; + optionsEnumArray[options++] = gps; +#endif + + optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; + optionsEnumArray[options++] = lora; + + optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; + optionsEnumArray[options++] = clock; + + optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; + optionsEnumArray[options++] = show_favorites; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Show/Hide Frames"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value + + bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + // Find the index of selected in optionsEnumArray + int idx = 0; + for (; idx < options; ++idx) { + if (optionsEnumArray[idx] == selected) + break; + } + lastSelectedIndex = idx; + + if (selected == Finish) { + screen->setFrames(Screen::FOCUS_DEFAULT); + } else if (selected == nodelist) { + screen->toggleFrameVisibility("nodelist"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_lastheard) { + screen->toggleFrameVisibility("nodelist_lastheard"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_hopsignal) { + screen->toggleFrameVisibility("nodelist_hopsignal"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_distance) { + screen->toggleFrameVisibility("nodelist_distance"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_bearings) { + screen->toggleFrameVisibility("nodelist_bearings"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == gps) { + screen->toggleFrameVisibility("gps"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == lora) { + screen->toggleFrameVisibility("lora"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == clock) { + screen->toggleFrameVisibility("clock"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_favorites) { + screen->toggleFrameVisibility("show_favorites"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != menu_none) @@ -1316,6 +1454,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case power_menu: powerMenu(); break; + case FrameToggles: + FrameToggles_menu(); + break; case throttle_message: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index ed49a89fb..4e7e02173 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -39,11 +39,13 @@ class menuHandler key_verification_final_prompt, trace_route_menu, throttle_message, + FrameToggles }; static screenMenus menuQueue; static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); + static void loraMenu(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); @@ -77,6 +79,7 @@ class menuHandler static void screenOptionsMenu(); static void powerMenu(); static void textMessageMenu(); + static void FrameToggles_menu(); private: static void saveUIConfig(); diff --git a/src/graphics/images.h b/src/graphics/images.h index 4a58edb3b..fd9a2db0f 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = { 0xA9 // Row 7: #..#.#.# }; -// 🪙 Memory Icon -const uint8_t icon_memory[] PROGMEM = { +// 🪙 System Icon +const uint8_t icon_system[] PROGMEM = { 0x24, // Row 0: ..#..#.. 0x3C, // Row 1: ..####.. 0xC3, // Row 2: ##....## From 8e1da8561e76f0645d25a3761bd04506a52640e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:56:47 -0500 Subject: [PATCH 33/62] Update actions/checkout action to v5 (#8031) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 12 ++++++------ .github/workflows/build_one_target.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 5901a335c..204130711 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -52,7 +52,7 @@ jobs: version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -266,7 +266,7 @@ jobs: ] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -345,7 +345,7 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -414,7 +414,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -469,7 +469,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 07478ff93..f5831ed52 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -63,7 +63,7 @@ jobs: if: ${{ inputs.target != '' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT @@ -171,7 +171,7 @@ jobs: needs: [version, build-arch] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} @@ -250,7 +250,7 @@ jobs: - package-pio-deps-native-tft steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -309,7 +309,7 @@ jobs: needs: [release-artifacts, version] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 @@ -364,7 +364,7 @@ jobs: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Python uses: actions/setup-python@v5 From f083864f1fbb20b04df2fd646bf25bd27d0775fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:56:57 -0500 Subject: [PATCH 34/62] Update actions/download-artifact action to v5 (#8032) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 14 +++++++------- .github/workflows/build_one_target.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 204130711..3b11a9758 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -271,7 +271,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-${{inputs.arch}}-* @@ -300,7 +300,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -364,14 +364,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -421,7 +421,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -438,7 +438,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -476,7 +476,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index f5831ed52..4bf5dfe74 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -176,7 +176,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: path: ./ pattern: firmware-*-* @@ -205,7 +205,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -269,14 +269,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -316,7 +316,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -333,7 +333,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: debug-elfs-*-${{ needs.version.outputs.long }}.zip merge-multiple: true @@ -371,7 +371,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true From 7821919faea62d751352d8859531d5a35caa17a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 20:12:28 -0500 Subject: [PATCH 35/62] Update actions/setup-python action to v6 (#8033) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build_one_arch.yml | 8 ++++---- .github/workflows/build_one_target.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_one_arch.yml b/.github/workflows/build_one_arch.yml index 3b11a9758..f5352b3c4 100644 --- a/.github/workflows/build_one_arch.yml +++ b/.github/workflows/build_one_arch.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -348,7 +348,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -417,7 +417,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -472,7 +472,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 4bf5dfe74..3c83ce960 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x cache: pip @@ -253,7 +253,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -312,7 +312,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x @@ -367,7 +367,7 @@ jobs: uses: actions/checkout@v5 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.x From dcd53eb7cb61a751903851215551b037e28c3301 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 5 Sep 2025 23:06:58 -0400 Subject: [PATCH 36/62] Phone GPS display on Position Screen for BaseUI (#7875) * Phone GPS display on Position Screen This is a PR to show when a phone shares GPS location with the node so you can reliably know what coordinate is being shared with the Mesh. --- src/graphics/draw/UIRenderer.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e00b19b2f..cd88d2f3d 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -937,7 +937,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) && + config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED); + + if (usePhoneGPS) { + // Phone-provided GPS is active + displayLine = "Phone GPS"; + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (isHighResolution) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // GPS disabled / not present if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { @@ -954,6 +973,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int xOffset = (isHighResolution) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { + // Onboard GPS UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); } @@ -1013,6 +1033,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Fifth Row: Altitude === char DisplayLineTwo[32] = {0}; + int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) + ? ourNode->position.altitude + : geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { From 39648e609a6229e89571d1b1495d1c8632fd06f5 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 16 Sep 2025 13:11:18 -0500 Subject: [PATCH 37/62] Merge pull request #8004 from compumike/compumike/debug-heap-add-free-heap-debugging-to-all-log-lines When `DEBUG_HEAP` is defined, add free heap bytes to every log line in `RedirectablePrint::log_to_serial` --- platformio.ini | 1 + src/RedirectablePrint.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/platformio.ini b/platformio.ini index 42453a33e..941e33beb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -55,6 +55,7 @@ build_flags = -Wno-missing-field-initializers -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-D OLED_PL=1 + #-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs monitor_speed = 115200 monitor_filters = direct diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 7c8d77651..1a5fd6e6f 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -4,6 +4,7 @@ #include "concurrency/OSThread.h" #include "configuration.h" #include "main.h" +#include "memGet.h" #include "mesh/generated/meshtastic/mesh.pb.h" #include #include @@ -166,6 +167,16 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, print(thread->ThreadName); print("] "); } + +#ifdef DEBUG_HEAP + // Add heap free space bytes prefix before every log message +#ifdef ARCH_PORTDUINO + ::printf("[heap %u] ", memGet.getFreeHeap()); +#else + printf("[heap %u] ", memGet.getFreeHeap()); +#endif +#endif // DEBUG_HEAP + r += vprintf(logLevel, format, arg); } From 2bafac242efe0257b7dae5d07f1143a6d07e587b Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Sep 2025 02:29:47 +0200 Subject: [PATCH 38/62] Feature: Seamless Cross-Preset Communication via UDP Multicast Bridging (#7753) * Added compatibility between nodes on different Presets through `Mesh via UDP` * Optimize multicast handling and channel mapping - FloodingRouter: remove redundant UDP-encrypted rebroadcast suppression. - Router: guard multicast fallback with HAS_UDP_MULTICAST and map fallback-decoded packets to the local default channel via isDefaultChannel() - UdpMulticastHandler: set transport_mechanism only after successful decode * trunk fmt * Move setting transport mechanism. --------- Co-authored-by: GUVWAF --- src/mesh/Channels.cpp | 26 +++++++++++++++++++++++ src/mesh/Channels.h | 2 ++ src/mesh/Router.cpp | 30 +++++++++++++++++++++++++++ src/mesh/udp/UdpMulticastHandler.h | 2 +- variants/esp32s3/t-deck-pro/variant.h | 15 +++++++------- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4ef41ddfb..4c0a0edad 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -423,6 +423,32 @@ bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash) } } +bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) +{ + // Iterate all known presets + for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; + ++preset) { + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + if (!name) + continue; + if (strcmp(name, "Invalid") == 0) + continue; // skip invalid placeholder + uint8_t h = xorHash((const uint8_t *)name, strlen(name)); + // Expand default PSK alias 1 to actual bytes and xor into hash + uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk)); + if (tmp == channelHash) { + // Set crypto to defaultpsk and report success + CryptoKey k; + memcpy(k.bytes, defaultpsk, sizeof(defaultpsk)); + k.length = sizeof(defaultpsk); + crypto->setKey(k); + LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash); + return true; + } + } + return false; +} + /** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured) * * This method is called before encoding outbound packets diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index 7873a306a..b53f552fa 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -94,6 +94,8 @@ class Channels bool ensureLicensedOperation(); + bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + private: /** Given a channel index, change to use the crypto key specified by that index * diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6c5d08a93..8bfa7ae9f 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -430,6 +430,36 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) } } } + +#if HAS_UDP_MULTICAST + // Fallback: for UDP multicast, try default preset names with default PSK if normal channel match failed + if (!decrypted && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP) { + if (channels.setDefaultPresetCryptoForHash(p->channel)) { + memcpy(bytes, p->encrypted.bytes, rawSize); + crypto->decrypt(p->from, p->id, rawSize, bytes); + + meshtastic_Data decodedtmp; + memset(&decodedtmp, 0, sizeof(decodedtmp)); + if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp) && + decodedtmp.portnum != meshtastic_PortNum_UNKNOWN_APP) { + p->decoded = decodedtmp; + p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; + // Map to our local default channel index (name+PSK default), not necessarily primary + ChannelIndex defaultIndex = channels.getPrimaryIndex(); + for (ChannelIndex i = 0; i < channels.getNumChannels(); ++i) { + if (channels.isDefaultChannel(i)) { + defaultIndex = i; + break; + } + } + chIndex = defaultIndex; + decrypted = true; + } else { + LOG_WARN("UDP fallback decode attempted but failed for hash 0x%x", p->channel); + } + } + } +#endif if (decrypted) { // parsing was successful p->channel = chIndex; // change to store the index instead of the hash diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 9650668a8..2df8686a3 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -50,10 +50,10 @@ class UdpMulticastHandler final LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif meshtastic_MeshPacket mp; - mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP; mp.pki_encrypted = false; mp.public_key.size = 0; memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index abe0a772a..35cb99435 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -93,11 +93,10 @@ // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) -#define MODEM_POWER_EN 41 -#define MODEM_PWRKEY 40 -#define MODEM_RST 9 -#define MODEM_RI 7 -#define MODEM_DTR 8 -#define MODEM_RX 10 -#define MODEM_TX 11 - +#define MODEM_POWER_EN 41 +#define MODEM_PWRKEY 40 +#define MODEM_RST 9 +#define MODEM_RI 7 +#define MODEM_DTR 8 +#define MODEM_RX 10 +#define MODEM_TX 11 From 68ba3b315c5d7e5b6b35764330b865d1343a1fa1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 17 Sep 2025 08:37:51 -0500 Subject: [PATCH 39/62] Auto-favorite remote admin node --- src/modules/AdminModule.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 407003f7e..79ea7bc0c 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -104,6 +104,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta (config.security.admin_key[2].size == 32 && memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) { LOG_INFO("PKC admin payload with authorized sender key"); + auto remoteNode = nodeDB->getMeshNode(mp.from); + if (remoteNode && !remoteNode->is_favorite) { + remoteNode->is_favorite = true; + } } else { myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp); LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!"); From b14e5770d540512f46cfa93fc046f9d2cdd8f804 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 12 Sep 2025 13:11:53 -0500 Subject: [PATCH 40/62] Merge pull request #7873 from compumike/compumike/client-base-role Add `CLIENT_BASE` role: `ROUTER` for favorites, `CLIENT` otherwise (for attic/roof nodes!) --- src/DisplayFormatters.cpp | 45 ++++++++++++++++++++++ src/graphics/Screen.cpp | 8 ++-- src/mesh/FloodingRouter.cpp | 26 +++++++++++-- src/mesh/FloodingRouter.h | 4 ++ src/mesh/NextHopRouter.cpp | 13 +++++-- src/mesh/NextHopRouter.h | 3 ++ src/mesh/NodeDB.cpp | 59 +++++++++++++++++++++++++++++ src/mesh/NodeDB.h | 10 +++++ src/mesh/RadioInterface.cpp | 23 +++++++++-- src/mesh/RadioInterface.h | 5 ++- src/mesh/RadioLibInterface.cpp | 8 ++-- src/mesh/RadioLibInterface.h | 2 +- src/platform/portduino/SimRadio.cpp | 6 +-- src/platform/portduino/SimRadio.h | 2 +- 14 files changed, 191 insertions(+), 23 deletions(-) diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d367aa661..5193e1cb4 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -38,4 +38,49 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC return useShortName ? "Custom" : "Invalid"; break; } +} + +const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role) +{ + switch (role) { + case meshtastic_Config_DeviceConfig_Role_CLIENT: + return "Client"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE: + return "Client Mute"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN: + return "Client Hidden"; + break; + case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE: + return "Client Base"; + break; + case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND: + return "Lost and Found"; + break; + case meshtastic_Config_DeviceConfig_Role_TRACKER: + return "Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_SENSOR: + return "Sensor"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK: + return "TAK"; + break; + case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER: + return "TAK Tracker"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER: + return "Router"; + break; + case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE: + return "Router Late"; + break; + case meshtastic_Config_DeviceConfig_Role_REPEATER: + return "Repeater"; + break; + default: + return "Unknown"; + break; + } } \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5bddb6cc8..807cb5867 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -25,6 +25,7 @@ along with this program. If not, see . #include "PowerMon.h" #include "Throttle.h" #include "configuration.h" +#include "meshUtils.h" #if HAS_SCREEN #include @@ -58,7 +59,6 @@ along with this program. If not, see . #include "mesh-pb-constants.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" -#include "meshUtils.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" @@ -1562,13 +1562,15 @@ bool shouldWakeOnReceivedMessage() /* The goal here is to determine when we do NOT wake up the screen on message received: - Any ext. notifications are turned on - - If role is not client / client_mute + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - If the battery level is very low */ if (moduleConfig.external_notification.enabled) { return false; } - if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) { + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { return false; } if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index dbd458b61..f805055c8 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -43,12 +43,30 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return Router::shouldFilterReceived(p); } +bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p) +{ + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + // ROUTER, REPEATER, ROUTER_LATE should never cancel relaying a packet (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return false; + } + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + // CLIENT_BASE: if the packet is from or to a favorited node, + // we should act like a ROUTER and should never cancel a rebroadcast (i.e. we should always rebroadcast), + // even if we've heard another station rebroadcast it already. + return !nodeDB->isFromOrToFavoritedNode(*p); + } + + // All other roles (such as CLIENT) should cancel a rebroadcast if they hear another station's rebroadcast. + return true; +} + void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && - p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) { + if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && roleAllowsCancelingDupe(p)) { // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! // But only LoRa packets should be able to trigger this. if (Router::cancelSending(p->from, p->id)) diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 36c6ad8aa..30ad5945b 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -59,6 +59,10 @@ class FloodingRouter : public Router */ virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) override; + // Return false for roles like ROUTER or REPEATER which should always rebroadcast even when we've heard another rebroadcast of + // the same packet + bool roleAllowsCancelingDupe(const meshtastic_MeshPacket *p); + /* Call when receiving a duplicate packet to check whether we should cancel a packet in the Tx queue */ void perhapsCancelDupe(const meshtastic_MeshPacket *p); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index db3d62038..9bb8b240c 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -161,6 +161,15 @@ bool NextHopRouter::stopRetransmission(NodeNum from, PacketId id) return stopRetransmission(key); } +bool NextHopRouter::roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p) +{ + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + + // Return false for roles like ROUTER, REPEATER, ROUTER_LATE which should always transmit the packet at least once. + + return roleAllowsCancelingDupe(p); // same logic as FloodingRouter::roleAllowsCancelingDupe +} + bool NextHopRouter::stopRetransmission(GlobalPacketId key) { auto old = findPendingPacket(key); @@ -170,9 +179,7 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) to avoid canceling a transmission if it was ACKed super fast via MQTT */ if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { // We only cancel it if we are the original sender or if we're not a router(_late)/repeater - if (isFromUs(p) || (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && - config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && - config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + if (isFromUs(p) || roleAllowsCancelingFromTxQueue(p)) { // remove the 'original' (identified by originator and packet->id) from the txqueue and free it cancelSending(getFrom(p), p->id); } diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index 6c2764aff..0022644e9 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -121,6 +121,9 @@ class NextHopRouter : public FloodingRouter */ PendingPacket *startRetransmission(meshtastic_MeshPacket *p, uint8_t numReTx = NUM_INTERMEDIATE_RETX); + // Return true if we're allowed to cancel a packet in the txQueue (so we may never transmit it even once) + bool roleAllowsCancelingFromTxQueue(const meshtastic_MeshPacket *p); + /** * Stop any retransmissions we are doing of the specified node/packet ID pair * diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 6473722d7..65d2f4d1c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1750,6 +1750,65 @@ void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId) } } +bool NodeDB::isFavorite(uint32_t nodeId) +{ + // returns true if nodeId is_favorite; false if not or not found + + // NODENUM_BROADCAST will never be in the DB + if (nodeId == NODENUM_BROADCAST) + return false; + + meshtastic_NodeInfoLite *lite = getMeshNode(nodeId); + + if (lite) { + return lite->is_favorite; + } + return false; +} + +bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p) +{ + // This method is logically equivalent to: + // return isFavorite(p.from) || isFavorite(p.to); + // but is more efficient by: + // 1. doing only one pass through the database, instead of two + // 2. exiting early when a favorite is found, or if both from and to have been seen + + if (p.to == NODENUM_BROADCAST) + return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from + + meshtastic_NodeInfoLite *lite = NULL; + + bool seenFrom = false; + bool seenTo = false; + + for (int i = 0; i < numMeshNodes; i++) { + lite = &meshNodes->at(i); + + if (lite->num == p.from) { + if (lite->is_favorite) + return true; + + seenFrom = true; + } + + if (lite->num == p.to) { + if (lite->is_favorite) + return true; + + seenTo = true; + } + + if (seenFrom && seenTo) + return false; // we've seen both, and neither is a favorite, so we can stop searching early + + // Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching + // all favorited nodes first. + } + + return false; +} + void NodeDB::pause_sort(bool paused) { sortingIsPaused = paused; diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 167dc1337..f73f64f92 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -185,6 +185,16 @@ class NodeDB */ void set_favorite(bool is_favorite, uint32_t nodeId); + /* + * Returns true if the node is in the NodeDB and marked as favorite + */ + bool isFavorite(uint32_t nodeId); + + /* + * Returns true if p->from or p->to is a favorited node + */ + bool isFromOrToFavoritedNode(const meshtastic_MeshPacket &p); + /** * Other functions like the node picker can request a pause in the node sorting */ diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index a5c293868..71fcf1e74 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -311,16 +311,33 @@ uint32_t RadioInterface::getTxDelayMsecWeightedWorst(float snr) return (2 * CWmax * slotTimeMsec) + pow_of_2(CWsize) * slotTimeMsec; } +/** Returns true if we should rebroadcast early like a ROUTER */ +bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) +{ + // If we are a ROUTER or REPEATER, we always rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + return true; + } + + // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { + return nodeDB->isFromOrToFavoritedNode(*p); + } + + return false; +} + /** The delay to use when we want to flood a message */ -uint32_t RadioInterface::getTxDelayMsecWeighted(float snr) +uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) { // high SNR = large CW size (Long Delay) // low SNR = small CW size (Short Delay) + float snr = p->rx_snr; uint32_t delay = 0; uint8_t CWsize = getCWsize(snr); // LOG_DEBUG("rx_snr of %f so setting CWsize to:%d", snr, CWsize); - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || - config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { + if (shouldRebroadcastEarlyLikeRouter(p)) { delay = random(0, 2 * CWsize) * slotTimeMsec; LOG_DEBUG("rx_snr found in packet. Router: setting tx delay:%d", delay); } else { diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index c9e71cfa8..eff284747 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -180,8 +180,11 @@ class RadioInterface /** The worst-case SNR_based packet delay */ uint32_t getTxDelayMsecWeightedWorst(float snr); + /** Returns true if we should rebroadcast early like a ROUTER */ + bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */ - uint32_t getTxDelayMsecWeighted(float snr); + uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p); /** If the packet is not already in the late rebroadcast window, move it there */ virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; } diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e3ef58f14..56341adf9 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -310,7 +310,7 @@ void RadioLibInterface::setTransmitDelay() // So we want to make sure the other side has had a chance to reconfigure its radio. if (p->tx_after) { - unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p->rx_snr) : getTxDelayMsec(); + unsigned long add_delay = p->rx_rssi ? getTxDelayMsecWeighted(p) : getTxDelayMsec(); unsigned long now = millis(); p->tx_after = min(max(p->tx_after + add_delay, now + add_delay), now + 2 * getTxDelayMsecWeightedWorst(p->rx_snr)); notifyLater(p->tx_after - now, TRANSMIT_DELAY_COMPLETED, false); @@ -323,7 +323,7 @@ void RadioLibInterface::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p); } } @@ -336,11 +336,11 @@ void RadioLibInterface::startTransmitTimer(bool withDelay) } } -void RadioLibInterface::startTransmitTimerSNR(float snr) +void RadioLibInterface::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delay = getTxDelayMsecWeighted(snr); + uint32_t delay = getTxDelayMsecWeighted(p); notifyLater(delay, TRANSMIT_DELAY_COMPLETED, false); // This will implicitly enable } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 2ab2679c0..9f497812f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -161,7 +161,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified * timer scaled to SNR of to be flooded packet * @return Timestamp after which the packet may be sent */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 4e748c5f9..cea1eab3a 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -43,7 +43,7 @@ void SimRadio::setTransmitDelay() } else { // If there is a SNR, start a timer scaled based on that SNR. LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr); - startTransmitTimerSNR(p->rx_snr); + startTransmitTimerRebroadcast(p); } } @@ -57,11 +57,11 @@ void SimRadio::startTransmitTimer(bool withDelay) } } -void SimRadio::startTransmitTimerSNR(float snr) +void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p) { // If we have work to do and the timer wasn't already scheduled, schedule it now if (!txQueue.empty()) { - uint32_t delayMsec = getTxDelayMsecWeighted(snr); + uint32_t delayMsec = getTxDelayMsecWeighted(p); // LOG_DEBUG("xmit timer %d", delay); notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false); } diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index ea534bd65..d8b53739f 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -64,7 +64,7 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr void startTransmitTimer(bool withDelay = true); /** timer scaled to SNR of to be flooded packet */ - void startTransmitTimerSNR(float snr); + void startTransmitTimerRebroadcast(meshtastic_MeshPacket *p); void handleTransmitInterrupt(); void handleReceiveInterrupt(); From e2ce36978274b7d5661ee77ff34934de2b3932cc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 06:29:18 -0500 Subject: [PATCH 41/62] Fixes --- src/DisplayFormatters.h | 1 + src/mesh/Channels.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index 2d7a3e8db..981010b33 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -6,4 +6,5 @@ class DisplayFormatters public: static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, bool usePreset); + static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role); }; diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4c0a0edad..aec112a3e 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -428,7 +428,8 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false); + const char *name = + DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, false); if (!name) continue; if (strcmp(name, "Invalid") == 0) From 3fbe7fd8b21df6528bc8df19b7cad7458b5e32f7 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 5 Sep 2025 20:44:32 -0500 Subject: [PATCH 42/62] BaseUI Updates (#7787) * Account for low resolution wide screen OLEDs * Allow picking of Device Role and new Display Formatter for Device Role * Add remainder of client roles to display formatter * Don't update the role unless you pick a value * Mascots are fun * Fix warnings during compile time * Improve some menus * Mascots need to work everywhere * Update Chirpy image * Fix Trunk * Update protobufs * Add date to Clock screen * Analog clocks love dates too * Finalize date moves for analog clock --- .vscode/settings.json | 5 ++ protobufs | 2 +- src/graphics/Screen.cpp | 10 ++++ src/graphics/Screen.h | 2 + src/graphics/SharedUIDisplay.cpp | 4 ++ src/graphics/draw/ClockRenderer.cpp | 25 ++++++++++ src/graphics/draw/DebugRenderer.cpp | 47 ++++++++++++++++-- src/graphics/draw/DebugRenderer.h | 3 ++ src/graphics/draw/MenuHandler.cpp | 77 +++++++++++++++++++++++++---- src/graphics/draw/MenuHandler.h | 3 ++ src/graphics/draw/UIRenderer.cpp | 44 ++++++++++++++++- src/graphics/images.h | 72 +++++++++++++++++++++++++++ 12 files changed, 277 insertions(+), 17 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 81deca8f9..a54386544 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,10 @@ }, "[powershell]": { "editor.defaultFormatter": "ms-vscode.powershell" + }, + "files.associations": { + "deque": "cpp", + "string": "cpp", + "vector": "cpp" } } diff --git a/protobufs b/protobufs index 945b796a9..27d9a99bd 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 945b796a982f38171a9e0d28b5c8b1f7d53c5cd1 +Subproject commit 27d9a99bd03efe35f91cafd7116c2386be5e26a1 diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 807cb5867..0c88ed0a6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -982,6 +982,11 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(digital_icon_clock); } #endif + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(small_chirpy); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!hiddenFrames.wifi && isWifiAvailable()) { @@ -1150,6 +1155,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName) if (frameName == "show_favorites") { hiddenFrames.show_favorites = !hiddenFrames.show_favorites; } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } } bool Screen::isFrameHidden(const std::string &frameName) const @@ -1178,6 +1186,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.clock; if (frameName == "show_favorites") return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; return false; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 85dcfaf69..262ba4175 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -669,6 +669,7 @@ class Screen : public concurrency::OSThread uint8_t nodelist_distance = 255; uint8_t nodelist_bearings = 255; uint8_t clock = 255; + uint8_t chirpy = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; uint8_t lora = 255; @@ -698,6 +699,7 @@ class Screen : public concurrency::OSThread #endif bool lora = false; bool show_favorites = false; + bool chirpy = true; } hiddenFrames; /// Try to start drawing ASAP diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 13691665a..0f32b0896 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -16,6 +16,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth) isHighResolution = true; } + if (screenwidth > 128 && screenheight <= 64) { + isHighResolution = false; + } + // Special case for Heltec Wireless Tracker v1.1 if (screenwidth == 160 && screenheight == 80) { isHighResolution = false; diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 08466662c..d046bda6f 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -191,6 +191,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -294,11 +295,21 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, isPM ? "pm" : "am"); } + #ifndef USE_EINK xOffset = (isHighResolution) ? 18 : 10; display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, secondString); #endif + + // Display GPS derived date + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); + yOffset = (isHighResolution) ? 12 : 1; + display->drawString(startingHourMinuteTextX + timeStringWidth - display->getStringWidth(fullLine), + getTextPositions(display)[line] + yOffset, fullLine); } void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) @@ -314,6 +325,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -511,6 +523,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // draw second hand display->drawLine(centerX, centerY, secondX, secondY); #endif + + display->setFont(FONT_SMALL); + // Display GPS derived date + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char fullLine[40]; + if (isHighResolution) { + snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); + } else { + snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); + } + display->drawString(display->getWidth() - 1 - display->getStringWidth(fullLine), getTextPositions(display)[line], + fullLine); } } diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 61e919208..fb35134fd 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -395,8 +395,18 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Second Row: Radio Preset === + + // === Second Row: Role === + auto role = DisplayFormatters::getDeviceRole(config.device.role); + char device_role[25]; + snprintf(device_role, sizeof(device_role), "Role: %s", role); + textWidth = display->getStringWidth(device_role); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], device_role); + + // === Third Row: Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { @@ -410,7 +420,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); - // === Third Row: Frequency / ChanNum === + // === Fourth Row: Frequency / ChanNum === char frequencyslot[35]; char freqStr[16]; float freq = RadioLibInterface::instance->getFreq(); @@ -437,7 +447,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); #if !defined(M5STACK_UNITC6L) - // === Fourth Row: Channel Utilization === + // === Fifth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); @@ -454,7 +464,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, getTextPositions(display)[line++], chUtil); + display->drawString(starting_position, getTextPositions(display)[line], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { @@ -491,7 +501,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); #endif } @@ -655,6 +665,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x } #endif } + +// **************************** +// * Chirpy Screen * +// **************************** +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); + int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; + int textX_offset = 10; + if (isHighResolution) { + iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); + iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; + textX_offset = textX_offset * 4; + display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); + } else { + display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); + } + + int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); + display->drawString(textX, getTextPositions(display)[line++], "Hello"); + textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); + display->drawString(textX, getTextPositions(display)[line++], "World!"); +} } // namespace DebugRenderer } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 3382e931d..563a6c1ce 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -33,6 +33,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // System screen display void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Chirpy screen display +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 48e7e808b..975fc7c0a 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -28,17 +28,19 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { - static const char *optionsArray[] = {"Back", "Region Picker"}; - enum optionsNumbers { Back = 0, lora_picker = 1 }; + static const char *optionsArray[] = {"Back", "Region Picker", "Device Role"}; + enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action } else if (selected == lora_picker) { menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == device_role_picker) { + menuHandler::menuQueue = menuHandler::device_role_picker; } }; screen->showOverlayBanner(bannerOptions); @@ -141,6 +143,40 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } +void menuHandler::DeviceRolePicker() +{ + static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; + enum optionsNumbers { + Back = 0, + devicerole_client = 1, + devicerole_clientmute = 2, + devicerole_lostandfound = 3, + devicerole_tracker = 4 + }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Device Role"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } else if (selected == devicerole_client) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else if (selected == devicerole_clientmute) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + } else if (selected == devicerole_lostandfound) { + config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + } else if (selected == devicerole_tracker) { + config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::TwelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; @@ -1038,16 +1074,33 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - static const char *optionsArray[] = {"Back", "Number Picker"}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; + + optionsArray[options] = "Number Picker"; + optionsEnumArray[options++] = NumberPicker; + + optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; + optionsEnumArray[options++] = ShowChirpy; + BannerOverlayOptions bannerOptions; - std::string message = "Test to Run?\n"; - bannerOptions.message = message.c_str(); + bannerOptions.message = "Hidden Test Menu"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { + if (selected == NumberPicker) { menuQueue = number_test; screen->runNow(); + } else if (selected == ShowChirpy) { + screen->toggleFrameVisibility("chirpy"); + screen->setFrames(Screen::FOCUS_SYSTEM); + + } else { + menuQueue = system_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -1306,7 +1359,7 @@ void menuHandler::FrameToggles_menu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value - bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + bannerOptions.bannerCallback = [options](int selected) mutable -> void { // Find the index of selected in optionsEnumArray int idx = 0; for (; idx < options; ++idx) { @@ -1365,9 +1418,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) switch (menuQueue) { case menu_none: break; + case lora_Menu: + loraMenu(); + break; case lora_picker: LoraRegionPicker(); break; + case device_role_picker: + DeviceRolePicker(); + break; case no_timeout_lora_picker: LoraRegionPicker(0); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 4e7e02173..1bfdf128f 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -9,7 +9,9 @@ class menuHandler public: enum screenMenus { menu_none, + lora_Menu, lora_picker, + device_role_picker, no_timeout_lora_picker, TZ_picker, twelve_hour_picker, @@ -46,6 +48,7 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); + static void DeviceRolePicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index cd88d2f3d..8df3ee517 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1019,6 +1019,45 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 2, latStr); #else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; + + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), "Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + } + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); #endif @@ -1029,6 +1068,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); #else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude === @@ -1037,9 +1077,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU ? ourNode->position.altitude : geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); #endif diff --git a/src/graphics/images.h b/src/graphics/images.h index fd9a2db0f..72dda7886 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -290,6 +290,78 @@ const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, #ifdef M5STACK_UNITC6L #include "img/icon_small.xbm" #else +#define chirpy_width 38 +#define chirpy_height 50 +static unsigned char chirpy[] = { + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, + 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, + 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, + 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, + 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, + 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, + 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, + 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, + 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + +#define chirpy_width_hirez 76 +#define chirpy_height_hirez 100 +static unsigned char chirpy_hirez[] = { + 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, + 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, + 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, + 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, + 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, + 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, + 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, + 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, + 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, + 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, + 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, + 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, + 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, + 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, + 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3}; + +#define chirpy_small_image_width 8 +#define chirpy_small_image_height 8 +static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; + #include "img/icon.xbm" #endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file From e20a91b94542ba59645523cd1d560a8c95f18b5c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:33:02 -0400 Subject: [PATCH 43/62] Added Last Coordinate counter to Position screen (#7865) Adding a counter to show the last time a GPS coordinate was detected to ensure the user is aware how long since the coordinate updated or to identify any errors. --- src/GPSStatus.h | 9 ++++++ src/graphics/draw/UIRenderer.cpp | 55 ++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/GPSStatus.h b/src/GPSStatus.h index 4b7997935..a1a9f2c56 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -22,6 +22,9 @@ class GPSStatus : public Status meshtastic_Position p = meshtastic_Position_init_default; + /// Time of last valid GPS fix (millis since boot) + uint32_t lastFixMillis = 0; + public: GPSStatus() { statusType = STATUS_TYPE_GPS; } @@ -83,6 +86,9 @@ class GPSStatus : public Status uint32_t getNumSatellites() const { return p.sats_in_view; } + /// Return millis() when the last GPS fix occurred (0 = never) + uint32_t getLastFixMillis() const { return lastFixMillis; } + bool matches(const GPSStatus *newStatus) const { #ifdef GPS_DEBUG @@ -114,6 +120,9 @@ class GPSStatus : public Status if (isDirty) { if (hasLock) { + // Record time of last valid GPS fix + lastFixMillis = millis(); + // In debug logs, identify position by @timestamp:stage (stage 3 = notify) LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 8df3ee517..e76a39398 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1000,17 +1000,54 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // If GPS is off, no need to display these parts if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { - - // === Second Row: Date === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char datetimeStr[25]; - bool showTime = false; // set to true for full datetime - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); + /* MUST BE MOVED TO CLOCK SCREEN + // === Second Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); #if !defined(M5STACK_UNITC6L) - display->drawString(0, getTextPositions(display)[line++], fullLine); + display->drawString(0, getTextPositions(display)[line++], fullLine); #endif + */ + + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; + + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), " Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), " Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], " Last: ?"); + } // === Third Row: Latitude === char latStr[32]; From 6a92358b6883f6dc86e9c456b45662732fd8b45e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 07:22:23 -0500 Subject: [PATCH 44/62] Fix --- src/graphics/draw/UIRenderer.cpp | 40 -------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e76a39398..5623c9026 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1056,45 +1056,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 2, latStr); #else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); - // === Second Row: Last GPS Fix === - if (gpsStatus->getLastFixMillis() > 0) { - uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix - uint32_t days = delta / 86400; - uint32_t hours = (delta % 86400) / 3600; - uint32_t mins = (delta % 3600) / 60; - uint32_t secs = delta % 60; - - char buf[32]; -#if defined(USE_EINK) - // E-Ink: skip seconds, show only days/hours/mins - if (days > 0) { - snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); - } else { - snprintf(buf, sizeof(buf), " Last: %um", mins); - } -#else - // Non E-Ink: include seconds where useful - if (days > 0) { - snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); - } else if (mins > 0) { - snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); - } else { - snprintf(buf, sizeof(buf), "Last: %us", secs); - } -#endif - - display->drawString(0, getTextPositions(display)[line++], buf); - } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); - } - - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); #endif @@ -1105,7 +1066,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); #else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude === From 1ac2382d7cad544c5685aa50a18096d1fe20aadb Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 07:29:54 -0500 Subject: [PATCH 45/62] Revert "Fix excluded modules configuration handling (#7838)" This reverts commit 9c6544ebfa8248412d1008f0d32c986c1dc99d38. --- src/configuration.h | 1 - src/main.cpp | 18 ++------------- src/mesh/NodeDB.cpp | 20 ----------------- src/mesh/PhoneAPI.cpp | 45 ------------------------------------- src/mesh/PhoneAPI.h | 3 --- src/modules/AdminModule.cpp | 29 ------------------------ 6 files changed, 2 insertions(+), 114 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 515380cfb..1b386ec17 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -438,7 +438,6 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_SERIAL 1 #define MESHTASTIC_EXCLUDE_POWERSTRESS 1 #define MESHTASTIC_EXCLUDE_ADMIN 1 -#define MESHTASTIC_EXCLUDE_AMBIENTLIGHTING 1 #endif // // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled) diff --git a/src/main.cpp b/src/main.cpp index d6f9fb95b..3f84a5e66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1513,9 +1513,6 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() deviceMetadata.hw_model = HW_VENDOR; deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled; deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE; -#if MESHTASTIC_EXCLUDE_MQTT - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_MQTT_CONFIG; -#endif #if MESHTASTIC_EXCLUDE_REMOTEHARDWARE deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG; #endif @@ -1538,21 +1535,10 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG; #endif -#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER - // PAXCOUNTER is only supported on ESP32 due to memory constraints -#else +#ifndef ARCH_ESP32 deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG; #endif -#if MESHTASTIC_EXCLUDE_STOREFORWARD - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_STOREFORWARD_CONFIG; -#endif -#if MESHTASTIC_EXCLUDE_RANGETEST - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_RANGETEST_CONFIG; -#endif -#if MESHTASTIC_EXCLUDE_NEIGHBORINFO - deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NEIGHBORINFO_CONFIG; -#endif -#if (!defined(HAS_RGB_LED) && !defined(RAK_4631)) || defined(MESHTASTIC_EXCLUDE_AMBIENTLIGHTING) +#if !defined(HAS_RGB_LED) && !RAK_4631 deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG; #endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 53a796169..237b4286c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -775,9 +775,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.version = DEVICESTATE_CUR_VER; moduleConfig.has_mqtt = true; -#if !MESHTASTIC_EXCLUDE_RANGETEST moduleConfig.has_range_test = true; -#endif moduleConfig.has_serial = true; moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; @@ -843,12 +841,6 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; -#if !MESHTASTIC_EXCLUDE_AUDIO - moduleConfig.has_audio = true; -#endif -#if !MESHTASTIC_EXCLUDE_PAXCOUNTER - moduleConfig.has_paxcounter = true; -#endif #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif @@ -891,14 +883,12 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH; moduleConfig.detection_sensor.minimum_broadcast_secs = 45; -#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING moduleConfig.has_ambient_lighting = true; moduleConfig.ambient_lighting.current = 10; // Default to a color based on our node number moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; -#endif initModuleConfigIntervals(); } @@ -1438,25 +1428,15 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) moduleConfig.has_canned_message = true; moduleConfig.has_external_notification = true; moduleConfig.has_mqtt = true; -#if !MESHTASTIC_EXCLUDE_RANGETEST moduleConfig.has_range_test = true; -#endif moduleConfig.has_serial = true; -#if !MESHTASTIC_EXCLUDE_STOREFORWARD moduleConfig.has_store_forward = true; -#endif moduleConfig.has_telemetry = true; moduleConfig.has_neighbor_info = true; moduleConfig.has_detection_sensor = true; -#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING moduleConfig.has_ambient_lighting = true; -#endif -#if !MESHTASTIC_EXCLUDE_AUDIO moduleConfig.has_audio = true; -#endif -#if !MESHTASTIC_EXCLUDE_PAXCOUNTER moduleConfig.has_paxcounter = true; -#endif success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index c6c3415bb..9fb1b589f 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -34,21 +34,6 @@ // Flag to indicate a heartbeat was received and we should send queue status bool heartbeatReceived = false; -// Helper function to skip excluded module configs and advance state -size_t PhoneAPI::skipExcludedModuleConfig(uint8_t *buf) -{ - config_state++; - if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) { - if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) { - state = STATE_SEND_FILEMANIFEST; - } else { - state = STATE_SEND_OTHER_NODEINFOS; - } - config_state = 0; - } - return getFromRadio(buf); -} - PhoneAPI::PhoneAPI() { lastContactMsec = millis(); @@ -370,35 +355,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial; break; case meshtastic_ModuleConfig_external_notification_tag: -#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION) LOG_DEBUG("Send module config: ext notification"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification; break; -#else - LOG_DEBUG("External Notification module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif case meshtastic_ModuleConfig_store_forward_tag: -#if !MESHTASTIC_EXCLUDE_STOREFORWARD LOG_DEBUG("Send module config: store forward"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward; break; -#else - LOG_DEBUG("Store & Forward module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif case meshtastic_ModuleConfig_range_test_tag: -#if !MESHTASTIC_EXCLUDE_RANGETEST LOG_DEBUG("Send module config: range test"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test; break; -#else - LOG_DEBUG("Range Test module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif case meshtastic_ModuleConfig_telemetry_tag: LOG_DEBUG("Send module config: telemetry"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; @@ -410,15 +380,10 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_ModuleConfig_audio_tag: -#if !MESHTASTIC_EXCLUDE_AUDIO LOG_DEBUG("Send module config: audio"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag; fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio; break; -#else - LOG_DEBUG("Audio module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif case meshtastic_ModuleConfig_remote_hardware_tag: LOG_DEBUG("Send module config: remote hardware"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; @@ -435,25 +400,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor; break; case meshtastic_ModuleConfig_ambient_lighting_tag: -#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING LOG_DEBUG("Send module config: ambient lighting"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; break; -#else - LOG_DEBUG("Ambient Lighting module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif case meshtastic_ModuleConfig_paxcounter_tag: -#if !MESHTASTIC_EXCLUDE_PAXCOUNTER LOG_DEBUG("Send module config: paxcounter"); fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; break; -#else - LOG_DEBUG("Paxcounter module excluded from build, skipping"); - return skipExcludedModuleConfig(buf); -#endif default: LOG_ERROR("Unknown module config type %d", config_state); } diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 6b4bb6fc1..0d7772d17 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -172,7 +172,4 @@ class PhoneAPI /// If the mesh service tells us fromNum has changed, tell the phone virtual int onNotify(uint32_t newValue) override; - - /// Helper function to skip excluded module configs and advance state - size_t skipExcludedModuleConfig(uint8_t *buf); }; diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 441a6e12b..79ea7bc0c 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1044,32 +1044,19 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.get_module_config_response.payload_variant.serial = moduleConfig.serial; break; case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: -#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION LOG_INFO("Get module config: External Notification"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification; -#else - LOG_DEBUG("External Notification module excluded from build, skipping config"); -#endif break; case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: -#if !MESHTASTIC_EXCLUDE_STOREFORWARD LOG_INFO("Get module config: Store & Forward"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward; -#else - LOG_DEBUG("Store & Forward module excluded from build, skipping config"); -#endif break; case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: -#if !MESHTASTIC_EXCLUDE_RANGETEST LOG_INFO("Get module config: Range Test"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test; -#else - LOG_DEBUG("Range Test module excluded from build, skipping config"); - // Don't set payload variant - will result in empty response -#endif break; case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: LOG_INFO("Get module config: Telemetry"); @@ -1082,13 +1069,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: -#if !MESHTASTIC_EXCLUDE_AUDIO LOG_INFO("Get module config: Audio"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; res.get_module_config_response.payload_variant.audio = moduleConfig.audio; -#else - LOG_DEBUG("Audio module excluded from build, skipping config"); -#endif break; case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: LOG_INFO("Get module config: Remote Hardware"); @@ -1101,31 +1084,19 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; break; case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: -#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR) LOG_INFO("Get module config: Detection Sensor"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor; -#else - LOG_DEBUG("Detection Sensor module excluded from build, skipping config"); -#endif break; case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: -#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING LOG_INFO("Get module config: Ambient Lighting"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting; -#else - LOG_DEBUG("Ambient Lighting module excluded from build, skipping config"); -#endif break; case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: -#if !MESHTASTIC_EXCLUDE_PAXCOUNTER LOG_INFO("Get module config: Paxcounter"); res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; -#else - LOG_DEBUG("Paxcounter module excluded from build, skipping config"); -#endif break; } From cc579dd0bdb83bf607486e9c365907b9aa4a8804 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 3 Sep 2025 17:50:26 -0500 Subject: [PATCH 46/62] Portduino config refactor (#7796) * Start portduino_config refactor * refactor GPIOs to new portduino_config * More portduino_config work * More conversion to portduino_config * Finish portduino_config transition * trunk * yaml output work * Simplify the GPIO config * Trunk --- .clusterfuzzlite/router_fuzzer.cpp | 4 +- src/DebugConfiguration.cpp | 2 +- src/RedirectablePrint.cpp | 14 +- src/gps/GPS.cpp | 2 +- src/graphics/Screen.cpp | 4 +- src/graphics/TFTDisplay.cpp | 88 +-- src/graphics/tftSetup.cpp | 96 ++-- src/input/LinuxInput.cpp | 4 +- src/input/TouchScreenImpl1.cpp | 2 +- src/input/TrackballInterruptBase.h | 2 +- src/main.cpp | 90 ++- src/mesh/LR11x0Interface.cpp | 6 +- src/mesh/NodeDB.cpp | 6 +- src/mesh/RF95Interface.cpp | 22 +- src/mesh/RadioLibInterface.cpp | 2 +- src/mesh/Router.cpp | 4 +- src/mesh/SX126xInterface.cpp | 18 +- src/mesh/SX128xInterface.cpp | 43 +- src/mesh/raspihttp/PiWebServer.cpp | 10 +- src/modules/Telemetry/HostMetrics.cpp | 10 +- src/platform/portduino/PortduinoGlue.cpp | 534 +++++++----------- src/platform/portduino/PortduinoGlue.h | 518 ++++++++++++++--- src/platform/portduino/architecture.h | 10 +- variants/native/portduino-buildroot/variant.h | 4 +- variants/native/portduino/variant.h | 6 +- 25 files changed, 855 insertions(+), 646 deletions(-) diff --git a/.clusterfuzzlite/router_fuzzer.cpp b/.clusterfuzzlite/router_fuzzer.cpp index bc4d248db..71e88dbff 100644 --- a/.clusterfuzzlite/router_fuzzer.cpp +++ b/.clusterfuzzlite/router_fuzzer.cpp @@ -76,7 +76,7 @@ bool loopCanSleep() // Called just prior to starting Meshtastic. Allows for setting config values before startup. void lateInitVariant() { - settingsMap[logoutputlevel] = level_error; + portduino_config.logoutputlevel = level_error; channelFile.channels[0] = meshtastic_Channel{ .has_settings = true, .settings = @@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht // Start Meshtastic in a thread and wait till it has reached the ON state. int LLVMFuzzerInitialize(int *argc, char ***argv) { - settingsMap[maxtophone] = 5; + portduino_config.maxtophone = 5; meshtasticThread = std::thread([program = *argv[0]]() { char nodeIdStr[12]; diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index 1c081ae29..d65c4f1e8 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess { int result; #ifdef ARCH_PORTDUINO - bool utf = !settingsMap[ascii_logs]; + bool utf = !portduino_config.ascii_logs; #else bool utf = true; #endif diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 1a5fd6e6f..9624a4593 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -58,7 +58,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l #endif #ifdef ARCH_PORTDUINO - bool color = !settingsMap[ascii_logs]; + bool color = !portduino_config.ascii_logs; #else bool color = true; #endif @@ -100,7 +100,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, size_t r = 0; #ifdef ARCH_PORTDUINO - bool color = !settingsMap[ascii_logs]; + bool color = !portduino_config.ascii_logs; #else bool color = true; #endif @@ -299,7 +299,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #if ARCH_PORTDUINO // level trace is special, two possible ways to handle it. if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - if (settingsStrings[traceFilename] != "") { + if (portduino_config.traceFilename != "") { va_list arg; va_start(arg, format); try { @@ -308,18 +308,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) } va_end(arg); } - if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { + if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { delete[] newFormat; return; } } - if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { + if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { delete[] newFormat; return; - } else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { + } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { delete[] newFormat; return; - } else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { + } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { delete[] newFormat; return; } diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index a663f46c4..cc0cfca08 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1440,7 +1440,7 @@ GPS *GPS::createGps() _en_gpio = PIN_GPS_EN; #endif #ifdef ARCH_PORTDUINO - if (!settingsMap[has_gps]) + if (!portduino_config.has_gps) return nullptr; #endif if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0c88ed0a6..007ce56ea 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -340,7 +340,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (settingsMap[displayPanel] != no_screen) { + if (portduino_config.displayPanel != no_screen) { LOG_DEBUG("Make TFTDisplay!"); dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -588,7 +588,7 @@ void Screen::setup() #if ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - if (settingsMap[touchscreenModule]) { + if (portduino_config.touchscreenModule) { touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 37ea9b94a..3eeb17ef0 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -767,24 +767,24 @@ class LGFX : public lgfx::LGFX_Device LGFX(void) { - if (settingsMap[displayPanel] == st7789) + if (portduino_config.displayPanel == st7789) _panel_instance = new lgfx::Panel_ST7789; - else if (settingsMap[displayPanel] == st7735) + else if (portduino_config.displayPanel == st7735) _panel_instance = new lgfx::Panel_ST7735; - else if (settingsMap[displayPanel] == st7735s) + else if (portduino_config.displayPanel == st7735s) _panel_instance = new lgfx::Panel_ST7735S; - else if (settingsMap[displayPanel] == st7796) + else if (portduino_config.displayPanel == st7796) _panel_instance = new lgfx::Panel_ST7796; - else if (settingsMap[displayPanel] == ili9341) + else if (portduino_config.displayPanel == ili9341) _panel_instance = new lgfx::Panel_ILI9341; - else if (settingsMap[displayPanel] == ili9342) + else if (portduino_config.displayPanel == ili9342) _panel_instance = new lgfx::Panel_ILI9342; - else if (settingsMap[displayPanel] == ili9488) + else if (portduino_config.displayPanel == ili9488) _panel_instance = new lgfx::Panel_ILI9488; - else if (settingsMap[displayPanel] == hx8357d) + else if (portduino_config.displayPanel == hx8357d) _panel_instance = new lgfx::Panel_HX8357D; #if defined(LGFX_SDL) - else if (settingsMap[displayPanel] == x11) { + else if (portduino_config.displayPanel == x11) { _panel_instance = new lgfx::Panel_sdl; } #endif @@ -795,61 +795,61 @@ class LGFX : public lgfx::LGFX_Device auto buscfg = _bus_instance.config(); buscfg.spi_mode = 0; - buscfg.spi_host = settingsMap[displayspidev]; + buscfg.spi_host = portduino_config.display_spi_dev_int; - buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable) + buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) _bus_instance.config(buscfg); // applies the set value to the bus. _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]); - cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) - cfg.pin_rst = settingsMap[displayReset]; - if (settingsMap[displayRotate]) { - cfg.panel_width = settingsMap[displayHeight]; // actual displayable width - cfg.panel_height = settingsMap[displayWidth]; // actual displayable height + LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); + cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = portduino_config.displayReset.pin; + if (portduino_config.displayRotate) { + cfg.panel_width = portduino_config.displayHeight; // actual displayable width + cfg.panel_height = portduino_config.displayWidth; // actual displayable height } else { - cfg.panel_width = settingsMap[displayWidth]; // actual displayable width - cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + cfg.panel_width = portduino_config.displayWidth; // actual displayable width + cfg.panel_height = portduino_config.displayHeight; // actual displayable height } - cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction - cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction - cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) - cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed + cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction + cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction + cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed _panel_instance->config(cfg); // Configure settings for touch control. - if (settingsMap[touchscreenModule]) { - if (settingsMap[touchscreenModule] == xpt2046) { + if (portduino_config.touchscreenModule) { + if (portduino_config.touchscreenModule == xpt2046) { _touch_instance = new lgfx::Touch_XPT2046; - } else if (settingsMap[touchscreenModule] == stmpe610) { + } else if (portduino_config.touchscreenModule == stmpe610) { _touch_instance = new lgfx::Touch_STMPE610; - } else if (settingsMap[touchscreenModule] == ft5x06) { + } else if (portduino_config.touchscreenModule == ft5x06) { _touch_instance = new lgfx::Touch_FT5x06; } auto touch_cfg = _touch_instance->config(); - touch_cfg.pin_cs = settingsMap[touchscreenCS]; + touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; touch_cfg.x_min = 0; - touch_cfg.x_max = settingsMap[displayHeight] - 1; + touch_cfg.x_max = portduino_config.displayHeight - 1; touch_cfg.y_min = 0; - touch_cfg.y_max = settingsMap[displayWidth] - 1; - touch_cfg.pin_int = settingsMap[touchscreenIRQ]; + touch_cfg.y_max = portduino_config.displayWidth - 1; + touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; touch_cfg.bus_shared = true; - touch_cfg.offset_rotation = settingsMap[touchscreenRotate]; - if (settingsMap[touchscreenI2CAddr] != -1) { - touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr]; + touch_cfg.offset_rotation = portduino_config.touchscreenRotate; + if (portduino_config.touchscreenI2CAddr != -1) { + touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; } else { - touch_cfg.spi_host = settingsMap[touchscreenspidev]; + touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; } _touch_instance->config(touch_cfg); _panel_instance->setTouch(_touch_instance); } #if defined(LGFX_SDL) - if (settingsMap[displayPanel] == x11) { + if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; sdl_panel_->setup(); sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); @@ -1115,10 +1115,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g backlightEnable = p; #if ARCH_PORTDUINO - if (settingsMap[displayRotate]) { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + if (portduino_config.displayRotate) { + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); } else { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); + setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); } #elif defined(SCREEN_ROTATE) @@ -1240,7 +1240,7 @@ void TFTDisplay::sdlLoop() #if defined(LGFX_SDL) static int lastPressed = 0; static int shuttingDown = false; - if (settingsMap[displayPanel] == x11) { + if (portduino_config.displayPanel == x11) { lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; if (sdl_panel_->loop() && !shuttingDown) { LOG_WARN("Window Closed!"); @@ -1288,8 +1288,8 @@ void TFTDisplay::sendCommand(uint8_t com) backlightEnable->set(true); #if ARCH_PORTDUINO display(true); - if (settingsMap[displayBacklight] > 0) - digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->wakeup(); tft->powerSaveOff(); @@ -1312,8 +1312,8 @@ void TFTDisplay::sendCommand(uint8_t com) backlightEnable->set(false); #if ARCH_PORTDUINO tft->clear(); - if (settingsMap[displayBacklight] > 0) - digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON); + if (portduino_config.displayBacklight.pin > 0) + digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->sleep(); tft->powerSaveOn(); diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index b2e92bdae..5654fa02a 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -41,78 +41,78 @@ void tftSetup(void) PacketAPI::create(PacketServer::init()); deviceScreen->init(new PacketClient); #else - if (settingsMap[displayPanel] != no_screen) { + if (portduino_config.displayPanel != no_screen) { DisplayDriverConfig displayConfig; static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S", "ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"}; static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"}; #if defined(USE_X11) - if (settingsMap[displayPanel] == x11) { - if (settingsMap[displayWidth] && settingsMap[displayHeight]) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth], - (uint16_t)settingsMap[displayHeight]); + if (portduino_config.displayPanel == x11) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::X11); } else #elif defined(USE_FRAMEBUFFER) - if (settingsMap[displayPanel] == fb) { - if (settingsMap[displayWidth] && settingsMap[displayHeight]) - displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth], - (uint16_t)settingsMap[displayHeight]); + if (portduino_config.displayPanel == fb) { + if (portduino_config.displayWidth && portduino_config.displayHeight) + displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth, + (uint16_t)portduino_config.displayHeight); else displayConfig.device(DisplayDriverConfig::device_t::FB); } else #endif { displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT) - .panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]], - .panel_width = (uint16_t)settingsMap[displayWidth], - .panel_height = (uint16_t)settingsMap[displayHeight], - .rotation = (bool)settingsMap[displayRotate], - .pin_cs = (int16_t)settingsMap[displayCS], - .pin_rst = (int16_t)settingsMap[displayReset], - .offset_x = (uint16_t)settingsMap[displayOffsetX], - .offset_y = (uint16_t)settingsMap[displayOffsetY], - .offset_rotation = (uint8_t)settingsMap[displayOffsetRotate], - .invert = settingsMap[displayInvert] ? true : false, - .rgb_order = (bool)settingsMap[displayRGBOrder], - .dlen_16bit = settingsMap[displayPanel] == ili9486 || - settingsMap[displayPanel] == ili9488}) - .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency], + .panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel], + .panel_width = (uint16_t)portduino_config.displayWidth, + .panel_height = (uint16_t)portduino_config.displayHeight, + .rotation = (bool)portduino_config.displayRotate, + .pin_cs = (int16_t)portduino_config.displayCS.pin, + .pin_rst = (int16_t)portduino_config.displayReset.pin, + .offset_x = (uint16_t)portduino_config.displayOffsetX, + .offset_y = (uint16_t)portduino_config.displayOffsetY, + .offset_rotation = (uint8_t)portduino_config.displayOffsetRotate, + .invert = portduino_config.displayInvert ? true : false, + .rgb_order = (bool)portduino_config.displayRGBOrder, + .dlen_16bit = portduino_config.displayPanel == ili9486 || + portduino_config.displayPanel == ili9488}) + .bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency, .freq_read = 16000000, - .spi{.pin_dc = (int8_t)settingsMap[displayDC], + .spi{.pin_dc = (int8_t)portduino_config.displayDC.pin, .use_lock = true, - .spi_host = (uint16_t)settingsMap[displayspidev]}}) - .input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice], - .pointerDevice = settingsStrings[pointerDevice]}) - .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight], - .pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel], - .invert = (bool)settingsMap[displayBacklightInvert]}); - if (settingsMap[touchscreenI2CAddr] == -1) { + .spi_host = (uint16_t)portduino_config.display_spi_dev_int}}) + .input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice, + .pointerDevice = portduino_config.pointerDevice}) + .light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin, + .pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin, + .invert = (bool)portduino_config.displayBacklightInvert}); + if (portduino_config.touchscreenI2CAddr == -1) { displayConfig.touch( - DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]], - .freq = (uint32_t)settingsMap[touchscreenBusFrequency], - .pin_int = (int16_t)settingsMap[touchscreenIRQ], - .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], + DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, .spi{ - .spi_host = (int8_t)settingsMap[touchscreenspidev], + .spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int, }, - .pin_cs = (int16_t)settingsMap[touchscreenCS]}); + .pin_cs = (int16_t)portduino_config.touchscreenCS.pin}); } else { displayConfig.touch(DisplayDriverConfig::touch_config_t{ - .type = touch[settingsMap[touchscreenModule]], - .freq = (uint32_t)settingsMap[touchscreenBusFrequency], + .type = touch[portduino_config.touchscreenModule], + .freq = (uint32_t)portduino_config.touchscreenBusFrequency, .x_min = 0, - .x_max = - (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) - - 1), + .x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth + : portduino_config.displayHeight) - + 1), .y_min = 0, - .y_max = - (int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) - - 1), - .pin_int = (int16_t)settingsMap[touchscreenIRQ], - .offset_rotation = (uint8_t)settingsMap[touchscreenRotate], - .i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}}); + .y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight + : portduino_config.displayWidth) - + 1), + .pin_int = (int16_t)portduino_config.touchscreenIRQ.pin, + .offset_rotation = (uint8_t)portduino_config.touchscreenRotate, + .i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}}); } } deviceScreen = &DeviceScreen::create(&displayConfig); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 7620080f0..fee7c8ded 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -33,9 +33,9 @@ int32_t LinuxInput::runOnce() { if (firstTime) { - if (settingsStrings[keyboardDevice] == "") + if (portduino_config.keyboardDevice == "") return disable(); - fd = open(settingsStrings[keyboardDevice].c_str(), O_RDWR); + fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR); if (fd < 0) return disable(); ret = ioctl(fd, EVIOCGRAB, (void *)1); diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 50a70d66e..69dcab04e 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -18,7 +18,7 @@ TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTo void TouchScreenImpl1::init() { #if ARCH_PORTDUINO - if (settingsMap[touchscreenModule]) { + if (portduino_config.touchscreenModule) { TouchScreenBase::init(true); inputBroker->registerSource(this); } else { diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 92db8720e..4991cabe2 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -6,7 +6,7 @@ #ifndef TB_DIRECTION #if ARCH_PORTDUINO #include "PortduinoGlue.h" -#define TB_DIRECTION (PinStatus) settingsMap[tbDirection] +#define TB_DIRECTION (PinStatus) portduino_config.lora_usb_vid #else #define TB_DIRECTION RISING #endif diff --git a/src/main.cpp b/src/main.cpp index d7e866a2a..107944dd5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -387,7 +387,7 @@ void setup() #endif concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO - SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0); + SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif @@ -532,9 +532,9 @@ void setup() #elif defined(I2C_SDA) && !defined(ARCH_RP2040) Wire.begin(I2C_SDA, I2C_SCL); #elif defined(ARCH_PORTDUINO) - if (settingsStrings[i2cdev] != "") { - LOG_INFO("Use %s as I2C device", settingsStrings[i2cdev].c_str()); - Wire.begin(settingsStrings[i2cdev].c_str()); + if (portduino_config.i2cdev != "") { + LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str()); + Wire.begin(portduino_config.i2cdev.c_str()); } else { LOG_INFO("No I2C device configured, Skip"); } @@ -586,7 +586,7 @@ void setup() #if defined(I2C_SDA) i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); #elif defined(ARCH_PORTDUINO) - if (settingsStrings[i2cdev] != "") { + if (portduino_config.i2cdev != "") { LOG_INFO("Scan for i2c devices"); i2cScanner->scanPort(ScanI2C::I2CPort::WIRE); } @@ -859,7 +859,7 @@ void setup() SPI.begin(false); #endif // HW_SPI1_DEVICE #elif ARCH_PORTDUINO - if (settingsStrings[spidev] != "ch341") { + if (portduino_config.lora_spi_dev != "ch341") { SPI.begin(); } #elif !defined(ARCH_ESP32) // ARCH_RP2040 @@ -886,7 +886,7 @@ void setup() defined(USE_SPISSD1306) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen = new graphics::Screen(screen_found, screen_model, screen_geometry); } @@ -987,13 +987,13 @@ void setup() #endif #if defined(ARCH_PORTDUINO) - if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + if (portduino_config.userButtonPin.enabled) { - LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]); + LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin); UserButtonThread = new ButtonThread("UserButton"); if (screen) { ButtonConfig config; - config.pinNumber = (uint8_t)settingsMap[userButtonPin]; + config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin; config.activeLow = true; config.activePullup = true; config.pullupSense = INPUT_PULLUP; @@ -1151,7 +1151,7 @@ void setup() if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } @@ -1167,15 +1167,10 @@ void setup() #endif #ifdef ARCH_PORTDUINO - const struct { - configNames cfgName; - std::string strName; - } loraModules[] = {{use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"}, - {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; // as one can't use a function pointer to the class constructor: - auto loraModuleInterface = [](configNames cfgName, LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, - RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) { - switch (cfgName) { + auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) { + switch (portduino_config.lora_module) { case use_rf95: return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); case use_sx1262: @@ -1192,31 +1187,34 @@ void setup() return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); case use_llcc68: return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + case use_simradio: + return (RadioInterface *)new SimRadio; default: assert(0); // shouldn't happen return (RadioInterface *)nullptr; } }; - for (auto &loraModule : loraModules) { - if (settingsMap[loraModule.cfgName] && !rIf) { - LOG_DEBUG("Activate %s radio on SPI port %s", loraModule.strName.c_str(), settingsStrings[spidev].c_str()); - if (settingsStrings[spidev] == "ch341") { - RadioLibHAL = ch341Hal; - } else { - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); - } - rIf = loraModuleInterface(loraModule.cfgName, (LockingArduinoHal *)RadioLibHAL, settingsMap[cs_pin], - settingsMap[irq_pin], settingsMap[reset_pin], settingsMap[busy_pin]); - if (!rIf->init()) { - LOG_WARN("No %s radio", loraModule.strName.c_str()); - delete rIf; - rIf = NULL; - exit(EXIT_FAILURE); - } else { - LOG_INFO("%s init success", loraModule.strName.c_str()); - } - } + + LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(), + portduino_config.lora_spi_dev.c_str()); + if (portduino_config.lora_spi_dev == "ch341") { + RadioLibHAL = ch341Hal; + } else { + RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); } + rIf = + loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, + portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin); + + if (!rIf->init()) { + LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); + delete rIf; + rIf = NULL; + exit(EXIT_FAILURE); + } else { + LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); + } + #elif defined(HW_SPI1_DEVICE) LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); #else // HW_SPI1_DEVICE @@ -1238,20 +1236,6 @@ void setup() } #endif -#if defined(ARCH_PORTDUINO) - if (!rIf) { - rIf = new SimRadio; - if (!rIf->init()) { - LOG_WARN("No simulated radio"); - delete rIf; - rIf = NULL; - } else { - LOG_INFO("Use SIMULATED radio!"); - radioType = SIM_RADIO; - } - } -#endif - #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); @@ -1462,7 +1446,7 @@ void setup() #ifdef ARCH_PORTDUINO #if __has_include() - if (settingsMap[webserverport] != -1) { + if (portduino_config.webserverport != -1) { piwebServerThread = new PiWebServerThread(); std::atexit([] { delete piwebServerThread; }); } diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a0d992c42..f83522c8b 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -21,7 +21,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR11x0 power config forgotten) #if ARCH_PORTDUINO -#define LR1110_MAX_POWER settingsMap[lr1110_max_power] +#define LR1110_MAX_POWER portduino_config.lr1110_max_power #endif #ifndef LR1110_MAX_POWER #define LR1110_MAX_POWER 22 @@ -30,7 +30,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = { // the 2.4G part maxes at 13dBm #if ARCH_PORTDUINO -#define LR1120_MAX_POWER settingsMap[lr1120_max_power] +#define LR1120_MAX_POWER portduino_config.lr1120_max_power #endif #ifndef LR1120_MAX_POWER #define LR1120_MAX_POWER 13 @@ -55,7 +55,7 @@ template bool LR11x0Interface::init() #endif #if ARCH_PORTDUINO - float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; // FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE #elif !defined(LR11X0_DIO3_TCXO_VOLTAGE) float tcxoVoltage = diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 65d2f4d1c..237b4286c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -673,7 +673,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #endif #elif ARCH_PORTDUINO bool hasScreen = false; - if (settingsMap[displayPanel]) + if (portduino_config.displayPanel) hasScreen = true; else hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; @@ -1334,8 +1334,8 @@ void NodeDB::loadFromDisk() } #if ARCH_PORTDUINO // set any config overrides - if (settingsMap[has_configDisplayMode]) { - config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)settingsMap[configDisplayMode]; + if (portduino_config.has_configDisplayMode) { + config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } #endif diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 97f21fc34..0f32f3427 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -10,7 +10,7 @@ #endif #if ARCH_PORTDUINO -#define RF95_MAX_POWER settingsMap[rf95_max_power] +#define RF95_MAX_POWER portduino_config.rf95_max_power #endif #ifndef RF95_MAX_POWER #define RF95_MAX_POWER 20 @@ -94,16 +94,16 @@ void RF95Interface::setTransmitEnable(bool txon) #ifdef RF95_TXEN digitalWrite(RF95_TXEN, txon ? 1 : 0); #elif ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], txon ? 1 : 0); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0); } #endif #ifdef RF95_RXEN digitalWrite(RF95_RXEN, txon ? 0 : 1); #elif ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], txon ? 0 : 1); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1); } #endif } @@ -164,13 +164,13 @@ bool RF95Interface::init() digitalWrite(RF95_RXEN, 1); #endif #if ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[txen_pin], OUTPUT); - digitalWrite(settingsMap[txen_pin], 0); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, 0); } - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[rxen_pin], OUTPUT); - digitalWrite(settingsMap[rxen_pin], 0); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, 0); } #endif setTransmitEnable(false); diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 56341adf9..0db101ce6 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -417,7 +417,7 @@ void RadioLibInterface::handleReceiveInterrupt() int state = iface->readData((uint8_t *)&radioBuffer, length); #if ARCH_PORTDUINO - if (settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.logoutputlevel == level_trace) { printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length); } #endif diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 8bfa7ae9f..09fb079c5 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -490,7 +490,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) #if ENABLE_JSON_LOGGING LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); #elif ARCH_PORTDUINO - if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); } #endif @@ -730,7 +730,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); #elif ARCH_PORTDUINO // Even ignored packets get logged in the trace - if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) { + if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str()); } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 729c1abc6..49dc562d4 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -12,7 +12,7 @@ // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and SX126x power config forgotten) #if ARCH_PORTDUINO -#define SX126X_MAX_POWER settingsMap[sx126x_max_power] +#define SX126X_MAX_POWER portduino_config.sx126x_max_power #endif #ifndef SX126X_MAX_POWER #define SX126X_MAX_POWER 22 @@ -53,10 +53,10 @@ template bool SX126xInterface::init() #endif #if ARCH_PORTDUINO - tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000; - if (settingsMap[sx126x_ant_sw_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[sx126x_ant_sw_pin], HIGH); - pinMode(settingsMap[sx126x_ant_sw_pin], OUTPUT); + tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; + if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH); + pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT); } #endif if (tcxoVoltage == 0.0) @@ -98,7 +98,7 @@ template bool SX126xInterface::init() bool dio2AsRfSwitch = true; #elif defined(ARCH_PORTDUINO) bool dio2AsRfSwitch = false; - if (settingsMap[dio2_as_rf_switch]) { + if (portduino_config.dio2_as_rf_switch) { dio2AsRfSwitch = true; } #else @@ -112,9 +112,9 @@ template bool SX126xInterface::init() // no effect #if ARCH_PORTDUINO if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", settingsMap[rxen_pin], - settingsMap[txen_pin]); - lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]); + LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin, + portduino_config.lora_txen_pin.pin); + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #else #ifndef SX126X_RXEN diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 866426872..cbc98eeb1 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -11,7 +11,7 @@ // Particular boards might define a different max power based on what their hardware can do #if ARCH_PORTDUINO -#define SX128X_MAX_POWER settingsMap[sx128x_max_power] +#define SX128X_MAX_POWER portduino_config.sx128x_max_power #endif #ifndef SX128X_MAX_POWER #define SX128X_MAX_POWER 13 @@ -41,13 +41,13 @@ template bool SX128xInterface::init() #endif #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[rxen_pin], OUTPUT); - digitalWrite(settingsMap[rxen_pin], LOW); // Set low before becoming an output + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - pinMode(settingsMap[txen_pin], OUTPUT); - digitalWrite(settingsMap[txen_pin], LOW); // Set low before becoming an output + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + pinMode(portduino_config.lora_txen_pin.pin, OUTPUT); + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode @@ -93,8 +93,9 @@ template bool SX128xInterface::init() lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN); } #elif ARCH_PORTDUINO - if (res == RADIOLIB_ERR_NONE && settingsMap[rxen_pin] != RADIOLIB_NC && settingsMap[txen_pin] != RADIOLIB_NC) { - lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]); + if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC && + portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin); } #endif @@ -174,11 +175,11 @@ template void SX128xInterface::setStandby() LOG_ERROR("SX128x standby %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], LOW); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], LOW); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else #if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power @@ -210,11 +211,11 @@ template void SX128xInterface::addReceiveMetadata(meshtastic_Mes template void SX128xInterface::configHardwareForSend() { #if ARCH_PORTDUINO - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], HIGH); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, HIGH); } - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], LOW); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); } #else @@ -241,11 +242,11 @@ template void SX128xInterface::startReceive() setStandby(); #if ARCH_PORTDUINO - if (settingsMap[rxen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[rxen_pin], HIGH); + if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH); } - if (settingsMap[txen_pin] != RADIOLIB_NC) { - digitalWrite(settingsMap[txen_pin], LOW); + if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) { + digitalWrite(portduino_config.lora_txen_pin.pin, LOW); } #else diff --git a/src/mesh/raspihttp/PiWebServer.cpp b/src/mesh/raspihttp/PiWebServer.cpp index 7d3542e83..3e9dbe8c2 100644 --- a/src/mesh/raspihttp/PiWebServer.cpp +++ b/src/mesh/raspihttp/PiWebServer.cpp @@ -65,8 +65,8 @@ mail: marchammermann@googlemail.com #define DEFAULT_REALM "default_realm" #define PREFIX "" -#define KEY_PATH settingsStrings[websslkeypath].c_str() -#define CERT_PATH settingsStrings[websslcertpath].c_str() +#define KEY_PATH portduino_config.webserver_ssl_key_path.c_str() +#define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str() struct _file_config configWeb; @@ -458,8 +458,8 @@ PiWebServerThread::PiWebServerThread() } } - if (settingsMap[webserverport] != 0) { - webservport = settingsMap[webserverport]; + if (portduino_config.webserverport != 0) { + webservport = portduino_config.webserverport; LOG_INFO("Use webserver port from yaml config %i ", webservport); } else { LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443"); @@ -490,7 +490,7 @@ PiWebServerThread::PiWebServerThread() u_map_put(&configWeb.mime_types, ".ico", "image/x-icon"); u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml"); - webrootpath = settingsStrings[webserverrootpath]; + webrootpath = portduino_config.webserver_root_path; configWeb.files_path = (char *)webrootpath.c_str(); configWeb.url_prefix = ""; diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index 8f10b9228..dcde495a2 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -9,11 +9,11 @@ int32_t HostMetricsModule::runOnce() { #if ARCH_PORTDUINO - if (settingsMap[hostMetrics_interval] == 0) { + if (portduino_config.hostMetrics_interval == 0) { return disable(); } else { sendMetrics(); - return 60 * 1000 * settingsMap[hostMetrics_interval]; + return 60 * 1000 * portduino_config.hostMetrics_interval; } #else return disable(); @@ -110,8 +110,8 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics() proc_loadavg.close(); } } - if (settingsStrings[hostMetrics_user_command] != "") { - std::string userCommandResult = exec(settingsStrings[hostMetrics_user_command].c_str()); + if (portduino_config.hostMetrics_user_command != "") { + std::string userCommandResult = exec(portduino_config.hostMetrics_user_command.c_str()); if (userCommandResult.length() > 1) { strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; @@ -135,7 +135,7 @@ bool HostMetricsModule::sendMetrics() p->to = NODENUM_BROADCAST; p->decoded.want_response = false; p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - p->channel = settingsMap[hostMetrics_channel]; + p->channel = portduino_config.hostMetrics_channel; LOG_INFO("Send packet to mesh"); service->sendToMesh(p, RX_SRC_LOCAL, true); return true; diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 929a45d09..b11d2547b 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -9,7 +9,6 @@ #include "api/ServerAPI.h" #include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" -#include "yaml-cpp/yaml.h" #include #include #include @@ -28,14 +27,13 @@ #include "platform/portduino/USBHal.h" -std::map settingsMap; -std::map settingsStrings; portduino_config_struct portduino_config; std::ofstream traceFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; bool verboseEnabled = false; +bool yamlOnly = false; const char *argp_program_version = optstr(APP_VERSION); @@ -75,6 +73,9 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) case 'v': verboseEnabled = true; break; + case 'y': + yamlOnly = true; + break; case ARGP_KEY_ARG: return 0; default: @@ -90,6 +91,7 @@ void portduinoCustomInit() {"hwid", 'h', "HWID", 0, "The mac address to assign to this virtual machine"}, {"sim", 's', 0, 0, "Run in Simulated radio mode"}, {"verbose", 'v', 0, 0, "Set log level to full debug"}, + {"output-yaml", 'y', 0, 0, "Output config yaml and exit"}, {0}}; static void *childArguments; static char doc[] = "Meshtastic native build."; @@ -115,8 +117,8 @@ void getMacAddr(uint8_t *dmac) dmac[4] = hwId >> 8; dmac[5] = hwId & 0xff; } - } else if (settingsStrings[mac_address].length() > 11) { - MAC_from_string(settingsStrings[mac_address], dmac); + } else if (portduino_config.mac_address.length() > 11) { + MAC_from_string(portduino_config.mac_address, dmac); exit; } else { @@ -148,89 +150,46 @@ void getMacAddr(uint8_t *dmac) */ void portduinoSetup() { - printf("Set up Meshtastic on Portduino...\n"); int max_GPIO = 0; - const configNames GPIO_lines[] = {cs_pin, - irq_pin, - busy_pin, - reset_pin, - sx126x_ant_sw_pin, - txen_pin, - rxen_pin, - displayDC, - displayCS, - displayBacklight, - displayBacklightPWMChannel, - displayReset, - touchscreenCS, - touchscreenIRQ, - userButtonPin, - tbUpPin, - tbDownPin, - tbLeftPin, - tbRightPin, - tbPressPin}; - std::string gpioChipName = "gpiochip"; - settingsStrings[i2cdev] = ""; - settingsStrings[keyboardDevice] = ""; - settingsStrings[pointerDevice] = ""; - settingsStrings[webserverrootpath] = ""; - settingsStrings[spidev] = ""; - settingsStrings[displayspidev] = ""; - settingsMap[spiSpeed] = 2000000; - settingsMap[ascii_logs] = !isatty(1); - settingsMap[displayPanel] = no_screen; - settingsMap[touchscreenModule] = no_touchscreen; - settingsMap[tbUpPin] = RADIOLIB_NC; - settingsMap[tbDownPin] = RADIOLIB_NC; - settingsMap[tbLeftPin] = RADIOLIB_NC; - settingsMap[tbRightPin] = RADIOLIB_NC; - settingsMap[tbPressPin] = RADIOLIB_NC; - - YAML::Node yamlConfig; + portduino_config.displayPanel = no_screen; if (portduino_config.force_simradio == true) { - settingsMap[use_simradio] = true; + portduino_config.lora_module = use_simradio; } else if (configPath != nullptr) { if (loadConfig(configPath)) { - std::cout << "Using " << configPath << " as config file" << std::endl; + if (!yamlOnly) + std::cout << "Using " << configPath << " as config file" << std::endl; } else { std::cout << "Unable to use " << configPath << " as config file" << std::endl; exit(EXIT_FAILURE); } } else if (access("config.yaml", R_OK) == 0) { if (loadConfig("config.yaml")) { - std::cout << "Using local config.yaml as config file" << std::endl; + if (!yamlOnly) + std::cout << "Using local config.yaml as config file" << std::endl; } else { std::cout << "Unable to use local config.yaml as config file" << std::endl; exit(EXIT_FAILURE); } } else if (access("/etc/meshtasticd/config.yaml", R_OK) == 0) { if (loadConfig("/etc/meshtasticd/config.yaml")) { - std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; + if (!yamlOnly) + std::cout << "Using /etc/meshtasticd/config.yaml as config file" << std::endl; } else { std::cout << "Unable to use /etc/meshtasticd/config.yaml as config file" << std::endl; exit(EXIT_FAILURE); } } else { - std::cout << "No 'config.yaml' found..." << std::endl; - settingsMap[use_simradio] = true; + if (!yamlOnly) + std::cout << "No 'config.yaml' found..." << std::endl; + portduino_config.lora_module = use_simradio; } - if (settingsMap[use_simradio] == true) { - std::cout << "Running in simulated mode." << std::endl; - settingsMap[maxnodes] = 200; // Default to 200 nodes - settingsMap[logoutputlevel] = level_debug; // Default to debug - // Set the random seed equal to TCPPort to have a different seed per instance - randomSeed(TCPPort); - return; - } - - if (settingsStrings[config_directory] != "") { + if (portduino_config.config_directory != "") { std::string filetype = ".yaml"; for (const std::filesystem::directory_entry &entry : - std::filesystem::directory_iterator{settingsStrings[config_directory]}) { + std::filesystem::directory_iterator{portduino_config.config_directory}) { if (ends_with(entry.path().string(), ".yaml")) { std::cout << "Also using " << entry << " as additional config file" << std::endl; loadConfig(entry.path().c_str()); @@ -238,15 +197,28 @@ void portduinoSetup() } } + if (yamlOnly) { + std::cout << portduino_config.emit_yaml() << std::endl; + exit(EXIT_SUCCESS); + } + + if (portduino_config.lora_module == use_simradio) { + std::cout << "Running in simulated mode." << std::endl; + portduino_config.MaxNodes = 200; // Default to 200 nodes + // Set the random seed equal to TCPPort to have a different seed per instance + randomSeed(TCPPort); + return; + } + // If LoRa `Module: auto` (default in config.yaml), // attempt to auto config based on Product Strings - if (settingsMap[use_autoconf] == true) { + if (portduino_config.lora_module == use_autoconf) { char autoconf_product[96] = {0}; // Try CH341 try { std::cout << "autoconf: Looking for CH341 device..." << std::endl; - ch341Hal = - new Ch341Hal(0, settingsStrings[lora_usb_serial_num], settingsMap[lora_usb_vid], settingsMap[lora_usb_pid]); + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); ch341Hal->getProductString(autoconf_product, 95); delete ch341Hal; std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl; @@ -323,7 +295,7 @@ void portduinoSetup() if (mac_start != nullptr) { std::cout << "autoconf: Found mac data " << mac_start << std::endl; if (strlen(mac_start) == 12) - settingsStrings[mac_address] = std::string(mac_start); + portduino_config.mac_address = std::string(mac_start); } if (devID_start != nullptr) { std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; @@ -354,7 +326,7 @@ void portduinoSetup() std::cerr << "autoconf: Unable to find config for " << autoconf_product << std::endl; exit(EXIT_FAILURE); } - if (loadConfig((settingsStrings[available_directory] + product_config).c_str())) { + if (loadConfig((portduino_config.available_directory + product_config).c_str())) { std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl; } else { std::cerr << "autoconf: Unable to use " << product_config << " as config file for " << autoconf_product @@ -363,15 +335,16 @@ void portduinoSetup() } } else { std::cerr << "autoconf: Could not locate any devices" << std::endl; + exit(EXIT_FAILURE); } } // if we're using a usermode driver, we need to initialize it here, to get a serial number back for mac address uint8_t dmac[6] = {0}; - if (settingsStrings[spidev] == "ch341") { + if (portduino_config.lora_spi_dev == "ch341") { try { - ch341Hal = - new Ch341Hal(0, settingsStrings[lora_usb_serial_num], settingsMap[lora_usb_vid], settingsMap[lora_usb_pid]); + ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid, + portduino_config.lora_usb_pid); } catch (std::exception &e) { std::cerr << e.what() << std::endl; std::cerr << "Could not initialize CH341 device!" << std::endl; @@ -383,7 +356,7 @@ void portduinoSetup() char product_string[96] = {0}; ch341Hal->getProductString(product_string, 95); std::cout << "CH341 Product " << product_string << std::endl; - if (strlen(serial) == 8 && settingsStrings[mac_address].length() < 12) { + if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) { uint8_t hash[32] = {0}; memcpy(hash, serial, 8); crypto->hash(hash, 8); @@ -395,7 +368,7 @@ void portduinoSetup() dmac[5] = hash[5]; char macBuf[13] = {0}; sprintf(macBuf, "%02X%02X%02X%02X%02X%02X", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); - settingsStrings[mac_address] = macBuf; + portduino_config.mac_address = macBuf; } } @@ -409,100 +382,38 @@ void portduinoSetup() // Rather important to set this, if not running simulated. randomSeed(time(NULL)); - std::string defaultGpioChipName = gpioChipName + std::to_string(settingsMap[default_gpiochip]); - - for (configNames i : GPIO_lines) { - if (settingsMap.count(i) && settingsMap[i] > max_GPIO) - max_GPIO = settingsMap[i]; + std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); + for (auto i : portduino_config.all_pins) { + if (i->enabled && i->pin > max_GPIO) + max_GPIO = i->pin; } gpioInit(max_GPIO + 1); // Done here so we can inform Portduino how many GPIOs we need. // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate - if (settingsMap.count(userButtonPin) > 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[userButtonPin], defaultGpioChipName, settingsMap[userButtonPin]) != ERRNO_OK) { - settingsMap[userButtonPin] = RADIOLIB_NC; - } - } - if (settingsMap.count(tbUpPin) > 0 && settingsMap[tbUpPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[tbUpPin], defaultGpioChipName, settingsMap[tbUpPin]) != ERRNO_OK) { - settingsMap[tbUpPin] = RADIOLIB_NC; - } - } - if (settingsMap.count(tbDownPin) > 0 && settingsMap[tbDownPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[tbDownPin], defaultGpioChipName, settingsMap[tbDownPin]) != ERRNO_OK) { - settingsMap[tbDownPin] = RADIOLIB_NC; - } - } - if (settingsMap.count(tbLeftPin) > 0 && settingsMap[tbLeftPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[tbLeftPin], defaultGpioChipName, settingsMap[tbLeftPin]) != ERRNO_OK) { - settingsMap[tbLeftPin] = RADIOLIB_NC; - } - } - if (settingsMap.count(tbRightPin) > 0 && settingsMap[tbRightPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[tbRightPin], defaultGpioChipName, settingsMap[tbRightPin]) != ERRNO_OK) { - settingsMap[tbRightPin] = RADIOLIB_NC; - } - } - if (settingsMap.count(tbPressPin) > 0 && settingsMap[tbPressPin] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[tbPressPin], defaultGpioChipName, settingsMap[tbPressPin]) != ERRNO_OK) { - settingsMap[tbPressPin] = RADIOLIB_NC; - } - } - if (settingsMap[displayPanel] != no_screen) { - if (settingsMap[displayCS] > 0) - initGPIOPin(settingsMap[displayCS], defaultGpioChipName, settingsMap[displayCS]); - if (settingsMap[displayDC] > 0) - initGPIOPin(settingsMap[displayDC], defaultGpioChipName, settingsMap[displayDC]); - if (settingsMap[displayBacklight] > 0) - initGPIOPin(settingsMap[displayBacklight], defaultGpioChipName, settingsMap[displayBacklight]); - if (settingsMap[displayReset] > 0) - initGPIOPin(settingsMap[displayReset], defaultGpioChipName, settingsMap[displayReset]); - } - if (settingsMap[touchscreenModule] != no_touchscreen) { - if (settingsMap[touchscreenCS] > 0) - initGPIOPin(settingsMap[touchscreenCS], defaultGpioChipName, settingsMap[touchscreenCS]); - if (settingsMap[touchscreenIRQ] > 0) - initGPIOPin(settingsMap[touchscreenIRQ], defaultGpioChipName, settingsMap[touchscreenIRQ]); + for (auto i : portduino_config.all_pins) { + if (i->enabled) + if (initGPIOPin(i->pin, gpioChipName + std::to_string(i->gpiochip), i->line) != ERRNO_OK) { + printf("Error setting pin number %d. It may not exist, or may already be in use.\n", i->line); + exit(EXIT_FAILURE); + } } // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware - if (settingsStrings[spidev] != "" && settingsStrings[spidev] != "ch341") { - const struct { - configNames pin; - configNames gpiochip; - configNames line; - } pinMappings[] = {{cs_pin, cs_gpiochip, cs_line}, - {irq_pin, irq_gpiochip, irq_line}, - {busy_pin, busy_gpiochip, busy_line}, - {reset_pin, reset_gpiochip, reset_line}, - {rxen_pin, rxen_gpiochip, rxen_line}, - {txen_pin, txen_gpiochip, txen_line}, - {sx126x_ant_sw_pin, sx126x_ant_sw_gpiochip, sx126x_ant_sw_line}}; - for (auto &pinMap : pinMappings) { - auto setMapIter = settingsMap.find(pinMap.pin); - if (setMapIter != settingsMap.end() && setMapIter->second != RADIOLIB_NC) { - if (initGPIOPin(setMapIter->second, gpioChipName + std::to_string(settingsMap[pinMap.gpiochip]), - settingsMap[pinMap.line]) != ERRNO_OK) { - printf("Error setting pin number %d. It may not exist, or may already be in use.\n", - settingsMap[pinMap.line]); - exit(EXIT_FAILURE); - } - } - } - SPI.begin(settingsStrings[spidev].c_str()); + if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { + SPI.begin(portduino_config.lora_spi_dev.c_str()); } - if (settingsStrings[traceFilename] != "") { + if (portduino_config.traceFilename != "") { try { - traceFile.open(settingsStrings[traceFilename], std::ios::out | std::ios::app); + traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); } catch (std::ofstream::failure &e) { std::cout << "*** traceFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); } } - if (verboseEnabled && settingsMap[logoutputlevel] != level_trace) { - settingsMap[logoutputlevel] = level_debug; + if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { + portduino_config.logoutputlevel = level_debug; } return; @@ -537,99 +448,78 @@ bool loadConfig(const char *configPath) yamlConfig = YAML::LoadFile(configPath); if (yamlConfig["Logging"]) { if (yamlConfig["Logging"]["LogLevel"].as("info") == "trace") { - settingsMap[logoutputlevel] = level_trace; + portduino_config.logoutputlevel = level_trace; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "debug") { - settingsMap[logoutputlevel] = level_debug; + portduino_config.logoutputlevel = level_debug; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "info") { - settingsMap[logoutputlevel] = level_info; + portduino_config.logoutputlevel = level_info; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "warn") { - settingsMap[logoutputlevel] = level_warn; + portduino_config.logoutputlevel = level_warn; } else if (yamlConfig["Logging"]["LogLevel"].as("info") == "error") { - settingsMap[logoutputlevel] = level_error; + portduino_config.logoutputlevel = level_error; } - settingsStrings[traceFilename] = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); if (yamlConfig["Logging"]["AsciiLogs"]) { // Default is !isatty(1) but can be set explicitly in config.yaml - settingsMap[ascii_logs] = yamlConfig["Logging"]["AsciiLogs"].as(); + portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); + portduino_config.ascii_logs_explicit = true; } } if (yamlConfig["Lora"]) { - const struct { - configNames cfgName; - std::string strName; - } loraModules[] = {{use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, - {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, - {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; - for (auto &loraModule : loraModules) { - settingsMap[loraModule.cfgName] = false; - } + if (yamlConfig["Lora"]["Module"]) { - for (auto &loraModule : loraModules) { - if (yamlConfig["Lora"]["Module"].as("") == loraModule.strName) { - settingsMap[loraModule.cfgName] = true; + for (auto &loraModule : portduino_config.loraModules) { + if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { + portduino_config.lora_module = loraModule.first; break; } } } + if (yamlConfig["Lora"]["SX126X_MAX_POWER"]) + portduino_config.sx126x_max_power = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["SX128X_MAX_POWER"]) + portduino_config.sx128x_max_power = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["LR1110_MAX_POWER"]) + portduino_config.lr1110_max_power = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); + if (yamlConfig["Lora"]["LR1120_MAX_POWER"]) + portduino_config.lr1120_max_power = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); + if (yamlConfig["Lora"]["RF95_MAX_POWER"]) + portduino_config.rf95_max_power = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); - settingsMap[sx126x_max_power] = yamlConfig["Lora"]["SX126X_MAX_POWER"].as(22); - settingsMap[sx128x_max_power] = yamlConfig["Lora"]["SX128X_MAX_POWER"].as(13); - settingsMap[lr1110_max_power] = yamlConfig["Lora"]["LR1110_MAX_POWER"].as(22); - settingsMap[lr1120_max_power] = yamlConfig["Lora"]["LR1120_MAX_POWER"].as(13); - settingsMap[rf95_max_power] = yamlConfig["Lora"]["RF95_MAX_POWER"].as(20); + if (portduino_config.lora_module != use_autoconf && portduino_config.lora_module != use_simradio && + !portduino_config.force_simradio) { + portduino_config.dio2_as_rf_switch = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); + portduino_config.dio3_tcxo_voltage = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; + if (portduino_config.dio3_tcxo_voltage == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { + portduino_config.dio3_tcxo_voltage = 1800; // default millivolts for "true" + } - settingsMap[dio2_as_rf_switch] = yamlConfig["Lora"]["DIO2_AS_RF_SWITCH"].as(false); - settingsMap[dio3_tcxo_voltage] = yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(0) * 1000; - if (settingsMap[dio3_tcxo_voltage] == 0 && yamlConfig["Lora"]["DIO3_TCXO_VOLTAGE"].as(false)) { - settingsMap[dio3_tcxo_voltage] = 1800; // default millivolts for "true" - } - - // backwards API compatibility and to globally set gpiochip once - int defaultGpioChip = settingsMap[default_gpiochip] = yamlConfig["Lora"]["gpiochip"].as(0); - - const struct { - configNames pin; - configNames gpiochip; - configNames line; - std::string strName; - } pinMappings[] = { - {cs_pin, cs_gpiochip, cs_line, "CS"}, - {irq_pin, irq_gpiochip, irq_line, "IRQ"}, - {busy_pin, busy_gpiochip, busy_line, "Busy"}, - {reset_pin, reset_gpiochip, reset_line, "Reset"}, - {txen_pin, txen_gpiochip, txen_line, "TXen"}, - {rxen_pin, rxen_gpiochip, rxen_line, "RXen"}, - {sx126x_ant_sw_pin, sx126x_ant_sw_gpiochip, sx126x_ant_sw_line, "SX126X_ANT_SW"}, - }; - for (auto &pinMap : pinMappings) { - if (yamlConfig["Lora"][pinMap.strName].IsMap()) { - settingsMap[pinMap.pin] = yamlConfig["Lora"][pinMap.strName]["pin"].as(RADIOLIB_NC); - settingsMap[pinMap.line] = yamlConfig["Lora"][pinMap.strName]["line"].as(settingsMap[pinMap.pin]); - settingsMap[pinMap.gpiochip] = yamlConfig["Lora"][pinMap.strName]["gpiochip"].as(defaultGpioChip); - } else { // backwards API compatibility - settingsMap[pinMap.pin] = yamlConfig["Lora"][pinMap.strName].as(RADIOLIB_NC); - settingsMap[pinMap.line] = settingsMap[pinMap.pin]; - settingsMap[pinMap.gpiochip] = defaultGpioChip; + // backwards API compatibility and to globally set gpiochip once + portduino_config.lora_default_gpiochip = yamlConfig["Lora"]["gpiochip"].as(0); + for (auto this_pin : portduino_config.all_pins) { + if (this_pin->config_section == "Lora") { + readGPIOFromYaml(yamlConfig["Lora"][this_pin->config_name], *this_pin); + } } } - settingsMap[spiSpeed] = yamlConfig["Lora"]["spiSpeed"].as(2000000); - settingsStrings[lora_usb_serial_num] = yamlConfig["Lora"]["USB_Serialnum"].as(""); - settingsMap[lora_usb_pid] = yamlConfig["Lora"]["USB_PID"].as(0x5512); - settingsMap[lora_usb_vid] = yamlConfig["Lora"]["USB_VID"].as(0x1A86); + portduino_config.spiSpeed = yamlConfig["Lora"]["spiSpeed"].as(2000000); + portduino_config.lora_usb_serial_num = yamlConfig["Lora"]["USB_Serialnum"].as(""); + portduino_config.lora_usb_pid = yamlConfig["Lora"]["USB_PID"].as(0x5512); + portduino_config.lora_usb_vid = yamlConfig["Lora"]["USB_VID"].as(0x1A86); - settingsStrings[spidev] = yamlConfig["Lora"]["spidev"].as("spidev0.0"); - if (settingsStrings[spidev] != "ch341") { - settingsStrings[spidev] = "/dev/" + settingsStrings[spidev]; - if (settingsStrings[spidev].length() == 14) { - int x = settingsStrings[spidev].at(11) - '0'; - int y = settingsStrings[spidev].at(13) - '0'; + portduino_config.lora_spi_dev = yamlConfig["Lora"]["spidev"].as("spidev0.0"); + if (portduino_config.lora_spi_dev != "ch341") { + portduino_config.lora_spi_dev = "/dev/" + portduino_config.lora_spi_dev; + if (portduino_config.lora_spi_dev.length() == 14) { + int x = portduino_config.lora_spi_dev.at(11) - '0'; + int y = portduino_config.lora_spi_dev.at(13) - '0'; // Pretty sure this is always true if (x >= 0 && x < 10 && y >= 0 && y < 10) { // I believe this bit of weirdness is specifically for the new GUI - settingsMap[spidev] = x + y << 4; - settingsMap[displayspidev] = settingsMap[spidev]; - settingsMap[touchscreenspidev] = settingsMap[spidev]; + portduino_config.lora_spi_dev_int = x + y << 4; + portduino_config.display_spi_dev_int = portduino_config.lora_spi_dev_int; + portduino_config.touchscreen_spi_dev_int = portduino_config.lora_spi_dev_int; } } } @@ -676,163 +566,152 @@ bool loadConfig(const char *configPath) } } } - if (yamlConfig["GPIO"]) { - settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); - } + readGPIOFromYaml(yamlConfig["GPIO"]["User"], portduino_config.userButtonPin); if (yamlConfig["GPS"]) { std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); if (serialPath != "") { Serial1.setPath(serialPath); - settingsMap[has_gps] = 1; + portduino_config.has_gps = 1; } } if (yamlConfig["I2C"]) { - settingsStrings[i2cdev] = yamlConfig["I2C"]["I2CDevice"].as(""); + portduino_config.i2cdev = yamlConfig["I2C"]["I2CDevice"].as(""); } if (yamlConfig["Display"]) { - if (yamlConfig["Display"]["Panel"].as("") == "ST7789") - settingsMap[displayPanel] = st7789; - else if (yamlConfig["Display"]["Panel"].as("") == "ST7735") - settingsMap[displayPanel] = st7735; - else if (yamlConfig["Display"]["Panel"].as("") == "ST7735S") - settingsMap[displayPanel] = st7735s; - else if (yamlConfig["Display"]["Panel"].as("") == "ST7796") - settingsMap[displayPanel] = st7796; - else if (yamlConfig["Display"]["Panel"].as("") == "ILI9341") - settingsMap[displayPanel] = ili9341; - else if (yamlConfig["Display"]["Panel"].as("") == "ILI9342") - settingsMap[displayPanel] = ili9342; - else if (yamlConfig["Display"]["Panel"].as("") == "ILI9486") - settingsMap[displayPanel] = ili9486; - else if (yamlConfig["Display"]["Panel"].as("") == "ILI9488") - settingsMap[displayPanel] = ili9488; - else if (yamlConfig["Display"]["Panel"].as("") == "HX8357D") - settingsMap[displayPanel] = hx8357d; - else if (yamlConfig["Display"]["Panel"].as("") == "X11") - settingsMap[displayPanel] = x11; - else if (yamlConfig["Display"]["Panel"].as("") == "FB") - settingsMap[displayPanel] = fb; - settingsMap[displayHeight] = yamlConfig["Display"]["Height"].as(0); - settingsMap[displayWidth] = yamlConfig["Display"]["Width"].as(0); - settingsMap[displayDC] = yamlConfig["Display"]["DC"].as(-1); - settingsMap[displayCS] = yamlConfig["Display"]["CS"].as(-1); - settingsMap[displayRGBOrder] = yamlConfig["Display"]["RGBOrder"].as(false); - settingsMap[displayBacklight] = yamlConfig["Display"]["Backlight"].as(-1); - settingsMap[displayBacklightInvert] = yamlConfig["Display"]["BacklightInvert"].as(false); - settingsMap[displayBacklightPWMChannel] = yamlConfig["Display"]["BacklightPWMChannel"].as(-1); - settingsMap[displayReset] = yamlConfig["Display"]["Reset"].as(-1); - settingsMap[displayOffsetX] = yamlConfig["Display"]["OffsetX"].as(0); - settingsMap[displayOffsetY] = yamlConfig["Display"]["OffsetY"].as(0); - settingsMap[displayRotate] = yamlConfig["Display"]["Rotate"].as(false); - settingsMap[displayOffsetRotate] = yamlConfig["Display"]["OffsetRotate"].as(1); - settingsMap[displayInvert] = yamlConfig["Display"]["Invert"].as(false); - settingsMap[displayBusFrequency] = yamlConfig["Display"]["BusFrequency"].as(40000000); + + for (auto &screen_name : portduino_config.screen_names) { + if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) + portduino_config.displayPanel = screen_name.first; + } + portduino_config.displayHeight = yamlConfig["Display"]["Height"].as(0); + portduino_config.displayWidth = yamlConfig["Display"]["Width"].as(0); + + readGPIOFromYaml(yamlConfig["Display"]["DC"], portduino_config.displayDC, -1); + readGPIOFromYaml(yamlConfig["Display"]["CS"], portduino_config.displayCS, -1); + readGPIOFromYaml(yamlConfig["Display"]["Backlight"], portduino_config.displayBacklight, -1); + readGPIOFromYaml(yamlConfig["Display"]["BacklightPWMChannel"], portduino_config.displayBacklightPWMChannel, -1); + readGPIOFromYaml(yamlConfig["Display"]["Reset"], portduino_config.displayReset, -1); + + portduino_config.displayBacklightInvert = yamlConfig["Display"]["BacklightInvert"].as(false); + portduino_config.displayRGBOrder = yamlConfig["Display"]["RGBOrder"].as(false); + portduino_config.displayOffsetX = yamlConfig["Display"]["OffsetX"].as(0); + portduino_config.displayOffsetY = yamlConfig["Display"]["OffsetY"].as(0); + portduino_config.displayRotate = yamlConfig["Display"]["Rotate"].as(false); + portduino_config.displayOffsetRotate = yamlConfig["Display"]["OffsetRotate"].as(1); + portduino_config.displayInvert = yamlConfig["Display"]["Invert"].as(false); + portduino_config.displayBusFrequency = yamlConfig["Display"]["BusFrequency"].as(40000000); if (yamlConfig["Display"]["spidev"]) { - settingsStrings[displayspidev] = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); - if (settingsStrings[displayspidev].length() == 14) { - int x = settingsStrings[displayspidev].at(11) - '0'; - int y = settingsStrings[displayspidev].at(13) - '0'; + portduino_config.display_spi_dev = "/dev/" + yamlConfig["Display"]["spidev"].as("spidev0.1"); + if (portduino_config.display_spi_dev.length() == 14) { + int x = portduino_config.display_spi_dev.at(11) - '0'; + int y = portduino_config.display_spi_dev.at(13) - '0'; if (x >= 0 && x < 10 && y >= 0 && y < 10) { - settingsMap[displayspidev] = x + y << 4; - settingsMap[touchscreenspidev] = settingsMap[displayspidev]; + portduino_config.display_spi_dev_int = x + y << 4; + portduino_config.touchscreen_spi_dev_int = portduino_config.display_spi_dev_int; } } } } if (yamlConfig["Touchscreen"]) { if (yamlConfig["Touchscreen"]["Module"].as("") == "XPT2046") - settingsMap[touchscreenModule] = xpt2046; + portduino_config.touchscreenModule = xpt2046; else if (yamlConfig["Touchscreen"]["Module"].as("") == "STMPE610") - settingsMap[touchscreenModule] = stmpe610; + portduino_config.touchscreenModule = stmpe610; else if (yamlConfig["Touchscreen"]["Module"].as("") == "GT911") - settingsMap[touchscreenModule] = gt911; + portduino_config.touchscreenModule = gt911; else if (yamlConfig["Touchscreen"]["Module"].as("") == "FT5x06") - settingsMap[touchscreenModule] = ft5x06; - settingsMap[touchscreenCS] = yamlConfig["Touchscreen"]["CS"].as(-1); - settingsMap[touchscreenIRQ] = yamlConfig["Touchscreen"]["IRQ"].as(-1); - settingsMap[touchscreenBusFrequency] = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); - settingsMap[touchscreenRotate] = yamlConfig["Touchscreen"]["Rotate"].as(-1); - settingsMap[touchscreenI2CAddr] = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); + portduino_config.touchscreenModule = ft5x06; + + readGPIOFromYaml(yamlConfig["Touchscreen"]["CS"], portduino_config.touchscreenCS, -1); + readGPIOFromYaml(yamlConfig["Touchscreen"]["IRQ"], portduino_config.touchscreenIRQ, -1); + + portduino_config.touchscreenBusFrequency = yamlConfig["Touchscreen"]["BusFrequency"].as(1000000); + portduino_config.touchscreenRotate = yamlConfig["Touchscreen"]["Rotate"].as(-1); + portduino_config.touchscreenI2CAddr = yamlConfig["Touchscreen"]["I2CAddr"].as(-1); if (yamlConfig["Touchscreen"]["spidev"]) { - settingsStrings[touchscreenspidev] = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); - if (settingsStrings[touchscreenspidev].length() == 14) { - int x = settingsStrings[touchscreenspidev].at(11) - '0'; - int y = settingsStrings[touchscreenspidev].at(13) - '0'; + portduino_config.touchscreen_spi_dev = "/dev/" + yamlConfig["Touchscreen"]["spidev"].as(""); + if (portduino_config.touchscreen_spi_dev.length() == 14) { + int x = portduino_config.touchscreen_spi_dev.at(11) - '0'; + int y = portduino_config.touchscreen_spi_dev.at(13) - '0'; if (x >= 0 && x < 10 && y >= 0 && y < 10) { - settingsMap[touchscreenspidev] = x + y << 4; + portduino_config.touchscreen_spi_dev_int = x + y << 4; } } } } if (yamlConfig["Input"]) { - settingsStrings[keyboardDevice] = (yamlConfig["Input"]["KeyboardDevice"]).as(""); - settingsStrings[pointerDevice] = (yamlConfig["Input"]["PointerDevice"]).as(""); - settingsMap[userButtonPin] = yamlConfig["Input"]["User"].as(RADIOLIB_NC); - settingsMap[tbUpPin] = yamlConfig["Input"]["TrackballUp"].as(RADIOLIB_NC); - settingsMap[tbDownPin] = yamlConfig["Input"]["TrackballDown"].as(RADIOLIB_NC); - settingsMap[tbLeftPin] = yamlConfig["Input"]["TrackballLeft"].as(RADIOLIB_NC); - settingsMap[tbRightPin] = yamlConfig["Input"]["TrackballRight"].as(RADIOLIB_NC); - settingsMap[tbPressPin] = yamlConfig["Input"]["TrackballPress"].as(RADIOLIB_NC); + portduino_config.keyboardDevice = (yamlConfig["Input"]["KeyboardDevice"]).as(""); + portduino_config.pointerDevice = (yamlConfig["Input"]["PointerDevice"]).as(""); + + readGPIOFromYaml(yamlConfig["Input"]["User"], portduino_config.userButtonPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballUp"], portduino_config.tbUpPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballDown"], portduino_config.tbDownPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballLeft"], portduino_config.tbLeftPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballRight"], portduino_config.tbRightPin); + readGPIOFromYaml(yamlConfig["Input"]["TrackballPress"], portduino_config.tbPressPin); + if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "RISING") { - settingsMap[tbDirection] = 4; + portduino_config.tbDirection = 4; } else if (yamlConfig["Input"]["TrackballDirection"].as("RISING") == "FALLING") { - settingsMap[tbDirection] = 3; + portduino_config.tbDirection = 3; } } if (yamlConfig["Webserver"]) { - settingsMap[webserverport] = (yamlConfig["Webserver"]["Port"]).as(-1); - settingsStrings[webserverrootpath] = + portduino_config.webserverport = (yamlConfig["Webserver"]["Port"]).as(-1); + portduino_config.webserver_root_path = (yamlConfig["Webserver"]["RootPath"]).as("/usr/share/meshtasticd/web"); - settingsStrings[websslkeypath] = + portduino_config.webserver_ssl_key_path = (yamlConfig["Webserver"]["SSLKey"]).as("/etc/meshtasticd/ssl/private_key.pem"); - settingsStrings[websslcertpath] = + portduino_config.webserver_ssl_cert_path = (yamlConfig["Webserver"]["SSLCert"]).as("/etc/meshtasticd/ssl/certificate.pem"); } if (yamlConfig["HostMetrics"]) { - settingsMap[hostMetrics_channel] = (yamlConfig["HostMetrics"]["Channel"]).as(0); - settingsMap[hostMetrics_interval] = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); - settingsStrings[hostMetrics_user_command] = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); + portduino_config.hostMetrics_channel = (yamlConfig["HostMetrics"]["Channel"]).as(0); + portduino_config.hostMetrics_interval = (yamlConfig["HostMetrics"]["ReportInterval"]).as(0); + portduino_config.hostMetrics_user_command = (yamlConfig["HostMetrics"]["UserStringCommand"]).as(""); } if (yamlConfig["Config"]) { if (yamlConfig["Config"]["DisplayMode"]) { - settingsMap[has_configDisplayMode] = true; + portduino_config.has_configDisplayMode = true; if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { - settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR; } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "INVERTED") { - settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_INVERTED; } else if ((yamlConfig["Config"]["DisplayMode"]).as("") == "COLOR") { - settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; } else { - settingsMap[configDisplayMode] = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; } } } if (yamlConfig["General"]) { - settingsMap[maxnodes] = (yamlConfig["General"]["MaxNodes"]).as(200); - settingsMap[maxtophone] = (yamlConfig["General"]["MaxMessageQueue"]).as(100); - settingsStrings[config_directory] = (yamlConfig["General"]["ConfigDirectory"]).as(""); - settingsStrings[available_directory] = + portduino_config.MaxNodes = (yamlConfig["General"]["MaxNodes"]).as(200); + portduino_config.maxtophone = (yamlConfig["General"]["MaxMessageQueue"]).as(100); + portduino_config.config_directory = (yamlConfig["General"]["ConfigDirectory"]).as(""); + portduino_config.available_directory = (yamlConfig["General"]["AvailableDirectory"]).as("/etc/meshtasticd/available.d/"); if ((yamlConfig["General"]["MACAddress"]).as("") != "" && (yamlConfig["General"]["MACAddressSource"]).as("") != "") { std::cout << "Cannot set both MACAddress and MACAddressSource!" << std::endl; exit(EXIT_FAILURE); } - settingsStrings[mac_address] = (yamlConfig["General"]["MACAddress"]).as(""); - if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { - std::ifstream infile("/sys/class/net/" + (yamlConfig["General"]["MACAddressSource"]).as("") + - "/address"); - std::getline(infile, settingsStrings[mac_address]); + portduino_config.mac_address = (yamlConfig["General"]["MACAddress"]).as(""); + if (portduino_config.mac_address != "") { + portduino_config.mac_address_explicit = true; + } else if ((yamlConfig["General"]["MACAddressSource"]).as("") != "") { + portduino_config.mac_address_source = (yamlConfig["General"]["MACAddressSource"]).as(""); + std::ifstream infile("/sys/class/net/" + portduino_config.mac_address_source + "/address"); + std::getline(infile, portduino_config.mac_address); } // https://stackoverflow.com/a/20326454 - settingsStrings[mac_address].erase( - std::remove(settingsStrings[mac_address].begin(), settingsStrings[mac_address].end(), ':'), - settingsStrings[mac_address].end()); + portduino_config.mac_address.erase( + std::remove(portduino_config.mac_address.begin(), portduino_config.mac_address.end(), ':'), + portduino_config.mac_address.end()); } } catch (YAML::Exception &e) { std::cout << "*** Exception " << e.what() << std::endl; @@ -851,12 +730,12 @@ bool MAC_from_string(std::string mac_str, uint8_t *dmac) { mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end()); if (mac_str.length() == 12) { - dmac[0] = std::stoi(settingsStrings[mac_address].substr(0, 2), nullptr, 16); - dmac[1] = std::stoi(settingsStrings[mac_address].substr(2, 2), nullptr, 16); - dmac[2] = std::stoi(settingsStrings[mac_address].substr(4, 2), nullptr, 16); - dmac[3] = std::stoi(settingsStrings[mac_address].substr(6, 2), nullptr, 16); - dmac[4] = std::stoi(settingsStrings[mac_address].substr(8, 2), nullptr, 16); - dmac[5] = std::stoi(settingsStrings[mac_address].substr(10, 2), nullptr, 16); + dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16); + dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16); + dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16); + dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16); + dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16); + dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16); return true; } else { return false; @@ -875,4 +754,19 @@ std::string exec(const char *cmd) result += buffer.data(); } return result; +} + +void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault) +{ + if (sourceNode.IsMap()) { + destPin.enabled = true; + destPin.pin = sourceNode["pin"].as(pinDefault); + destPin.line = sourceNode["line"].as(destPin.pin); + destPin.gpiochip = sourceNode["gpiochip"].as(portduino_config.lora_default_gpiochip); + } else if (sourceNode) { // backwards API compatibility + destPin.enabled = true; + destPin.pin = sourceNode.as(pinDefault); + destPin.line = destPin.pin; + destPin.gpiochip = portduino_config.lora_default_gpiochip; + } } \ No newline at end of file diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 8c36a1180..106900c48 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -6,6 +6,7 @@ #include "LR11x0Interface.h" #include "Module.h" #include "platform/portduino/USBHal.h" +#include "yaml-cpp/yaml.h" // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} @@ -19,36 +20,10 @@ inline const std::unordered_map configProducts = { {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; -enum configNames { - default_gpiochip, - cs_pin, - cs_line, - cs_gpiochip, - irq_pin, - irq_line, - irq_gpiochip, - busy_pin, - busy_line, - busy_gpiochip, - reset_pin, - reset_line, - reset_gpiochip, - txen_pin, - txen_line, - txen_gpiochip, - rxen_pin, - rxen_line, - rxen_gpiochip, - sx126x_ant_sw_pin, - sx126x_ant_sw_line, - sx126x_ant_sw_gpiochip, - sx126x_max_power, - sx128x_max_power, - lr1110_max_power, - lr1120_max_power, - rf95_max_power, - dio2_as_rf_switch, - dio3_tcxo_voltage, +enum screen_modules { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; +enum touchscreen_modules { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; +enum portduino_log_level { level_error, level_warn, level_info, level_debug, level_trace }; +enum lora_module_enum { use_simradio, use_autoconf, use_rf95, @@ -58,72 +33,18 @@ enum configNames { use_lr1110, use_lr1120, use_lr1121, - use_llcc68, - lora_usb_serial_num, - lora_usb_pid, - lora_usb_vid, - userButtonPin, - tbUpPin, - tbDownPin, - tbLeftPin, - tbRightPin, - tbPressPin, - tbDirection, - spidev, - spiSpeed, - i2cdev, - has_gps, - touchscreenModule, - touchscreenCS, - touchscreenIRQ, - touchscreenI2CAddr, - touchscreenBusFrequency, - touchscreenRotate, - touchscreenspidev, - displayspidev, - displayBusFrequency, - displayPanel, - displayWidth, - displayHeight, - displayCS, - displayDC, - displayRGBOrder, - displayBacklight, - displayBacklightPWMChannel, - displayBacklightInvert, - displayReset, - displayRotate, - displayOffsetRotate, - displayOffsetX, - displayOffsetY, - displayInvert, - keyboardDevice, - pointerDevice, - logoutputlevel, - traceFilename, - webserver, - webserverport, - webserverrootpath, - websslkeypath, - websslcertpath, - maxtophone, - maxnodes, - ascii_logs, - config_directory, - available_directory, - mac_address, - hostMetrics_interval, - hostMetrics_channel, - hostMetrics_user_command, - configDisplayMode, - has_configDisplayMode + use_llcc68 +}; + +struct pinMapping { + std::string config_section; + std::string config_name; + int pin = RADIOLIB_NC; + int gpiochip; + int line; + bool enabled = false; }; -enum { no_screen, x11, fb, st7789, st7735, st7735s, st7796, ili9341, ili9342, ili9486, ili9488, hx8357d }; -enum { no_touchscreen, xpt2046, stmpe610, gt911, ft5x06 }; -enum { level_error, level_warn, level_info, level_debug, level_trace }; -extern std::map settingsMap; -extern std::map settingsStrings; extern std::ofstream traceFile; extern Ch341Hal *ch341Hal; int initGPIOPin(int pinNum, std::string gpioChipname, int line); @@ -131,13 +52,422 @@ bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); bool MAC_from_string(std::string mac_str, uint8_t *dmac); +void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault = RADIOLIB_NC); std::string exec(const char *cmd); extern struct portduino_config_struct { + // Lora + std::map loraModules = { + {use_simradio, "sim"}, {use_autoconf, "auto"}, {use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, + {use_sx1280, "sx1280"}, {use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}}; + + std::map screen_names = {{x11, "X11"}, {fb, "FB"}, {st7789, "ST7789"}, + {st7735, "ST7735"}, {st7735s, "ST7735S"}, {st7796, "ST7796"}, + {ili9341, "ILI9341"}, {ili9342, "ILI9342"}, {ili9486, "ILI9486"}, + {ili9488, "ILI9488"}, {hx8357d, "HX8357D"}}; + + lora_module_enum lora_module; bool has_rfswitch_table = false; uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; Module::RfSwitchMode_t rfswitch_table[8]; bool force_simradio = false; bool has_device_id = false; uint8_t device_id[16] = {0}; + std::string lora_spi_dev = ""; + std::string lora_usb_serial_num = ""; + int lora_spi_dev_int = 0; + int lora_default_gpiochip = 0; + int sx126x_max_power = 22; + int sx128x_max_power = 13; + int lr1110_max_power = 22; + int lr1120_max_power = 13; + int rf95_max_power = 20; + bool dio2_as_rf_switch = false; + int dio3_tcxo_voltage = 0; + int lora_usb_pid = 0x5512; + int lora_usb_vid = 0x1A86; + int spiSpeed = 2000000; + pinMapping lora_cs_pin = {"Lora", "CS"}; + pinMapping lora_irq_pin = {"Lora", "IRQ"}; + pinMapping lora_busy_pin = {"Lora", "Busy"}; + pinMapping lora_reset_pin = {"Lora", "Reset"}; + pinMapping lora_txen_pin = {"Lora", "TXen"}; + pinMapping lora_rxen_pin = {"Lora", "RXen"}; + pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + + // GPS + bool has_gps = false; + + // I2C + std::string i2cdev = ""; + + // Display + std::string display_spi_dev = ""; + int display_spi_dev_int = 0; + int displayBusFrequency = 40000000; + screen_modules displayPanel = no_screen; + int displayWidth = 0; + int displayHeight = 0; + bool displayRGBOrder = false; + bool displayBacklightInvert = false; + bool displayRotate = false; + int displayOffsetRotate = 1; + bool displayInvert = false; + int displayOffsetX = 0; + int displayOffsetY = 0; + pinMapping displayDC = {"Display", "DC"}; + pinMapping displayCS = {"Display", "CS"}; + pinMapping displayBacklight = {"Display", "Backlight"}; + pinMapping displayBacklightPWMChannel = {"Display", "BacklightPWMChannel"}; + pinMapping displayReset = {"Display", "Reset"}; + + // Touchscreen + std::string touchscreen_spi_dev = ""; + int touchscreen_spi_dev_int = 0; + touchscreen_modules touchscreenModule = no_touchscreen; + int touchscreenI2CAddr = -1; + int touchscreenBusFrequency = 1000000; + int touchscreenRotate = -1; + pinMapping touchscreenCS = {"Touchscreen", "CS"}; + pinMapping touchscreenIRQ = {"Touchscreen", "IRQ"}; + + // Input + std::string keyboardDevice = ""; + std::string pointerDevice = ""; + int tbDirection; + pinMapping userButtonPin = {"Input", "User"}; + pinMapping tbUpPin = {"Input", "TrackballUp"}; + pinMapping tbDownPin = {"Input", "TrackballDown"}; + pinMapping tbLeftPin = {"Input", "TrackballLwft"}; + pinMapping tbRightPin = {"Input", "TrackballRight"}; + pinMapping tbPressPin = {"Input", "TrackballPress"}; + + // Logging + portduino_log_level logoutputlevel = level_debug; + std::string traceFilename; + bool ascii_logs = !isatty(1); + bool ascii_logs_explicit = false; + + // Webserver + std::string webserver_root_path = ""; + std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; + std::string webserver_ssl_cert_path = "/etc/meshtasticd/ssl/certificate.pem"; + int webserverport = -1; + + // HostMetrics + std::string hostMetrics_user_command = ""; + int hostMetrics_interval = 0; + int hostMetrics_channel = 0; + + // config + int configDisplayMode = 0; + bool has_configDisplayMode = false; + + // General + std::string mac_address = ""; + bool mac_address_explicit = false; + std::string mac_address_source = ""; + std::string config_directory = ""; + std::string available_directory = "/etc/meshtasticd/available.d/"; + int maxtophone = 100; + int MaxNodes = 200; + + pinMapping *all_pins[20] = {&lora_cs_pin, + &lora_irq_pin, + &lora_busy_pin, + &lora_reset_pin, + &lora_txen_pin, + &lora_rxen_pin, + &lora_sx126x_ant_sw_pin, + &displayDC, + &displayCS, + &displayBacklight, + &displayBacklightPWMChannel, + &displayReset, + &touchscreenCS, + &touchscreenIRQ, + &userButtonPin, + &tbUpPin, + &tbDownPin, + &tbLeftPin, + &tbRightPin, + &tbPressPin}; + + std::string emit_yaml() + { + YAML::Emitter out; + out << YAML::BeginMap; + + // Lora + out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; + + for (auto lora_pin : all_pins) { + if (lora_pin->config_section == "Lora" && lora_pin->enabled) { + out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; + out << YAML::Key << "line" << YAML::Value << lora_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << lora_pin->gpiochip; + out << YAML::EndMap; // User + } + } + + if (sx126x_max_power != 22) + out << YAML::Key << "SX126X_MAX_POWER" << YAML::Value << sx126x_max_power; + if (sx128x_max_power != 13) + out << YAML::Key << "SX128X_MAX_POWER" << YAML::Value << sx128x_max_power; + if (lr1110_max_power != 22) + out << YAML::Key << "LR1110_MAX_POWER" << YAML::Value << lr1110_max_power; + if (lr1120_max_power != 13) + out << YAML::Key << "LR1120_MAX_POWER" << YAML::Value << lr1120_max_power; + if (rf95_max_power != 20) + out << YAML::Key << "RF95_MAX_POWER" << YAML::Value << rf95_max_power; + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; + if (dio3_tcxo_voltage != 0) + out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << dio3_tcxo_voltage; + if (lora_usb_pid != 0x5512) + out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; + if (lora_usb_vid != 0x1A86) + out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; + if (lora_spi_dev != "") + out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; + if (lora_usb_serial_num != "") + out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; + out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; + if (rfswitch_dio_pins[0] != RADIOLIB_NC) { + out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; + + out << YAML::Key << "pins"; + out << YAML::Value << YAML::Flow << YAML::BeginSeq; + + for (int i = 0; i < 5; i++) { + // set up the pin array first + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO5) + out << "DIO5"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO6) + out << "DIO6"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO7) + out << "DIO7"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO8) + out << "DIO8"; + if (rfswitch_dio_pins[i] == RADIOLIB_LR11X0_DIO10) + out << "DIO10"; + } + out << YAML::EndSeq; + + for (int i = 0; i < 7; i++) { + switch (i) { + case 0: + out << YAML::Key << "MODE_STBY"; + break; + case 1: + out << YAML::Key << "MODE_RX"; + break; + case 2: + out << YAML::Key << "MODE_TX"; + break; + case 3: + out << YAML::Key << "MODE_TX_HP"; + break; + case 4: + out << YAML::Key << "MODE_TX_HF"; + break; + case 5: + out << YAML::Key << "MODE_GNSS"; + break; + case 6: + out << YAML::Key << "MODE_WIFI"; + break; + } + + out << YAML::Value << YAML::Flow << YAML::BeginSeq; + for (int j = 0; j < 5; j++) { + if (rfswitch_table[i].values[j] == HIGH) { + out << "HIGH"; + } else { + out << "LOW"; + } + } + out << YAML::EndSeq; + } + out << YAML::EndMap; // rfswitch_table + } + out << YAML::EndMap; // Lora + + if (i2cdev != "") { + out << YAML::Key << "I2C" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "I2CDevice" << YAML::Value << i2cdev; + out << YAML::EndMap; // I2C + } + + // Display + if (displayPanel != no_screen) { + out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; + for (auto &screen_name : screen_names) { + if (displayPanel == screen_name.first) + out << YAML::Key << "Module" << YAML::Value << screen_name.second; + } + for (auto display_pin : all_pins) { + if (display_pin->config_section == "Display" && display_pin->enabled) { + out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << display_pin->pin; + out << YAML::Key << "line" << YAML::Value << display_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << display_pin->gpiochip; + out << YAML::EndMap; + } + } + out << YAML::Key << "spidev" << YAML::Value << display_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << displayBusFrequency; + if (displayWidth) + out << YAML::Key << "Width" << YAML::Value << displayWidth; + if (displayHeight) + out << YAML::Key << "Height" << YAML::Value << displayHeight; + if (displayRGBOrder) + out << YAML::Key << "RGBOrder" << YAML::Value << true; + if (displayBacklightInvert) + out << YAML::Key << "BacklightInvert" << YAML::Value << true; + if (displayRotate) + out << YAML::Key << "Rotate" << YAML::Value << true; + if (displayInvert) + out << YAML::Key << "Invert" << YAML::Value << true; + if (displayOffsetX) + out << YAML::Key << "OffsetX" << YAML::Value << displayOffsetX; + if (displayOffsetY) + out << YAML::Key << "OffsetY" << YAML::Value << displayOffsetY; + + out << YAML::Key << "OffsetRotate" << YAML::Value << displayOffsetRotate; + + out << YAML::EndMap; // Display + } + + // Touchscreen + if (touchscreen_spi_dev != "") { + out << YAML::Key << "Touchscreen" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "spidev" << YAML::Value << touchscreen_spi_dev; + out << YAML::Key << "BusFrequency" << YAML::Value << touchscreenBusFrequency; + switch (touchscreenModule) { + case xpt2046: + out << YAML::Key << "Module" << YAML::Value << "XPT2046"; + case stmpe610: + out << YAML::Key << "Module" << YAML::Value << "STMPE610"; + case gt911: + out << YAML::Key << "Module" << YAML::Value << "GT911"; + case ft5x06: + out << YAML::Key << "Module" << YAML::Value << "FT5x06"; + } + for (auto touchscreen_pin : all_pins) { + if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { + out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; + out << YAML::Key << "line" << YAML::Value << touchscreen_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << touchscreen_pin->gpiochip; + out << YAML::EndMap; + } + } + if (touchscreenRotate != -1) + out << YAML::Key << "Rotate" << YAML::Value << touchscreenRotate; + if (touchscreenI2CAddr != -1) + out << YAML::Key << "I2CAddr" << YAML::Value << touchscreenI2CAddr; + out << YAML::EndMap; // Touchscreen + } + + // Input + out << YAML::Key << "Input" << YAML::Value << YAML::BeginMap; + if (keyboardDevice != "") + out << YAML::Key << "KeyboardDevice" << YAML::Value << keyboardDevice; + if (pointerDevice != "") + out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; + + for (auto input_pin : all_pins) { + if (input_pin->config_section == "Input" && input_pin->enabled) { + out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; + out << YAML::Key << "pin" << YAML::Value << input_pin->pin; + out << YAML::Key << "line" << YAML::Value << input_pin->line; + out << YAML::Key << "gpiochip" << YAML::Value << input_pin->gpiochip; + out << YAML::EndMap; + } + } + if (tbDirection == 3) + out << YAML::Key << "TrackballDirection" << YAML::Value << "FALLING"; + + out << YAML::EndMap; // Input + + out << YAML::Key << "Logging" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "LogLevel" << YAML::Value; + switch (logoutputlevel) { + case level_error: + out << "error"; + break; + case level_warn: + out << "warn"; + break; + case level_info: + out << "info"; + break; + case level_debug: + out << "debug"; + break; + case level_trace: + out << "trace"; + break; + } + if (traceFilename != "") + out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (ascii_logs_explicit) { + out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; + } + out << YAML::EndMap; // Logging + + // Webserver + if (webserver_root_path != "") { + out << YAML::Key << "Webserver" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "RootPath" << YAML::Value << webserver_root_path; + out << YAML::Key << "SSLKey" << YAML::Value << webserver_ssl_key_path; + out << YAML::Key << "SSLCert" << YAML::Value << webserver_ssl_cert_path; + out << YAML::Key << "Port" << YAML::Value << webserverport; + out << YAML::EndMap; // Webserver + } + + // HostMetrics + if (hostMetrics_user_command != "") { + out << YAML::Key << "HostMetrics" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "UserStringCommand" << YAML::Value << hostMetrics_user_command; + out << YAML::Key << "ReportInterval" << YAML::Value << hostMetrics_interval; + out << YAML::Key << "Channel" << YAML::Value << hostMetrics_channel; + + out << YAML::EndMap; // HostMetrics + } + + // config + if (has_configDisplayMode) { + out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + } + + out << YAML::EndMap; // Config + } + + // General + out << YAML::Key << "General" << YAML::Value << YAML::BeginMap; + if (config_directory != "") + out << YAML::Key << "ConfigDirectory" << YAML::Value << config_directory; + if (mac_address_explicit) + out << YAML::Key << "MACAddress" << YAML::Value << mac_address; + if (mac_address_source != "") + out << YAML::Key << "MACAddressSource" << YAML::Value << mac_address_source; + if (available_directory != "") + out << YAML::Key << "AvailableDirectory" << YAML::Value << available_directory; + out << YAML::Key << "MaxMessageQueue" << YAML::Value << maxtophone; + out << YAML::Key << "MaxNodes" << YAML::Value << MaxNodes; + out << YAML::EndMap; // General + return out.c_str(); + } } portduino_config; \ No newline at end of file diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index 07d0aeee0..e10519d21 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -28,9 +28,9 @@ #endif #ifndef HAS_TRACKBALL #define HAS_TRACKBALL 1 -#define TB_DOWN (uint8_t) settingsMap[tbDownPin] -#define TB_UP (uint8_t) settingsMap[tbUpPin] -#define TB_LEFT (uint8_t) settingsMap[tbLeftPin] -#define TB_RIGHT (uint8_t) settingsMap[tbRightPin] -#define TB_PRESS (uint8_t) settingsMap[tbPressPin] +#define TB_DOWN (uint8_t) portduino_config.tbDownPin.pin +#define TB_UP (uint8_t) portduino_config.tbUpPin.pin +#define TB_LEFT (uint8_t) portduino_config.tbLeftPin.pin +#define TB_RIGHT (uint8_t) portduino_config.tbRightPin.pin +#define TB_PRESS (uint8_t) portduino_config.tbPressPin.pin #endif \ No newline at end of file diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h index 11a6c0bd3..3e91c6820 100644 --- a/variants/native/portduino-buildroot/variant.h +++ b/variants/native/portduino-buildroot/variant.h @@ -1,5 +1,5 @@ #define HAS_SCREEN 1 #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 -#define MAX_RX_TOPHONE settingsMap[maxtophone] -#define MAX_NUM_NODES settingsMap[maxnodes] +#define MAX_RX_TOPHONE portduino_config.maxtophone +#define MAX_NUM_NODES portduino_config.MaxNodes diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index a7ca865be..af05fcf8d 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -3,8 +3,8 @@ #endif #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 -#define MAX_RX_TOPHONE settingsMap[maxtophone] -#define MAX_NUM_NODES settingsMap[maxnodes] +#define MAX_RX_TOPHONE portduino_config.maxtophone +#define MAX_NUM_NODES portduino_config.MaxNodes // RAK12002 RTC Module -#define RV3028_RTC (uint8_t)0b1010010 \ No newline at end of file +#define RV3028_RTC (uint8_t)0b1010010 From 7b2ff7e196f8fb2c0fb1dde5997535908745ba58 Mon Sep 17 00:00:00 2001 From: "Trent V." Date: Sat, 13 Sep 2025 11:56:23 -0500 Subject: [PATCH 47/62] updated shebang to use a more standard path for bash (#7922) Signed-off-by: Trenton VanderWert --- bin/device-install.sh | 2 +- bin/device-update.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/device-install.sh b/bin/device-install.sh index 594f9dd6b..ede75bbba 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} BPS_RESET=false diff --git a/bin/device-update.sh b/bin/device-update.sh index 6f29496e9..f64280a5b 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash PYTHON=${PYTHON:-$(which python3 python|head -n 1)} CHANGE_MODE=false From a1cf305336d60bc4128121318aaf1c8d4f24d852 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sun, 7 Sep 2025 14:34:07 -0500 Subject: [PATCH 48/62] Show GPS Date properly in drawCommonHeader (#7887) * Commit good code that is sustainable * Fix new build errors --- src/graphics/SharedUIDisplay.cpp | 47 +++++++++++++++---- src/graphics/SharedUIDisplay.h | 3 +- src/graphics/draw/ClockRenderer.cpp | 7 +-- src/graphics/draw/UIRenderer.h | 1 + .../Telemetry/EnvironmentTelemetry.cpp | 3 +- src/modules/Telemetry/PowerTelemetry.cpp | 3 +- 6 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 0f32b0896..3937bcf50 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -1,6 +1,7 @@ #include "graphics/SharedUIDisplay.h" #include "RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" #include "power.h" @@ -57,7 +58,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only) +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -73,7 +74,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int screenW = display->getWidth(); const int screenH = display->getHeight(); - if (!battery_only) { + if (!force_no_invert) { // === Inverted Header Background === if (isInverted) { display->setColor(BLACK); @@ -191,13 +192,28 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeX = screenW - xOffset - timeStrWidth + 4; - if (rtc_sec > 0 && !battery_only) { + if (rtc_sec > 0) { // === Build Time String === long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + // === Build Date String === + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char dateLine[40]; + + if (isHighResolution) { + snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); + } else { + if (hasUnreadMessage) { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]); + } else { + snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]); + } + } + if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; @@ -206,7 +222,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } - timeStrWidth = display->getStringWidth(timeStr); + if (show_date) { + timeStrWidth = display->getStringWidth(dateLine); + } else { + timeStrWidth = display->getStringWidth(timeStr); + } timeX = screenW - xOffset - timeStrWidth + 3; // === Show Mail or Mute Icon to the Left of Time === @@ -233,7 +253,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(BLACK); @@ -248,7 +268,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } else { int iconX = iconRightEdge - (mail_width - 2); int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->setColor(BLACK); @@ -291,10 +311,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } } - // === Draw Time === - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); + if (show_date) { + // === Draw Date === + display->drawString(timeX, textY, dateLine); + if (isBold) + display->drawString(timeX - 1, textY, dateLine); + } else { + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + } } else { // === No Time Available: Mail/Mute Icon Moves to Far Right === diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index b8d82795e..e1a7c6383 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -49,7 +49,8 @@ void determineResolution(int16_t screenheight, int16_t screenwidth); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, + bool show_date = false); const int *getTextPositions(OLEDDisplay *display); diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index d046bda6f..aec0a701f 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -2,7 +2,6 @@ #if HAS_SCREEN #include "ClockRenderer.h" #include "NodeDB.h" -#include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" @@ -190,8 +189,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true); - int line = 0; + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -324,8 +322,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // === Set Title, Blank for Clock const char *titleStr = ""; // === Header === - graphics::drawCommonHeader(display, x, y, titleStr, true); - int line = 0; + graphics::drawCommonHeader(display, x, y, titleStr, true, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 3c8e1dd9d..eada150f9 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -1,5 +1,6 @@ #pragma once +#include "NodeDB.h" #include "graphics/Screen.h" #include "graphics/emotes.h" #include diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index c90d9250f..8ac160f8b 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -30,7 +30,8 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, + bool show_date); } #if __has_include() #include "Sensor/AHT10.h" diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 35409edef..479861a2e 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -24,7 +24,8 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, + bool show_date); } int32_t PowerTelemetryModule::runOnce() From c11680fcc0d9f68d3716f46b7259c58162cdf97b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 08:37:58 -0500 Subject: [PATCH 49/62] Fix formatting and trunk issues --- .github/workflows/main_matrix.yml | 1 - src/graphics/Screen.cpp | 1186 +++++++++++++++-------------- src/mesh/StreamAPI.h | 6 +- 3 files changed, 598 insertions(+), 595 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index b4fa0c56e..f61e314a7 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -270,7 +270,6 @@ jobs: push: false gather-artifacts: - if: github.repository == 'meshtastic/firmware' # trunk-ignore(checkov/CKV2_GHA_1) if: github.repository == 'meshtastic/firmware' permissions: diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6925bbc26..689c550d3 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -955,703 +955,707 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) if (!hiddenFrames.clock) { if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; - #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + fsi.positions.clock = numframes; +#if defined(M5STACK_UNITC6L) + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); + indicatorIcons.push_back(digital_icon_clock); #endif - // Declare this early so it’s available in FOCUS_PRESERVE block - bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); + // Declare this early so it’s available in FOCUS_PRESERVE block + bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - if (!hiddenFrames.home) { - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); - } + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; - indicatorIcons.push_back(icon_mail); + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist) { - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; - indicatorIcons.push_back(icon_nodes); - } + if (!hiddenFrames.nodelist) { + fsi.positions.nodelist = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - if (!hiddenFrames.nodelist_lastheard) { - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_hopsignal) { - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - } - if (!hiddenFrames.nodelist_distance) { - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); - } + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS - if (!hiddenFrames.nodelist_bearings) { - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - } - if (!hiddenFrames.gps) { - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); - } + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance && !hiddenFrames.lora) { - fsi.positions.lora = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; - indicatorIcons.push_back(icon_radio); - } - if (!hiddenFrames.system) { - fsi.positions.system = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; - indicatorIcons.push_back(icon_system); - } + if (RadioLibInterface::instance && !hiddenFrames.lora) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); + } #if !defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); - } + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.chirpy) { - fsi.positions.chirpy = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; - indicatorIcons.push_back(small_chirpy); - } + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(small_chirpy); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!hiddenFrames.wifi && isWifiAvailable()) { - fsi.positions.wifi = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; - indicatorIcons.push_back(icon_wifi); - } + if (!hiddenFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); + } #endif - // Beware of what changes you make in this code! - // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! - // Inside of that callback, goes over to MeshModule.cpp and we run - // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr - // entries until we're ready to start building the matching entries. - // We are doing our best to keep the normalFrames vector - // and the moduleFrames vector in lock step. - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); - LOG_DEBUG("Show %d module frames", moduleFrames.size()); + // Beware of what changes you make in this code! + // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! + // Inside of that callback, goes over to MeshModule.cpp and we run + // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr + // entries until we're ready to start building the matching entries. + // We are doing our best to keep the normalFrames vector + // and the moduleFrames vector in lock step. + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); + LOG_DEBUG("Show %d module frames", moduleFrames.size()); - for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { - // Draw the module frame, using the hack described above - if (*i != nullptr) { - normalFrames[numframes] = drawModuleFrame; + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + if (*i != nullptr) { + normalFrames[numframes] = drawModuleFrame; - // Check if the module being drawn has requested focus - // We will honor this request later, if setFrames was triggered by a UIFrameEvent - MeshModule *m = *i; - if (m && m->isRequestingFocus()) - fsi.positions.focusedModule = numframes; - if (m && m == waypointModule) - fsi.positions.waypoint = numframes; + // Check if the module being drawn has requested focus + // We will honor this request later, if setFrames was triggered by a UIFrameEvent + MeshModule *m = *i; + if (m && m->isRequestingFocus()) + fsi.positions.focusedModule = numframes; + if (m && m == waypointModule) + fsi.positions.waypoint = numframes; - indicatorIcons.push_back(icon_module); - numframes++; + indicatorIcons.push_back(icon_module); + numframes++; + } + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); + + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } + } + + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; + } + } + + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // ✅ Save frame count for use in custom overlay + LOG_DEBUG("Finished build frames. numframes: %d", numframes); + + ui->setFrames(normalFrames, numframes); + ui->disableAllIndicators(); + + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) + + // Focus on a specific frame, in the frame set we just created + switch (focus) { + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // ✅ Clear when message is *viewed* + ui->switchToFrame(fsi.positions.textMessage); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.focusedModule); + break; + case FOCUS_CLOCK: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.clock); + break; + case FOCUS_SYSTEM: + ui->switchToFrame(fsi.positions.system); + break; + + case FOCUS_PRESERVE: + // No more adjustment — force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { + ui->switchToFrame(originalPosition); + } + break; + } + + // Store the info about this frameset, for future setFrames calls + this->framesetInfo = fsi; + + setFastFramerate(); // Draw ASAP } - } - LOG_DEBUG("Added modules. numframes: %d", numframes); + void Screen::setFrameImmediateDraw(FrameCallback * drawFrames) + { + ui->disableAllIndicators(); + ui->setFrames(drawFrames, 1); + setFastFramerate(); + } - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; - - if (!hiddenFrames.show_favorites) { - // Temporary array to hold favorite node frames - std::vector favoriteFrames; - - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + void Screen::toggleFrameVisibility(const std::string &frameName) + { +#ifndef USE_EINK + if (frameName == "nodelist") { + hiddenFrames.nodelist = !hiddenFrames.nodelist; + } +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } +#endif + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; } } - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); + bool Screen::isFrameHidden(const std::string &frameName) const + { +#ifndef USE_EINK + if (frameName == "nodelist") + return hiddenFrames.nodelist; +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; + if (frameName == "gps") + return hiddenFrames.gps; +#endif + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; + + return false; + } + + // Dismisses the currently displayed screen frame, if possible + // Relevant for text message, waypoint, others in future? + // Triggered with a CardKB keycombo + void Screen::hideCurrentFrame() + { + uint8_t currentFrame = ui->getUiState()->currentFrame; + bool dismissed = false; + if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { + LOG_INFO("Hide Text Message"); + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + LOG_DEBUG("Hide Waypoint"); + devicestate.has_rx_waypoint = false; + hiddenFrames.waypoint = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.wifi) { + LOG_DEBUG("Hide WiFi Screen"); + hiddenFrames.wifi = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.lora) { + LOG_INFO("Hide LoRa"); + hiddenFrames.lora = true; + dismissed = true; + } + + if (dismissed) { + setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; } - } - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE - this->frameCount = numframes; // ✅ Save frame count for use in custom overlay - LOG_DEBUG("Finished build frames. numframes: %d", numframes); + void Screen::handleStartFirmwareUpdateScreen() + { + LOG_DEBUG("Show firmware screen"); + showingNormalScreen = false; + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - ui->setFrames(normalFrames, numframes); - ui->disableAllIndicators(); - - // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) - - // Focus on a specific frame, in the frame set we just created - switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(fsi.positions.deviceFocused); - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // ✅ Clear when message is *viewed* - ui->switchToFrame(fsi.positions.textMessage); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.focusedModule); - break; - case FOCUS_CLOCK: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.clock); - break; - case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.system); - break; - - case FOCUS_PRESERVE: - // No more adjustment — force stay on same index - if (previousFrameCount > fsi.frameCount) { - ui->switchToFrame(originalPosition - 1); - } else if (previousFrameCount < fsi.frameCount) { - ui->switchToFrame(originalPosition + 1); - } else { - ui->switchToFrame(originalPosition); + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; + setFrameImmediateDraw(frames); } - break; - } - // Store the info about this frameset, for future setFrames calls - this->framesetInfo = fsi; + void Screen::blink() + { + setFastFramerate(); + uint8_t count = 10; + dispdev->setBrightness(254); + while (count > 0) { + dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); + dispdev->display(); + delay(50); + dispdev->clear(); + dispdev->display(); + delay(50); + count = count - 1; + } + // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in + // OLEDDisplay. + dispdev->setBrightness(brightness); + } - setFastFramerate(); // Draw ASAP -} - -void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) -{ - ui->disableAllIndicators(); - ui->setFrames(drawFrames, 1); - setFastFramerate(); -} - -void Screen::toggleFrameVisibility(const std::string &frameName) -{ -#ifndef USE_EINK - if (frameName == "nodelist") { - hiddenFrames.nodelist = !hiddenFrames.nodelist; - } -#endif -#ifdef USE_EINK - if (frameName == "nodelist_lastheard") { - hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; - } - if (frameName == "nodelist_hopsignal") { - hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; - } - if (frameName == "nodelist_distance") { - hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; - } -#endif -#if HAS_GPS - if (frameName == "nodelist_bearings") { - hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; - } - if (frameName == "gps") { - hiddenFrames.gps = !hiddenFrames.gps; - } -#endif - if (frameName == "lora") { - hiddenFrames.lora = !hiddenFrames.lora; - } - if (frameName == "clock") { - hiddenFrames.clock = !hiddenFrames.clock; - } - if (frameName == "show_favorites") { - hiddenFrames.show_favorites = !hiddenFrames.show_favorites; - } - if (frameName == "chirpy") { - hiddenFrames.chirpy = !hiddenFrames.chirpy; - } -} - -bool Screen::isFrameHidden(const std::string &frameName) const -{ -#ifndef USE_EINK - if (frameName == "nodelist") - return hiddenFrames.nodelist; -#endif -#ifdef USE_EINK - if (frameName == "nodelist_lastheard") - return hiddenFrames.nodelist_lastheard; - if (frameName == "nodelist_hopsignal") - return hiddenFrames.nodelist_hopsignal; - if (frameName == "nodelist_distance") - return hiddenFrames.nodelist_distance; -#endif -#if HAS_GPS - if (frameName == "nodelist_bearings") - return hiddenFrames.nodelist_bearings; - if (frameName == "gps") - return hiddenFrames.gps; -#endif - if (frameName == "lora") - return hiddenFrames.lora; - if (frameName == "clock") - return hiddenFrames.clock; - if (frameName == "show_favorites") - return hiddenFrames.show_favorites; - if (frameName == "chirpy") - return hiddenFrames.chirpy; - - return false; -} - -// Dismisses the currently displayed screen frame, if possible -// Relevant for text message, waypoint, others in future? -// Triggered with a CardKB keycombo -void Screen::hideCurrentFrame() -{ - uint8_t currentFrame = ui->getUiState()->currentFrame; - bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Hide Text Message"); - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Hide Waypoint"); - devicestate.has_rx_waypoint = false; - hiddenFrames.waypoint = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Hide WiFi Screen"); - hiddenFrames.wifi = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.lora) { - LOG_INFO("Hide LoRa"); - hiddenFrames.lora = true; - dismissed = true; - } - - if (dismissed) { - setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE - } -} - -void Screen::handleStartFirmwareUpdateScreen() -{ - LOG_DEBUG("Show firmware screen"); - showingNormalScreen = false; - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - - static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; - setFrameImmediateDraw(frames); -} - -void Screen::blink() -{ - setFastFramerate(); - uint8_t count = 10; - dispdev->setBrightness(254); - while (count > 0) { - dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); - dispdev->display(); - delay(50); - dispdev->clear(); - dispdev->display(); - delay(50); - count = count - 1; - } - // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay. - dispdev->setBrightness(brightness); -} - -void Screen::increaseBrightness() -{ - brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); + void Screen::increaseBrightness() + { + brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); #if defined(ST7789_CS) - // run the setDisplayBrightness function. This works on t-decks - static_cast(dispdev)->setDisplayBrightness(brightness); + // run the setDisplayBrightness function. This works on t-decks + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ + } -void Screen::decreaseBrightness() -{ - brightness = (brightness < 70) ? brightness : (brightness - 62); + void Screen::decreaseBrightness() + { + brightness = (brightness < 70) ? brightness : (brightness - 62); #if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ -} - -void Screen::setFunctionSymbol(std::string sym) -{ - if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { - functionSymbol.push_back(sym); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ } - setFastFramerate(); - } -} -void Screen::removeFunctionSymbol(std::string sym) -{ - functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); -} + void Screen::setFunctionSymbol(std::string sym) + { + if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { + functionSymbol.push_back(sym); + functionSymbolString = ""; + for (auto symbol : functionSymbol) { + functionSymbolString = symbol + " " + functionSymbolString; + } + setFastFramerate(); + } + } -void Screen::handleOnPress() -{ - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} + void Screen::removeFunctionSymbol(std::string sym) + { + functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); + functionSymbolString = ""; + for (auto symbol : functionSymbol) { + functionSymbolString = symbol + " " + functionSymbolString; + } + setFastFramerate(); + } -void Screen::handleShowPrevFrame() -{ - // If screen was off, just wake it, otherwise go back to previous frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->previousFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} + void Screen::handleOnPress() + { + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } + } -void Screen::handleShowNextFrame() -{ - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} + void Screen::handleShowPrevFrame() + { + // If screen was off, just wake it, otherwise go back to previous frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->previousFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } + } + + void Screen::handleShowNextFrame() + { + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } + } #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif -void Screen::setFastFramerate() -{ + void Screen::setFastFramerate() + { #if defined(M5STACK_UNITC6L) - dispdev->clear(); - dispdev->display(); + dispdev->clear(); + dispdev->display(); #endif - // We are about to start a transition so speed up fps - targetFramerate = SCREEN_TRANSITION_FRAMERATE; + // We are about to start a transition so speed up fps + targetFramerate = SCREEN_TRANSITION_FRAMERATE; - ui->setTargetFPS(targetFramerate); - setInterval(0); // redraw ASAP - runASAP = true; -} - -int Screen::handleStatusUpdate(const meshtastic::Status *arg) -{ - // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); - switch (arg->getStatusType()) { - case STATUS_TYPE_NODE: - if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { - setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) + ui->setTargetFPS(targetFramerate); + setInterval(0); // redraw ASAP + runASAP = true; } - nodeDB->updateGUI = false; - break; - } - return 0; -} - -// Handles when message is received; will jump to text message frame. -int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) -{ - if (showingNormalScreen) { - if (packet->from == 0) { - // Outgoing message (likely sent from phone) - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - hiddenFrames.textMessage = true; - hasUnreadMessage = false; // Clear unread state when user replies - - setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list - } else { - // Incoming message - devicestate.has_rx_text_message = true; // Needed to include the message frame - hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view - - // Only wake/force display if the configuration allows it - if (shouldWakeOnReceivedMessage()) { - setOn(true); // Wake up the screen first - forceDisplay(); // Forces screen redraw - } - // === Prepare banner content === - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); - const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; - - const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); - - char banner[256]; - - // Check for bell character in message to determine alert type - bool isAlert = false; - for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == '\x07') { - isAlert = true; - break; + int Screen::handleStatusUpdate(const meshtastic::Status *arg) + { + // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); + switch (arg->getStatusType()) { + case STATUS_TYPE_NODE: + if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { + setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) } + nodeDB->updateGUI = false; + break; } - if (isAlert) { - if (longName && longName[0]) { - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + return 0; + } + + // Handles when message is received; will jump to text message frame. + int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) + { + if (showingNormalScreen) { + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + hiddenFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies + + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list } else { - strcpy(banner, "Alert Received"); - } - } else { - if (longName && longName[0]) { + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + + // Only wake/force display if the configuration allows it + if (shouldWakeOnReceivedMessage()) { + setOn(true); // Wake up the screen first + forceDisplay(); // Forces screen redraw + } + // === Prepare banner content === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + + char banner[256]; + + // Check for bell character in message to determine alert type + bool isAlert = false; + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } + } + + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + } else { + strcpy(banner, "Alert Received"); + } + } else { + if (longName && longName[0]) { #if defined(M5STACK_UNITC6L) - strcpy(banner, "New Message"); + strcpy(banner, "New Message"); #else snprintf(banner, sizeof(banner), "New Message from\n%s", longName); #endif - } else { - strcpy(banner, "New Message"); - } - } + } else { + strcpy(banner, "New Message"); + } + } #if defined(M5STACK_UNITC6L) - screen->setOn(true); - screen->showSimpleBanner(banner, 1500); - playLongBeep(); + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + playLongBeep(); #else screen->showSimpleBanner(banner, 3000); #endif + } + } + + return 0; } - } - return 0; -} + // Triggered by MeshModules + int Screen::handleUIFrameEvent(const UIFrameEvent *event) + { + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } -// Triggered by MeshModules -int Screen::handleUIFrameEvent(const UIFrameEvent *event) -{ - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } + if (showingNormalScreen) { + // Regenerate the frameset, potentially honoring a module's internal requestFocus() call + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) + setFrames(FOCUS_MODULE); - if (showingNormalScreen) { - // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) - setFrames(FOCUS_MODULE); + // Regenerate the frameset, while Attempt to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) + setFrames(FOCUS_PRESERVE); - // Regenerate the frameset, while Attempt to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) - setFrames(FOCUS_PRESERVE); + // Don't regenerate the frameset, just re-draw whatever is on screen ASAP + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) + setFastFramerate(); + } - // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) - setFastFramerate(); - } + return 0; + } - return 0; -} + int Screen::handleInputEvent(const InputEvent *event) + { + if (!screenOn) + return 0; -int Screen::handleInputEvent(const InputEvent *event) -{ - if (!screenOn) - return 0; - - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) - setFastFramerate(); // Draw ASAP + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP #endif - if (NotificationRenderer::isOverlayBannerShowing()) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); - menuHandler::handleMenuSwitch(dispdev); - return 0; - } + menuHandler::handleMenuSwitch(dispdev); + return 0; + } - // Use left or right input from a keyboard to move between frames, - // so long as a mesh module isn't using these events for some other purpose - if (showingNormalScreen) { + // Use left or right input from a keyboard to move between frames, + // so long as a mesh module isn't using these events for some other purpose + if (showingNormalScreen) { - // Ask any MeshModules if they're handling keyboard input right now - bool inputIntercepted = false; - for (MeshModule *module : moduleFrames) { - if (module && module->interceptingKeyboardInput()) - inputIntercepted = true; - } + // Ask any MeshModules if they're handling keyboard input right now + bool inputIntercepted = false; + for (MeshModule *module : moduleFrames) { + if (module && module->interceptingKeyboardInput()) + inputIntercepted = true; + } - // If no modules are using the input, move between frames - if (!inputIntercepted) { - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showNextFrame(); - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { - menuHandler::systemBaseMenu(); + // If no modules are using the input, move between frames + if (!inputIntercepted) { + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { + showNextFrame(); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + menuHandler::homeBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { + menuHandler::systemBaseMenu(); #if HAS_GPS - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { - menuHandler::positionBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + menuHandler::positionBaseMenu(); #endif - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { - menuHandler::clockMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::loraMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (devicestate.rx_text_message.from) { - menuHandler::messageResponseMenu(); - } else { + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + menuHandler::clockMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { + menuHandler::loraMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (devicestate.rx_text_message.from) { + menuHandler::messageResponseMenu(); + } else { #if defined(M5STACK_UNITC6L) - menuHandler::textMessageMenu(); + menuHandler::textMessageMenu(); #else menuHandler::textMessageBaseMenu(); #endif + } + } else if (framesetInfo.positions.firstFavorite != 255 && + this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + menuHandler::favoriteBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + menuHandler::nodeListMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); + } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); } - } else if (framesetInfo.positions.firstFavorite != 255 && - this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && - this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { - menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || - this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { - menuHandler::nodeListMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { - menuHandler::wifiBaseMenu(); } - } else if (event->inputEvent == INPUT_BROKER_BACK) { - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_CANCEL) { - setOn(false); } + + return 0; } - } - return 0; -} + int Screen::handleAdminMessage(AdminModule_ObserverData * arg) + { + switch (arg->request->which_payload_variant) { + // Node removed manually (i.e. via app) + case meshtastic_AdminMessage_remove_by_nodenum_tag: + setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; + break; -int Screen::handleAdminMessage(AdminModule_ObserverData *arg) -{ - switch (arg->request->which_payload_variant) { - // Node removed manually (i.e. via app) - case meshtastic_AdminMessage_remove_by_nodenum_tag: - setFrames(FOCUS_PRESERVE); - *arg->result = AdminMessageHandleResult::HANDLED; - break; + // Default no-op, in case the admin message observable gets used by other classes in future + default: + break; + } + return 0; + } - // Default no-op, in case the admin message observable gets used by other classes in future - default: - break; - } - return 0; -} + bool Screen::isOverlayBannerShowing() + { + return NotificationRenderer::isOverlayBannerShowing(); + } -bool Screen::isOverlayBannerShowing() -{ - return NotificationRenderer::isOverlayBannerShowing(); -} - -} // namespace graphics + } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN -bool shouldWakeOnReceivedMessage() -{ - /* - The goal here is to determine when we do NOT wake up the screen on message received: - - Any ext. notifications are turned on - - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - - If the battery level is very low - */ - if (moduleConfig.external_notification.enabled) { - return false; + bool shouldWakeOnReceivedMessage() + { + /* + The goal here is to determine when we do NOT wake up the screen on message received: + - Any ext. notifications are turned on + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE + - If the battery level is very low + */ + if (moduleConfig.external_notification.enabled) { + return false; + } + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { + return false; + } + if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { + return false; + } + return true; } - if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, - meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { - return false; - } - if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { - return false; - } - return true; -} diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 547dd0175..4ca2c197f 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -50,15 +50,15 @@ class StreamAPI : public PhoneAPI * phone. */ virtual int32_t runOncePart(); - virtual int32_t runOncePart(char *buf,uint16_t bufLen); + virtual int32_t runOncePart(char *buf, uint16_t bufLen); private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); - int32_t readStream(char *buf,uint16_t bufLen); - int32_t handleRecStream(char *buf,uint16_t bufLen); + int32_t readStream(char *buf, uint16_t bufLen); + int32_t handleRecStream(char *buf, uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream From 8264d4d65ed742b5675576aa8e08a0b5057edf4d Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 5 Sep 2025 20:44:32 -0500 Subject: [PATCH 50/62] BaseUI Updates (#7787) * Account for low resolution wide screen OLEDs * Allow picking of Device Role and new Display Formatter for Device Role * Add remainder of client roles to display formatter * Don't update the role unless you pick a value * Mascots are fun * Fix warnings during compile time * Improve some menus * Mascots need to work everywhere * Update Chirpy image * Fix Trunk * Update protobufs * Add date to Clock screen * Analog clocks love dates too * Finalize date moves for analog clock --- src/graphics/draw/ClockRenderer.cpp | 2 ++ src/graphics/draw/UIRenderer.cpp | 40 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index aec0a701f..5afcf094c 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -190,6 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -323,6 +324,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 5623c9026..e76a39398 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1056,6 +1056,45 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 2, latStr); #else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; + + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), "Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + } + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); #endif @@ -1066,6 +1105,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); #else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude === From e6adb197e44c76a378210598f115ff482200a6b9 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sat, 13 Sep 2025 15:06:36 -0500 Subject: [PATCH 51/62] Add formatting and menu picking for other GPS format options (#7974) * Add back options for other GPS format options * Rename variables and don't overlap elements * Fix default value * Should probably add a menu while I'm here! * Shorten names just a bit to fit on screens * Fix off by one * Labels try to make things better * Missed a label --- src/graphics/draw/MenuHandler.cpp | 55 ++++++++++++++++-- src/graphics/draw/MenuHandler.h | 2 + src/graphics/draw/UIRenderer.cpp | 93 +++++++++++++++++++------------ src/graphics/draw/UIRenderer.h | 3 +- 4 files changed, 112 insertions(+), 41 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 975fc7c0a..edf1d5d1f 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -633,11 +633,11 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd }; + enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; - static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"}; - static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu}; - int options = 3; + static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; + static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; + int options = 4; if (accelerometerThread) { optionsArray[options] = "Compass Calibrate"; @@ -653,6 +653,9 @@ void menuHandler::positionBaseMenu() if (selected == GPSToggle) { menuQueue = gps_toggle_menu; screen->runNow(); + } else if (selected == GPSFormat) { + menuQueue = gps_format_menu; + screen->runNow(); } else if (selected == CompassMenu) { menuQueue = compass_point_north_menu; screen->runNow(); @@ -779,6 +782,47 @@ void menuHandler::GPSToggleMenu() bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; screen->showOverlayBanner(bannerOptions); } +void menuHandler::GPSFormatMenu() +{ + + static const char *optionsArray[] = {"Back", + isHighResolution ? "Decimal Degrees" : "DEC", + isHighResolution ? "Degrees Minutes Seconds" : "DMS", + isHighResolution ? "Universal Transverse Mercator" : "UTM", + isHighResolution ? "Military Grid Reference System" : "MGRS", + isHighResolution ? "Open Location Code" : "OLC", + isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "GPS Format"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 7; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 1) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 3) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 4) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 5) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 6) { + config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR; + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuQueue = position_base_menu; + screen->runNow(); + } + }; + bannerOptions.InitialSelected = config.display.gps_format + 1; + screen->showOverlayBanner(bannerOptions); +} #endif void menuHandler::BluetoothToggleMenu() @@ -1452,6 +1496,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case gps_toggle_menu: GPSToggleMenu(); break; + case gps_format_menu: + GPSFormatMenu(); + break; #endif case compass_point_north_menu: compassNorthMenu(); diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1bfdf128f..8afcba6c8 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -19,6 +19,7 @@ class menuHandler clock_menu, position_base_menu, gps_toggle_menu, + gps_format_menu, compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -63,6 +64,7 @@ class menuHandler static void positionBaseMenu(); static void compassNorthMenu(); static void GPSToggleMenu(); + static void GPSFormatMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e76a39398..52398d905 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -116,64 +116,78 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con } // Draw GPS status coordinates -void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, + const char *mode) { auto gpsFormat = config.display.gps_format; char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { strcpy(displayLine, "No GPS present"); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + display->drawString(x, y, displayLine); } else if (!gps->getHasLock() && !config.position.fixed_position) { strcpy(displayLine, "No GPS Lock"); - display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + display->drawString(x, y, displayLine); } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { - char coordinateLine[22]; + char coordinateLine_1[22]; + char coordinateLine_2[22]; if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, - geoCoord.getLongitude() * 1e-7); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), - geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), + geoCoord.getUTMBand(), geoCoord.getUTMEasting()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), - geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), + geoCoord.getMGRSNorthing()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine); + geoCoord.getOLCCode(coordinateLine_1); + coordinateLine_2[0] = '\0'; } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region - snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); - else - snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); + coordinateLine_2[0] = '\0'; + } else { + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), + geoCoord.getOSGRNorthing()); + } } - // If fixed position, display text "Fixed GPS" alternating with the coordinates. - if (config.position.fixed_position) { - if ((millis() / 10000) % 2) { - display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, - coordinateLine); - } else { - display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else if (strcmp(mode, "combined") == 0) { + display->drawString(x, y, coordinateLine_1); + if (coordinateLine_2[0] != '\0') { + display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2); } - } else { - display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); } + } else { - char latLine[22]; - char lonLine[22]; - snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, - latLine); - display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine); + char coordinateLine_1[22]; + char coordinateLine_2[22]; + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), + geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), + geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + if (strcmp(mode, "line1") == 0) { + display->drawString(x, y, coordinateLine_1); + } else if (strcmp(mode, "line2") == 0) { + display->drawString(x, y, coordinateLine_2); + } else { // both + display->drawString(x, y, coordinateLine_1); + display->drawString(x, y + 10, coordinateLine_2); + } } } } @@ -1092,6 +1106,13 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(0, getTextPositions(display)[line++], "Last: ?"); } + // === Third Row: Line 1 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); + + if (config.display.gps_format != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { + // === Fourth Row: Line 2 GPS Info === + UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); + } // === Third Row: Latitude === char latStr[32]; snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); @@ -1108,7 +1129,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); - // === Fifth Row: Altitude === + // === Fourth/Fifth Row: Altitude === char DisplayLineTwo[32] = {0}; int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) ? ourNode->position.altitude diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index eada150f9..438d56cc2 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -38,7 +38,8 @@ class UIRenderer // GPS status functions static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, + const char *mode = "line1"); static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); From 42fbb62f18a9b29db9afc2e29ee9259b294fae9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 08:47:53 -0500 Subject: [PATCH 52/62] Update protobufs (#8038) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/config.pb.h | 49 +++++++------------ .../generated/meshtastic/device_ui.pb.cpp | 2 + src/mesh/generated/meshtastic/device_ui.pb.h | 43 ++++++++++++++-- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 2 + 7 files changed, 64 insertions(+), 38 deletions(-) diff --git a/protobufs b/protobufs index 27d9a99bd..6a8b80a10 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 27d9a99bd03efe35f91cafd7116c2386be5e26a1 +Subproject commit 6a8b80a10835acf48b2dfa2ad8aa0cc596219619 diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 59e55db3f..0453ecad2 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -173,28 +173,10 @@ typedef enum _meshtastic_Config_NetworkConfig_ProtocolFlags { meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST = 1 } meshtastic_Config_NetworkConfig_ProtocolFlags; -/* How the GPS coordinates are displayed on the OLED screen. */ -typedef enum _meshtastic_Config_DisplayConfig_GpsCoordinateFormat { - /* GPS coordinates are displayed in the normal decimal degrees format: - DD.DDDDDD DDD.DDDDDD */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC = 0, - /* GPS coordinates are displayed in the degrees minutes seconds format: - DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS = 1, - /* Universal Transverse Mercator format: - ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM = 2, - /* Military Grid Reference System format: - ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square, - E is easting, N is northing */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS = 3, - /* Open Location Code (aka Plus Codes). */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC = 4, - /* Ordnance Survey Grid Reference (the National Grid System of the UK). - Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square, - E is the easting, N is the northing */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR = 5 -} meshtastic_Config_DisplayConfig_GpsCoordinateFormat; +/* Deprecated in 2.7.4: Unused */ +typedef enum _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat { + meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED = 0 +} meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat; /* Unit display preference */ typedef enum _meshtastic_Config_DisplayConfig_DisplayUnits { @@ -491,7 +473,7 @@ typedef struct _meshtastic_Config_DisplayConfig { uint32_t screen_on_secs; /* Deprecated in 2.7.4: Unused How the GPS coordinates are formatted on the OLED screen. */ - meshtastic_Config_DisplayConfig_GpsCoordinateFormat gps_format; + meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat gps_format; /* Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds. Potentially useful for devices without user buttons. */ uint32_t auto_screen_carousel_secs; @@ -515,6 +497,9 @@ typedef struct _meshtastic_Config_DisplayConfig { /* If false (default), the device will display the time in 24-hour format on screen. If true, the device will display the time in 12-hour format on screen. */ bool use_12h_clock; + /* If false (default), the device will use short names for various display screens. + If true, node names will show in long format */ + bool use_long_node_name; } meshtastic_Config_DisplayConfig; /* Lora Config */ @@ -678,9 +663,9 @@ extern "C" { #define _meshtastic_Config_NetworkConfig_ProtocolFlags_MAX meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST #define _meshtastic_Config_NetworkConfig_ProtocolFlags_ARRAYSIZE ((meshtastic_Config_NetworkConfig_ProtocolFlags)(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST+1)) -#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC -#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR -#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_ARRAYSIZE ((meshtastic_Config_DisplayConfig_GpsCoordinateFormat)(meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR+1)) +#define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED +#define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED +#define _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat)(meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_UNUSED+1)) #define _meshtastic_Config_DisplayConfig_DisplayUnits_MIN meshtastic_Config_DisplayConfig_DisplayUnits_METRIC #define _meshtastic_Config_DisplayConfig_DisplayUnits_MAX meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL @@ -721,7 +706,7 @@ extern "C" { #define meshtastic_Config_NetworkConfig_address_mode_ENUMTYPE meshtastic_Config_NetworkConfig_AddressMode -#define meshtastic_Config_DisplayConfig_gps_format_ENUMTYPE meshtastic_Config_DisplayConfig_GpsCoordinateFormat +#define meshtastic_Config_DisplayConfig_gps_format_ENUMTYPE meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat #define meshtastic_Config_DisplayConfig_units_ENUMTYPE meshtastic_Config_DisplayConfig_DisplayUnits #define meshtastic_Config_DisplayConfig_oled_ENUMTYPE meshtastic_Config_DisplayConfig_OledType #define meshtastic_Config_DisplayConfig_displaymode_ENUMTYPE meshtastic_Config_DisplayConfig_DisplayMode @@ -742,7 +727,7 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} +#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} #define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} #define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} @@ -753,7 +738,7 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} +#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} #define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} #define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} @@ -820,6 +805,7 @@ extern "C" { #define meshtastic_Config_DisplayConfig_wake_on_tap_or_motion_tag 10 #define meshtastic_Config_DisplayConfig_compass_orientation_tag 11 #define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12 +#define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13 #define meshtastic_Config_LoRaConfig_use_preset_tag 1 #define meshtastic_Config_LoRaConfig_modem_preset_tag 2 #define meshtastic_Config_LoRaConfig_bandwidth_tag 3 @@ -965,7 +951,8 @@ X(a, STATIC, SINGULAR, UENUM, displaymode, 8) \ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \ X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \ X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \ -X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) +X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \ +X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) #define meshtastic_Config_DisplayConfig_CALLBACK NULL #define meshtastic_Config_DisplayConfig_DEFAULT NULL @@ -1043,7 +1030,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 #define meshtastic_Config_DeviceConfig_size 100 -#define meshtastic_Config_DisplayConfig_size 32 +#define meshtastic_Config_DisplayConfig_size 34 #define meshtastic_Config_LoRaConfig_size 85 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 204 diff --git a/src/mesh/generated/meshtastic/device_ui.pb.cpp b/src/mesh/generated/meshtastic/device_ui.pb.cpp index 2fc8d9461..01940265f 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.cpp +++ b/src/mesh/generated/meshtastic/device_ui.pb.cpp @@ -28,3 +28,5 @@ PB_BIND(meshtastic_Map, meshtastic_Map, AUTO) + + diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 8f693e570..d9eb90773 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -74,6 +74,32 @@ typedef enum _meshtastic_Language { meshtastic_Language_TRADITIONAL_CHINESE = 31 } meshtastic_Language; +/* How the GPS coordinates are displayed on the OLED screen. */ +typedef enum _meshtastic_DeviceUIConfig_GpsCoordinateFormat { + /* GPS coordinates are displayed in the normal decimal degrees format: + DD.DDDDDD DDD.DDDDDD */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC = 0, + /* GPS coordinates are displayed in the degrees minutes seconds format: + DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS = 1, + /* Universal Transverse Mercator format: + ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM = 2, + /* Military Grid Reference System format: + ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square, + E is easting, N is northing */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS = 3, + /* Open Location Code (aka Plus Codes). */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC = 4, + /* Ordnance Survey Grid Reference (the National Grid System of the UK). + Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square, + E is the easting, N is the northing */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR = 5, + /* Maidenhead Locator System + Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS = 6 +} meshtastic_DeviceUIConfig_GpsCoordinateFormat; + /* Struct definitions */ typedef struct _meshtastic_NodeFilter { /* Filter unknown nodes */ @@ -163,6 +189,8 @@ typedef struct _meshtastic_DeviceUIConfig { /* Clockface analog style true for analog clockface, false for digital clockface */ bool is_clockface_analog; + /* How the GPS coordinates are formatted on the OLED screen. */ + meshtastic_DeviceUIConfig_GpsCoordinateFormat gps_format; } meshtastic_DeviceUIConfig; @@ -183,9 +211,14 @@ extern "C" { #define _meshtastic_Language_MAX meshtastic_Language_TRADITIONAL_CHINESE #define _meshtastic_Language_ARRAYSIZE ((meshtastic_Language)(meshtastic_Language_TRADITIONAL_CHINESE+1)) +#define _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MIN meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC +#define _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MAX meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS +#define _meshtastic_DeviceUIConfig_GpsCoordinateFormat_ARRAYSIZE ((meshtastic_DeviceUIConfig_GpsCoordinateFormat)(meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS+1)) + #define meshtastic_DeviceUIConfig_theme_ENUMTYPE meshtastic_Theme #define meshtastic_DeviceUIConfig_language_ENUMTYPE meshtastic_Language #define meshtastic_DeviceUIConfig_compass_mode_ENUMTYPE meshtastic_CompassMode +#define meshtastic_DeviceUIConfig_gps_format_ENUMTYPE meshtastic_DeviceUIConfig_GpsCoordinateFormat @@ -193,12 +226,12 @@ extern "C" { /* Initializer values for message structs */ -#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}, false, meshtastic_Map_init_default, _meshtastic_CompassMode_MIN, 0, 0} +#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}, false, meshtastic_Map_init_default, _meshtastic_CompassMode_MIN, 0, 0, _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MIN} #define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_default {0, 0, 0} #define meshtastic_Map_init_default {false, meshtastic_GeoPoint_init_default, "", 0} -#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}, false, meshtastic_Map_init_zero, _meshtastic_CompassMode_MIN, 0, 0} +#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}, false, meshtastic_Map_init_zero, _meshtastic_CompassMode_MIN, 0, 0, _meshtastic_DeviceUIConfig_GpsCoordinateFormat_MIN} #define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, "", 0} #define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""} #define meshtastic_GeoPoint_init_zero {0, 0, 0} @@ -241,6 +274,7 @@ extern "C" { #define meshtastic_DeviceUIConfig_compass_mode_tag 16 #define meshtastic_DeviceUIConfig_screen_rgb_color_tag 17 #define meshtastic_DeviceUIConfig_is_clockface_analog_tag 18 +#define meshtastic_DeviceUIConfig_gps_format_tag 19 /* Struct field encoding specification for nanopb */ #define meshtastic_DeviceUIConfig_FIELDLIST(X, a) \ @@ -261,7 +295,8 @@ X(a, STATIC, SINGULAR, BYTES, calibration_data, 14) \ X(a, STATIC, OPTIONAL, MESSAGE, map_data, 15) \ X(a, STATIC, SINGULAR, UENUM, compass_mode, 16) \ X(a, STATIC, SINGULAR, UINT32, screen_rgb_color, 17) \ -X(a, STATIC, SINGULAR, BOOL, is_clockface_analog, 18) +X(a, STATIC, SINGULAR, BOOL, is_clockface_analog, 18) \ +X(a, STATIC, SINGULAR, UENUM, gps_format, 19) #define meshtastic_DeviceUIConfig_CALLBACK NULL #define meshtastic_DeviceUIConfig_DEFAULT NULL #define meshtastic_DeviceUIConfig_node_filter_MSGTYPE meshtastic_NodeFilter @@ -318,7 +353,7 @@ extern const pb_msgdesc_t meshtastic_Map_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size -#define meshtastic_DeviceUIConfig_size 201 +#define meshtastic_DeviceUIConfig_size 204 #define meshtastic_GeoPoint_size 33 #define meshtastic_Map_size 58 #define meshtastic_NodeFilter_size 47 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 9b6330596..c6cad8a2a 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2273 +#define meshtastic_BackupPreferences_size 2275 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index da224fb94..1fa4f33dd 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -187,7 +187,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size -#define meshtastic_LocalConfig_size 747 +#define meshtastic_LocalConfig_size 749 #define meshtastic_LocalModuleConfig_size 671 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 294f0beac..6292ce070 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -276,6 +276,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_HELTEC_V4 = 110, /* M5Stack C6L */ meshtastic_HardwareModel_M5STACK_C6L = 111, + /* M5Stack Cardputer Adv */ + meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV = 112, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From c8f69913d693b06831840781694c4a267b51a449 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sat, 13 Sep 2025 15:06:36 -0500 Subject: [PATCH 53/62] Add formatting and menu picking for other GPS format options (#7974) * Add back options for other GPS format options * Rename variables and don't overlap elements * Fix default value * Should probably add a menu while I'm here! * Shorten names just a bit to fit on screens * Fix off by one * Labels try to make things better * Missed a label From af26408d73a0adbb858caaa98b006c67f1d4f92f Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:29:53 +0800 Subject: [PATCH 54/62] Add a new GPS model CM121. (#7852) * Add a new GPS model CM121. * Add CM121 to Unicore. * Trunk fixes, remove unneded NMEA lines --------- Co-authored-by: Tom Fifield --- src/gps/GPS.cpp | 18 ++++++++++++++++-- src/gps/GPS.h | 3 ++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index cc0cfca08..a4c7464b8 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -807,6 +807,14 @@ bool GPS::setup() } else { LOG_INFO("GNSS module configuration saved!"); } + } else if (gnssModel == GNSS_MODEL_CM121) { + // only ask for RMC and GGA + // enable GGA + _serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n"); + delay(250); + // enable RMC + _serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n"); + delay(250); } didSerialInit = true; } @@ -1239,9 +1247,15 @@ GnssModel_t GPS::probe(int serialSpeed) _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); delay(20); + // Close NMEA sequences on CM121 + _serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n"); + _serial_gps->write("$CFGMSG,0,2,0,1*18\r\n"); + _serial_gps->write("$CFGMSG,0,3,0,1*19\r\n"); + delay(20); - // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A - std::vector unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}}; + // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 + std::vector unicore = { + {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); std::vector atgm = { diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 46701f611..cba767460 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -31,7 +31,8 @@ typedef enum { GNSS_MODEL_MTK_PA1616S, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352, - GNSS_MODEL_LS20031 + GNSS_MODEL_LS20031, + GNSS_MODEL_CM121 } GnssModel_t; typedef enum { From 72b9a02f3e278d409f2be114203a1969ee6162f4 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:41:22 +0100 Subject: [PATCH 55/62] (resubmission) Manual GitHub actions to allow building one target or arch (#7997) * Reset the modified files * Fix some changes * Fix some changes * Trunk. That is all. --------- Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> From 8095261dfd615a615a5b1141ddb0e03ef29d9760 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 9 Sep 2025 17:01:04 -0400 Subject: [PATCH 56/62] PPA: Enable Ubuntu 25.10 (questing) (#7940) --- .github/workflows/daily_packaging.yml | 4 ++-- .github/workflows/release_channels.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 7e2316e3d..392faeb8a 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -33,8 +33,8 @@ jobs: fail-fast: false matrix: series: - - jammy # 22.04 - - noble # 24.04 + - jammy # 22.04 LTS + - noble # 24.04 LTS - plucky # 25.04 - questing # 25.10 uses: ./.github/workflows/package_ppa.yml diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index 486f4b1a6..d5d642db4 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -21,10 +21,10 @@ jobs: fail-fast: false matrix: series: - - jammy # 22.04 - - noble # 24.04 + - jammy # 22.04 LTS + - noble # 24.04 LTS - plucky # 25.04 - # - questing # 25.10 + - questing # 25.10 uses: ./.github/workflows/package_ppa.yml with: ppa_repo: |- From f32e06a3218af120d66f02dfe23e2dd6e259b3ec Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 19 Sep 2025 10:51:07 -0500 Subject: [PATCH 57/62] Update Protobuf usage, add MLS, fix clock (#8041) --- src/graphics/draw/ClockRenderer.cpp | 23 +---- src/graphics/draw/DebugRenderer.cpp | 3 +- src/graphics/draw/MenuHandler.cpp | 22 ++-- src/graphics/draw/UIRenderer.cpp | 149 ++++++++++------------------ 4 files changed, 69 insertions(+), 128 deletions(-) diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 5afcf094c..d0c4e5c6e 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -2,6 +2,7 @@ #if HAS_SCREEN #include "ClockRenderer.h" #include "NodeDB.h" +#include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" @@ -300,15 +301,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, secondString); #endif - - // Display GPS derived date - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); - yOffset = (isHighResolution) ? 12 : 1; - display->drawString(startingHourMinuteTextX + timeStringWidth - display->getStringWidth(fullLine), - getTextPositions(display)[line] + yOffset, fullLine); } void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) @@ -522,19 +514,6 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // draw second hand display->drawLine(centerX, centerY, secondX, secondY); #endif - - display->setFont(FONT_SMALL); - // Display GPS derived date - char datetimeStr[25]; - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); - char fullLine[40]; - if (isHighResolution) { - snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); - } else { - snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); - } - display->drawString(display->getWidth() - 1 - display->getStringWidth(fullLine), getTextPositions(display)[line], - fullLine); } } diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index fb35134fd..2c641accb 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -330,8 +330,7 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #if HAS_GPS if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { // Line 3 - if (config.display.gps_format != - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); // Line 4 diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index edf1d5d1f..c98217e96 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -791,36 +791,40 @@ void menuHandler::GPSFormatMenu() isHighResolution ? "Universal Transverse Mercator" : "UTM", isHighResolution ? "Military Grid Reference System" : "MGRS", isHighResolution ? "Open Location Code" : "OLC", - isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR"}; + isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR", + isHighResolution ? "Maidenhead Locator" : "MLS"}; BannerOverlayOptions bannerOptions; bannerOptions.message = "GPS Format"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 7; + bannerOptions.optionsCount = 8; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == 2) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == 3) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == 4) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == 5) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC; service->reloadConfig(SEGMENT_CONFIG); } else if (selected == 6) { - config.display.gps_format = meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR; + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR; + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 7) { + uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS; service->reloadConfig(SEGMENT_CONFIG); } else { menuQueue = position_base_menu; screen->runNow(); } }; - bannerOptions.InitialSelected = config.display.gps_format + 1; + bannerOptions.InitialSelected = uiconfig.gps_format + 1; screen->showOverlayBanner(bannerOptions); } #endif diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 52398d905..46a7a9ea0 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -119,7 +119,7 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, const char *mode) { - auto gpsFormat = config.display.gps_format; + auto gpsFormat = uiconfig.gps_format; char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { @@ -132,25 +132,25 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { + if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { char coordinateLine_1[22]; char coordinateLine_2[22]; - if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), geoCoord.getUTMBand(), geoCoord.getUTMEasting()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code geoCoord.getOLCCode(coordinateLine_1); coordinateLine_2[0] = '\0'; - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); coordinateLine_2[0] = '\0'; @@ -160,6 +160,48 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); } + } else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System + double lat = geoCoord.getLatitude() * 1e-7; + double lon = geoCoord.getLongitude() * 1e-7; + + // Normalize + if (lat > 90.0) + lat = 90.0; + if (lat < -90.0) + lat = -90.0; + while (lon < -180.0) + lon += 360.0; + while (lon >= 180.0) + lon -= 360.0; + + double adjLon = lon + 180.0; + double adjLat = lat + 90.0; + + char maiden[10]; // enough for 8-char + null + + // Field (2 letters) + int lonField = int(adjLon / 20.0); + int latField = int(adjLat / 10.0); + adjLon -= lonField * 20.0; + adjLat -= latField * 10.0; + + // Square (2 digits) + int lonSquare = int(adjLon / 2.0); + int latSquare = int(adjLat / 1.0); + adjLon -= lonSquare * 2.0; + adjLat -= latSquare * 1.0; + + // Subsquare (2 letters) + double lonUnit = 2.0 / 24.0; + double latUnit = 1.0 / 24.0; + int lonSub = int(adjLon / lonUnit); + int latSub = int(adjLat / latUnit); + + snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare, + 'A' + lonSub, 'A' + latSub); + + snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden); + coordinateLine_2[0] = '\0'; // only need one line } if (strcmp(mode, "line1") == 0) { @@ -1014,19 +1056,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // If GPS is off, no need to display these parts if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { - /* MUST BE MOVED TO CLOCK SCREEN - // === Second Row: Date === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char datetimeStr[25]; - bool showTime = false; // set to true for full datetime - UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); -#if !defined(M5STACK_UNITC6L) - display->drawString(0, getTextPositions(display)[line++], fullLine); -#endif - */ - // === Second Row: Last GPS Fix === if (gpsStatus->getLastFixMillis() > 0) { uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix @@ -1039,54 +1068,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU #if defined(USE_EINK) // E-Ink: skip seconds, show only days/hours/mins if (days > 0) { - snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); } else if (hours > 0) { - snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); } else { - snprintf(buf, sizeof(buf), " Last: %um", mins); - } -#else - // Non E-Ink: include seconds where useful - if (days > 0) { - snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); - } else if (mins > 0) { - snprintf(buf, sizeof(buf), " Last: %um %us", mins, secs); - } else { - snprintf(buf, sizeof(buf), " Last: %us", secs); - } -#endif - - display->drawString(0, getTextPositions(display)[line++], buf); - } else { - display->drawString(0, getTextPositions(display)[line++], " Last: ?"); - } - - // === Third Row: Latitude === - char latStr[32]; -#if defined(M5STACK_UNITC6L) - snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++] + 2, latStr); -#else - snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); - // === Second Row: Last GPS Fix === - if (gpsStatus->getLastFixMillis() > 0) { - uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix - uint32_t days = delta / 86400; - uint32_t hours = (delta % 86400) / 3600; - uint32_t mins = (delta % 3600) / 60; - uint32_t secs = delta % 60; - - char buf[32]; -#if defined(USE_EINK) - // E-Ink: skip seconds, show only days/hours/mins - if (days > 0) { - snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); - } else { - snprintf(buf, sizeof(buf), " Last: %um", mins); + snprintf(buf, sizeof(buf), "Last: %um", mins); } #else // Non E-Ink: include seconds where useful @@ -1109,38 +1095,11 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // === Third Row: Line 1 GPS Info === UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); - if (config.display.gps_format != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { + if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && + uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // === Fourth Row: Line 2 GPS Info === UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2"); } - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++], latStr); -#endif - - // === Fourth Row: Longitude === - char lonStr[32]; -#if defined(M5STACK_UNITC6L) - snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); -#else - snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, getTextPositions(display)[line++], lonStr); - - // === Fourth/Fifth Row: Altitude === - char DisplayLineTwo[32] = {0}; - int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode)) - ? ourNode->position.altitude - : geoCoord.getAltitude(); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); - } else { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude()); - } - display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); -#endif } #if !defined(M5STACK_UNITC6L) // === Draw Compass if heading is valid === From 3a63a56cffba779b5f0973c493e4d22c22b22bfe Mon Sep 17 00:00:00 2001 From: WillyJL Date: Fri, 19 Sep 2025 19:00:59 +0200 Subject: [PATCH 58/62] Fix build fail on develop branch (#8043) --- src/graphics/draw/ClockRenderer.cpp | 1 + src/graphics/draw/MenuHandler.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index d0c4e5c6e..751db8d88 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -8,6 +8,7 @@ #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/UIRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 510f8097a..56477258f 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -85,7 +85,6 @@ class menuHandler static void powerMenu(); static void FrameToggles_menu(); static void textMessageMenu(); - static void FrameToggles_menu(); private: static void saveUIConfig(); From 6677255f6cff797e107e311f1d4af30d4c382ce6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 12:09:56 -0500 Subject: [PATCH 59/62] Fix --- src/graphics/draw/UIRenderer.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 2da53bda4..6844aa50f 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -117,10 +117,7 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con // Draw GPS status coordinates void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, - const char *mode) void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, - int16_t y, - const meshtastic::GPSStatus *gps, - const char *mode) + const char *mode) { auto gpsFormat = uiconfig.gps_format; char displayLine[32]; @@ -1377,4 +1374,5 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi } // namespace graphics +#endif // HAS_GPS #endif // HAS_SCREEN \ No newline at end of file From 787642ad4c4d063fc5bfe4248a60bbe63ad9ceab Mon Sep 17 00:00:00 2001 From: WillyJL Date: Fri, 19 Sep 2025 22:17:31 +0200 Subject: [PATCH 60/62] Fix more build failures (#8044) --- src/graphics/draw/UIRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 6844aa50f..202a28835 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1184,6 +1184,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } } #endif +#endif // HAS_GPS } #ifdef USERPREFS_OEM_TEXT @@ -1374,5 +1375,4 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi } // namespace graphics -#endif // HAS_GPS #endif // HAS_SCREEN \ No newline at end of file From fdc879605233202b132a4c64487ff515a6bf8a38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:50:33 -0500 Subject: [PATCH 61/62] Update protobufs (#8045) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- src/mesh/generated/meshtastic/module_config.pb.h | 13 +++++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/protobufs b/protobufs index 6a8b80a10..46b81e822 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 6a8b80a10835acf48b2dfa2ad8aa0cc596219619 +Subproject commit 46b81e822af1b8e408f437092337f129dee693e6 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index c6cad8a2a..7fab82ff7 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2275 +#define meshtastic_BackupPreferences_size 2277 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 1fa4f33dd..3ab6f02c1 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -188,7 +188,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size #define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 671 +#define meshtastic_LocalModuleConfig_size 673 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 16c4c230c..47d3b5baa 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -356,6 +356,9 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig { uint32_t health_update_interval; /* Enable/Disable the health telemetry module on-device display */ bool health_screen_enabled; + /* Enable/Disable the device telemetry module to send metrics to the mesh + Note: We will still send telemtry to the connected phone / client every minute over the API */ + bool device_telemetry_enabled; } meshtastic_ModuleConfig_TelemetryConfig; /* Canned Messages Module Config */ @@ -523,7 +526,7 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} @@ -539,7 +542,7 @@ extern "C" { #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0} -#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} @@ -627,6 +630,7 @@ extern "C" { #define meshtastic_ModuleConfig_TelemetryConfig_health_measurement_enabled_tag 11 #define meshtastic_ModuleConfig_TelemetryConfig_health_update_interval_tag 12 #define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13 +#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14 #define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2 #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3 @@ -825,7 +829,8 @@ X(a, STATIC, SINGULAR, UINT32, power_update_interval, 9) \ X(a, STATIC, SINGULAR, BOOL, power_screen_enabled, 10) \ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \ X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \ -X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) +X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \ +X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) #define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL #define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL @@ -910,7 +915,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 -#define meshtastic_ModuleConfig_TelemetryConfig_size 46 +#define meshtastic_ModuleConfig_TelemetryConfig_size 48 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 From 9b6a7ed3bb1bea5d4eb949be04cfb2c206de6951 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 16:00:24 -0500 Subject: [PATCH 62/62] Fix icon --- src/graphics/img/icon_small.xbm | 88 ++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/graphics/img/icon_small.xbm b/src/graphics/img/icon_small.xbm index e320a1fea..97884edad 100644 --- a/src/graphics/img/icon_small.xbm +++ b/src/graphics/img/icon_small.xbm @@ -27,4 +27,90 @@ static uint8_t icon_bits[] = { 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; -#endif \ No newline at end of file +#endif + +// Chirpy image definitions for M5STACK_UNITC6L compatibility +#define chirpy_width 38 +#define chirpy_height 50 +static unsigned char chirpy[] = { + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, + 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, + 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, + 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, + 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, + 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, + 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, + 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, + 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + +#define chirpy_width_hirez 76 +#define chirpy_height_hirez 100 +static unsigned char chirpy_hirez[] = { + 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, + 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, + 0x03, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, + 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, + 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, + 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, + 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, + 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, + 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, + 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, + 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, + 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, + 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, + 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, + 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, + 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, + 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, + 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, + 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, + 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, + 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, + 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, + 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, + 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, + 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x1f, + 0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, + 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03, + 0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e, + 0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00, + 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, + 0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, + 0x03, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07, + 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, + 0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc, + 0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03, + 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00, + 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x80, 0x07, + 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0xc0, 0x03, 0x00, + 0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3 +}; + +#define chirpy_small_image_width 8 +#define chirpy_small_image_height 8 +static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; \ No newline at end of file