diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index c048b7ac2..e93796614 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -100,7 +100,7 @@ runs: id: version - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/actions/setup-base/action.yml b/.github/actions/setup-base/action.yml index 80f5c6855..8e461998a 100644 --- a/.github/actions/setup-base/action.yml +++ b/.github/actions/setup-base/action.yml @@ -8,8 +8,6 @@ runs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install dependencies shell: bash diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0142c57a2..8f7670fa5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,16 +4,16 @@ - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback - is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc... + is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc... - Please do not check in files that don't have real changes - Please do not reformat lines that you didn't have to change the code on -- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version), +- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version), because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines. - If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description. - If your other co-developers have comments on your PR please tweak as needed. - Please also enable "Allow edits by maintainers". - Please do not submit untested code. -- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes. +- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes. - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord ## ๐Ÿค Attestations diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index de114be1c..d1bcd8898 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -28,8 +27,6 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash @@ -42,7 +39,8 @@ jobs: sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -60,11 +58,11 @@ jobs: run: debian/ci_pack_sdeb.sh env: SERIES: ${{ inputs.series }} - GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }} + GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }} PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src overwrite: true diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 23690766a..470104688 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -26,8 +26,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Build ${{ inputs.platform }} id: build @@ -111,7 +109,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-firmware with: name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} @@ -127,7 +125,7 @@ jobs: release/device-*.bat - name: Store manifests as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 9cc0bac78..806df4e7a 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -4,9 +4,14 @@ on: workflow_dispatch: inputs: # trunk-ignore(checkov/CKV_GHA_7) + target: + type: string + required: false + description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. arch: type: choice options: + - all - esp32 - esp32s3 - esp32c3 @@ -15,32 +20,18 @@ on: - rp2040 - rp2350 - stm32 - target: - type: string - required: false - description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets. - # find-target: - # type: boolean - # default: true - # description: 'Find the available targets' + description: Choose an arch to limit the search, or 'all' to search all architectures. + default: all permissions: read-all jobs: find-targets: - if: ${{ inputs.target == '' }} strategy: fail-fast: false matrix: arch: - - esp32 - - esp32s3 - - esp32c3 - - esp32c6 - - nrf52840 - - rp2040 - - rp2350 - - stm32 + - all runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -51,14 +42,37 @@ jobs: - run: pip install -U platformio - name: Generate matrix id: jsonStep + env: + BUILDTARGET: ${{ inputs.target }} + MATRIXARCH: ${{ inputs.arch }} run: | TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra) - echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY - echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY - echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY - echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY - echo "Targets:" >> $GITHUB_STEP_SUMMARY - echo $TARGETS | jq -r 'sort_by(.board) |.[] | "- " + .board' >> $GITHUB_STEP_SUMMARY + if [ "$BUILDTARGET" = "" ]; then + echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY + echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY + echo "Arch: $MATRIXARCH" >> $GITHUB_STEP_SUMMARY + echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY + echo "## ๐ŸŽฏ The following target boards are available to build:" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo $TARGETS | jq -r 'sort_by(.board) | sort_by(.platform) |.[] | "| " + .platform + " | " + .board + " |" ' >> $GITHUB_STEP_SUMMARY + else + echo "We build this one:" >> $GITHUB_STEP_SUMMARY + ARCH=$(echo "$TARGETS" | jq --arg BUILDTARGET "$BUILDTARGET" -r '.[] | select(.board==$BUILDTARGET) | .platform') + echo "| Platform | Board |" >> $GITHUB_STEP_SUMMARY + echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY + echo "| $ARCH | "$BUILDTARGET" |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "$ARCH" == "" ]]; then + echo "## โŒ Error: Target "$BUILDTARGET" not found!" >> $GITHUB_STEP_SUMMARY + else + echo "## โœ… Target "$BUILDTARGET" found, proceeding to build." >> $GITHUB_STEP_SUMMARY + fi + echo "You may need to refresh this page to make the built firmware appear below." >> $GITHUB_STEP_SUMMARY + echo "arch=$ARCH" >> $GITHUB_OUTPUT + fi + outputs: + arch: ${{ steps.jsonStep.outputs.arch }} version: if: ${{ inputs.target != '' }} @@ -78,16 +92,16 @@ jobs: build: if: ${{ inputs.target != '' && inputs.arch != 'native' }} - needs: [version] + needs: [version, find-targets] uses: ./.github/workflows/build_firmware.yml with: version: ${{ needs.version.outputs.long }} pio_env: ${{ inputs.target }} - platform: ${{ inputs.arch }} + platform: ${{ needs.find-targets.outputs.arch }} gather-artifacts: permissions: - contents: write + contents: read pull-requests: write runs-on: ubuntu-latest needs: [version, build] @@ -98,7 +112,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-*-* @@ -111,7 +125,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true @@ -127,7 +141,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -146,7 +160,7 @@ jobs: run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 392faeb8a..16363f562 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: push: branches: - - master + - develop # Default branch, same as 'cron' above paths: - debian/** - "*.rpkg" @@ -16,7 +16,7 @@ on: - .github/workflows/hook_copr.yml permissions: - contents: write + contents: read packages: write jobs: @@ -35,8 +35,8 @@ jobs: series: - jammy # 22.04 LTS - noble # 24.04 LTS - - plucky # 25.04 - questing # 25.10 + - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: ppa:meshtastic/daily diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 8d19af894..d9b23a7e8 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -37,7 +37,7 @@ on: value: ${{ jobs.docker-build.outputs.digest }} permissions: - contents: write + contents: read packages: write jobs: @@ -50,35 +50,41 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - - name: Docker login - if: ${{ inputs.push }} - uses: docker/login-action@v3 - with: - username: meshtastic - password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 + with: + cache-image: true - name: Docker setup - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 + with: + # Add Google/Amazon DockerHub mirrors to buildkit config + # https://docs.docker.com/build/ci/github-actions/configure-builder/#registry-mirror + buildkitd-config-inline: | + [registry."docker.io"] + mirrors = ["mirror.gcr.io", "public.ecr.aws"] - name: Sanitize platform string id: sanitize_platform # Replace slashes with underscores run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT + - name: Docker login + if: ${{ inputs.push }} + uses: docker/login-action@v4 + with: + username: meshtastic + password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} + - name: Docker tag id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | @@ -86,7 +92,7 @@ jobs: flavor: latest=false - name: Docker build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 id: docker_variant with: context: . @@ -97,3 +103,6 @@ jobs: platforms: ${{ inputs.platform }} build-args: | PIO_ENV=${{ inputs.pio_env }} + # Cache image layers in GitHub Actions cache to speed up subsequent builds. + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/docker_manifest.yml b/.github/workflows/docker_manifest.yml index 396ddb68e..b2fd12599 100644 --- a/.github/workflows/docker_manifest.yml +++ b/.github/workflows/docker_manifest.yml @@ -12,7 +12,7 @@ on: type: string permissions: - contents: write + contents: read packages: write jobs: @@ -86,8 +86,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: | @@ -139,14 +137,14 @@ jobs: id: tags - name: Docker login - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: meshtastic password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} - name: Docker meta (Debian) id: meta_debian - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | @@ -167,7 +165,7 @@ jobs: - name: Docker meta (Alpine) id: meta_alpine - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: meshtastic/meshtasticd tags: | diff --git a/.github/workflows/hook_copr.yml b/.github/workflows/hook_copr.yml index eb4ebc57b..c419848a8 100644 --- a/.github/workflows/hook_copr.yml +++ b/.github/workflows/hook_copr.yml @@ -11,8 +11,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-copr-hook: @@ -22,8 +21,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{ github.ref }} - repository: ${{ github.repository }} - name: Trigger COPR build uses: vidplace7/copr-build@main diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 6b48e8128..f0b16a31f 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -15,8 +15,7 @@ on: - "**.md" - version.properties - # Note: This is different from "pull_request". Need to specify ref when doing checkouts. - pull_request_target: + pull_request: branches: - master - develop @@ -29,6 +28,8 @@ on: workflow_dispatch: +permissions: read-all + jobs: setup: strategy: @@ -88,8 +89,6 @@ jobs: - uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} uses: meshtastic/gh-action-firmware@main with: @@ -126,9 +125,16 @@ jobs: test-native: if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} + permissions: # Needed for dorny/test-reporter. + contents: read + actions: read + checks: write uses: ./.github/workflows/test_native.yml docker: + permissions: # Needed for pushing to GHCR. + contents: read + packages: write strategy: fail-fast: false matrix: @@ -153,9 +159,6 @@ jobs: gather-artifacts: # trunk-ignore(checkov/CKV2_GHA_1) if: github.repository == 'meshtastic/firmware' - permissions: - contents: write - pull-requests: write strategy: fail-fast: false matrix: @@ -173,11 +176,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -187,7 +187,7 @@ jobs: run: ls -R - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -205,7 +205,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -224,20 +224,13 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} 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 }} - shame: if: github.repository == 'meshtastic/firmware' continue-on-error: true @@ -245,42 +238,44 @@ jobs: needs: [build] steps: - uses: actions/checkout@v6 - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' with: filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) fetch-depth: 0 - name: Download the current manifests - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: path: ./manifests-new/ pattern: manifest-* merge-multiple: true - name: Upload combined manifests for later commit and global stats crunching. - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifests-${{ github.sha }} overwrite: true path: manifests-new/*.mt.json - name: Find the merge base - if: github.event_name == 'pull_request_target' + if: github.event_name == 'pull_request' run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV env: base: ${{ github.base_ref }} head: ${{ github.sha }} # Currently broken (for-loop through EVERY artifact -- rate limiting) # - name: Download the old manifests - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/ # env: # GH_TOKEN: ${{ github.token }} # merge_base: ${{ env.MERGE_BASE }} # repo: ${{ github.repository }} # - name: Do scan and post comment - # if: github.event_name == 'pull_request_target' + # if: github.event_name == 'pull_request' # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ release-artifacts: + permissions: # Needed for 'gh release upload'. + contents: write runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: @@ -314,7 +309,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 id: create_release with: draft: true @@ -324,14 +319,14 @@ jobs: body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 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@v7 + uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -355,7 +350,7 @@ jobs: }' > firmware-${{ needs.version.outputs.long }}.json - name: Save Release manifest artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: manifest-${{ needs.version.outputs.long }} overwrite: true @@ -372,6 +367,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-firmware: + permissions: # Needed for 'gh release upload'. + contents: write strategy: fail-fast: false matrix: @@ -396,7 +393,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -413,7 +410,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -454,14 +451,14 @@ jobs: python-version: 3.x - name: Get firmware artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Get manifest artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml deleted file mode 100644 index bd3f6d4eb..000000000 --- a/.github/workflows/merge_queue.yml +++ /dev/null @@ -1,371 +0,0 @@ -name: Merge Queue -# 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 -on: - # Merge group is a special trigger that is used to trigger the workflow when a merge group is created. - merge_group: - -jobs: - setup: - strategy: - fail-fast: true - matrix: - arch: - - all - - check - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - 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}} --level pr) - fi - echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF" - echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT - outputs: - all: ${{ steps.jsonStep.outputs.all }} - check: ${{ steps.jsonStep.outputs.check }} - - version: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - 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: - check: ${{ fromJson(needs.setup.outputs.check) }} - - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_dispatch' }} - steps: - - uses: actions/checkout@v6 - - name: Build base - id: base - uses: ./.github/actions/setup-base - - name: Check ${{ matrix.check.board }} - run: bin/check-all.sh ${{ matrix.check.board }} - - build: - needs: [setup, version] - strategy: - matrix: - build: ${{ fromJson(needs.setup.outputs.all) }} - uses: ./.github/workflows/build_firmware.yml - with: - version: ${{ needs.version.outputs.long }} - pio_env: ${{ matrix.build.board }} - platform: ${{ matrix.build.platform }} - - 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: - strategy: - fail-fast: false - matrix: - distro: [debian, alpine] - platform: [linux/amd64, linux/arm64, linux/arm/v7] - pio_env: [native, native-tft] - exclude: - - distro: alpine - platform: linux/arm/v7 - - pio_env: native-tft - platform: linux/arm64 - - pio_env: native-tft - platform: linux/arm/v7 - uses: ./.github/workflows/docker_build.yml - with: - distro: ${{ matrix.distro }} - platform: ${{ matrix.platform }} - runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} - pio_env: ${{ matrix.pio_env }} - push: false - - gather-artifacts: - # trunk-ignore(checkov/CKV2_GHA_1) - 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] - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - - - uses: actions/download-artifact@v7 - 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@v6 - with: - name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} - overwrite: true - path: | - ./firmware-*.bin - ./firmware-*.uf2 - ./firmware-*.hex - ./firmware-*.zip - ./device-*.sh - ./device-*.bat - ./littlefs-*.bin - ./bleota*bin - ./Meshtastic_nRF52_factory_erase*.uf2 - retention-days: 30 - - - uses: actions/download-artifact@v7 - 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 || true - chmod +x ./output/device-update.sh || true - - - 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@v6 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - 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@v6 - - - 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@v7 - 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@v7 - 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@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v7 - 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 || true - chmod +x ./output/device-update.sh || true - - - name: Zip firmware - run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - - uses: actions/download-artifact@v7 - with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} - 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@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - - uses: actions/download-artifact@v7 - 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/models_issue_triage.yml b/.github/workflows/models_issue_triage.yml index ef12885f8..a02646ea0 100644 --- a/.github/workflows/models_issue_triage.yml +++ b/.github/workflows/models_issue_triage.yml @@ -38,7 +38,7 @@ jobs: - name: Apply quality label if needed if: steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' - uses: actions/github-script@v8 + uses: actions/github-script@v9 env: QUALITY_LABEL: ${{ steps.quality.outputs.response }} with: @@ -80,7 +80,7 @@ jobs: # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - name: Determine if completeness check should be skipped if: steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '' - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: check-skip with: script: | @@ -134,7 +134,7 @@ jobs: - name: Process analysis result if: (steps.quality.outputs.response == 'ok' || steps.quality.outputs.response == '') && steps.check-skip.outputs.should_skip != 'true' && steps.analysis.outputs.response != '' - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: process env: AI_RESPONSE: ${{ steps.analysis.outputs.response }} @@ -174,7 +174,7 @@ jobs: - name: Apply triage label if: steps.process.outputs.label != '' && steps.process.outputs.label != 'none' - uses: actions/github-script@v8 + uses: actions/github-script@v9 env: LABEL_NAME: ${{ steps.process.outputs.label }} with: @@ -200,7 +200,7 @@ jobs: - name: Comment on issue if: steps.process.outputs.should_comment == 'true' - uses: actions/github-script@v8 + uses: actions/github-script@v9 env: COMMENT_BODY: ${{ steps.process.outputs.comment_body }} with: diff --git a/.github/workflows/models_pr_triage.yml b/.github/workflows/models_pr_triage.yml index d4c8509d2..f39ee4845 100644 --- a/.github/workflows/models_pr_triage.yml +++ b/.github/workflows/models_pr_triage.yml @@ -22,7 +22,7 @@ jobs: # Step 1: Check if PR already has automation/type labels (skip if so) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - name: Check existing labels - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: check-labels with: script: | @@ -58,7 +58,7 @@ jobs: - name: Apply quality label if needed if: steps.check-labels.outputs.skip_all != 'true' && steps.quality.outputs.response != '' && steps.quality.outputs.response != 'ok' - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: quality-label env: QUALITY_LABEL: ${{ steps.quality.outputs.response }} @@ -113,7 +113,7 @@ jobs: - name: Apply type label if: steps.check-labels.outputs.skip_all != 'true' && steps.check-labels.outputs.has_type_label != 'true' && steps.classify.outputs.response != '' - uses: actions/github-script@v8 + uses: actions/github-script@v9 env: TYPE_LABEL: ${{ steps.classify.outputs.response }} with: diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 63f1fe8a0..b491f0062 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -18,8 +18,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -58,7 +57,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 82ffe66e9..6bd256f52 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -16,8 +16,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: pkg-pio-libdeps: @@ -27,8 +26,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Setup Python uses: actions/setup-python@v6 @@ -54,9 +51,19 @@ jobs: PLATFORMIO_LIBDEPS_DIR: pio/libdeps PLATFORMIO_PACKAGES_DIR: pio/packages PLATFORMIO_CORE_DIR: pio/core + PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0 + PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650 + PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240 + + - name: Mangle platformio cache + # Add "1" to epoch-timestamps of all downloads in the cache. + # This is a hack to prevent internet access at build-time. + run: | + cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak + jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 9a463dbea..c51e64e78 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -5,6 +5,8 @@ on: secrets: PPA_GPG_PRIVATE_KEY: required: true + PPA_SFTP_PRIVATE_KEY: + required: true inputs: ppa_repo: description: Meshtastic PPA to target @@ -16,8 +18,7 @@ on: type: string permissions: - contents: write - packages: write + contents: read jobs: build-debian-src: @@ -28,6 +29,7 @@ jobs: build_location: ppa package-ppa: + if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} runs-on: ubuntu-24.04 needs: build-debian-src steps: @@ -36,17 +38,15 @@ jobs: with: submodules: recursive path: meshtasticd - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Install deps shell: bash run: | sudo apt-get update -y --fix-missing - sudo apt-get install -y dput + sudo apt-get install -y dput openssh-client - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true @@ -68,7 +68,42 @@ jobs: - name: Display structure of downloaded files run: ls -lah - - name: Publish with dput - if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }} + - name: Trust Launchpad's SSH key run: | - dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes + mkdir -p ~/.ssh + ssh-keyscan -H ppa.launchpad.net >> ~/.ssh/known_hosts + + - name: Setup dput config + env: + ppa_login: meshtasticorg + run: | + sudo tee /etc/meshtastic-dput.cf >/dev/null < + dput -c /etc/meshtastic-dput.cf + ssh-${up_ppa_repo} + meshtasticd_${up_version}~${up_series}_source.changes diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index d60c9c8ca..bf2239f63 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check for PR labels - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 6306d777f..e3321712e 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -50,7 +50,7 @@ jobs: - name: Download test artifacts if: needs.native-tests.result != 'skipped' - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true @@ -177,7 +177,7 @@ jobs: - name: Comment test results on PR if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index 7f925b67c..a85979c72 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -23,8 +23,8 @@ jobs: series: - jammy # 22.04 LTS - noble # 24.04 LTS - - plucky # 25.04 - questing # 25.10 + - resolute # 26.04 LTS uses: ./.github/workflows/package_ppa.yml with: ppa_repo: |- diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d93449d6d..95e5c2c3d 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -33,7 +33,7 @@ jobs: # step 3 - name: save report as pipeline artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: report.sarif overwrite: true diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index b527c2fd9..2fabf0591 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -16,8 +16,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -59,7 +57,7 @@ jobs: id: version - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} @@ -72,8 +70,6 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} submodules: recursive - name: Setup native build @@ -94,7 +90,7 @@ jobs: - name: Save test results if: always() # run this step even if previous step failed - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true @@ -108,7 +104,7 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} @@ -128,29 +124,26 @@ jobs: if: always() steps: - uses: actions/checkout@v6 - with: - ref: ${{github.event.pull_request.head.ref}} - repository: ${{github.event.pull_request.head.repo.full_name}} - name: Get release version string run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT id: version - name: Download test artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.5.0 + uses: dorny/test-reporter@v3.0.0 with: name: PlatformIO Tests path: testreport.xml reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report @@ -163,7 +156,7 @@ jobs: genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report - name: Save Code Coverage Report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: code-coverage-report-${{ steps.version.outputs.long }} path: code-coverage-report diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 241f2cd10..be5142843 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: node-version: 24 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: version: latest diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 35565d1e4..e9380467e 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -6,7 +6,7 @@ permissions: read-all jobs: update-protobufs: runs-on: ubuntu-latest - permissions: + permissions: # Needed for peter-evans/create-pull-request. contents: write pull-requests: write steps: diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index a1d07145a..d0cbaa8bc 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,31 +4,31 @@ cli: plugins: sources: - id: trunk - ref: v1.7.4 + ref: v1.7.6 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.506 - - renovate@43.31.9 + - checkov@3.2.517 + - renovate@43.110.9 - prettier@3.8.1 - - trufflehog@3.93.4 + - trufflehog@3.94.3 - yamllint@1.38.0 - - bandit@1.9.3 - - trivy@0.69.1 + - bandit@1.9.4 + - trivy@0.69.3 - taplo@0.10.0 - - ruff@0.15.2 - - isort@8.0.0 - - markdownlint@0.47.0 + - ruff@0.15.9 + - isort@8.0.1 + - markdownlint@0.48.0 - oxipng@10.1.0 - - svgo@4.0.0 - - actionlint@1.7.11 + - svgo@4.0.1 + - actionlint@1.7.12 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 - shellcheck@0.11.0 - - black@26.1.0 + - black@26.3.1 - git-diff-check - - gitleaks@8.30.0 + - gitleaks@8.30.1 - clang-format@16.0.3 ignore: - linters: [ALL] diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 3a160707e..ced166bdc 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -187,6 +187,7 @@ Logging: LogLevel: info # debug, info, warn, error # TraceFile: /var/log/meshtasticd.json # JSONFile: /packets.json # File location for JSON output of decoded packets +# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation # JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout @@ -224,3 +225,4 @@ General: AvailableDirectory: /etc/meshtasticd/available.d/ # MACAddress: AA:BB:CC:DD:EE:FF # MACAddressSource: eth0 +# APIPort: 4403 diff --git a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml index 825ab2699..a854dff5b 100644 --- a/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml +++ b/bin/config.d/OpenWRT/BananaPi-BPI-R4-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: BananaPi-BPI-R4-sx1262 + support: community + compatible: + - bananapi_bpi-r4 # OpenWrt target + Lora: Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header ## CS: 28 diff --git a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml index ca5b27ebc..7687e0f58 100644 --- a/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml +++ b/bin/config.d/OpenWRT/OpenWRT-One-mikroBUS-LR-IOT-CLICK.yaml @@ -1,4 +1,10 @@ ## https://www.mikroe.com/lr-iot-click +Meta: + name: OpenWRT One mikroBUS LR-IOT-CLICK + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK # CS: 25 diff --git a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml index 6dc1e870d..ab0b62810 100644 --- a/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml +++ b/bin/config.d/OpenWRT/OpenWRT_One_mikroBUS_sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: OpenWRT One mikroBUS sx1262 + support: community + compatible: + - openwrt_one # OpenWrt target + Lora: Module: sx1262 IRQ: 10 diff --git a/bin/config.d/README.md b/bin/config.d/README.md new file mode 100644 index 000000000..b199fb439 --- /dev/null +++ b/bin/config.d/README.md @@ -0,0 +1,28 @@ +# meshtasticd configuration files + +This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware. + +## Metadata structure + +Each configuration file includes a `Meta` section that provides information about the configuration. +This configuration is consumed by configuration-selection tools. + +```yaml +Meta: + name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration. + support: community # community, official, or deprecated; determined by Meshtastic Leads. + compatible: # A list of compatible products or platforms. + - raspberry-pi +``` +`name`: A unique identifier for the configuration, typically reflecting the hardware it supports. + +`support`: Indicates the level of support for this configuration. It can be one of the following: + +- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration. +- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration. +- `deprecated`: No longer recommended for deployment by Meshtastic. + +`compatible`: A list of compatible products or platforms that can use this configuration. +This will vary depending on the intended use case / platform. +Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value. +These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc. diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml index d37f6cf6a..e6b4f8271 100644 --- a/bin/config.d/display-waveshare-1-44.yaml +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 1.44inch LCD HAT + support: community + compatible: + - raspberry-pi + ### Waveshare 1.44inch LCD HAT Display: Panel: ST7735S diff --git a/bin/config.d/display-waveshare-2.8.yaml b/bin/config.d/display-waveshare-2.8.yaml index 2e28276d8..586d1107e 100644 --- a/bin/config.d/display-waveshare-2.8.yaml +++ b/bin/config.d/display-waveshare-2.8.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare 2.8inch LCD HAT + support: community + compatible: + - raspberry-pi + Display: ### Waveshare 2.8inch RPi LCD diff --git a/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml new file mode 100644 index 000000000..2f2b24603 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_E80-900M2213S.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, HIGH, LOW] + MODE_TX: [HIGH, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + MODE_TX_HF: [LOW, LOW, LOW] + MODE_GNSS: [LOW, LOW, HIGH] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_LR1121 generic.yaml b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml new file mode 100644 index 000000000..c66eebed5 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_LR1121 generic.yaml @@ -0,0 +1,46 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: + - DIO5 + - DIO6 + MODE_STBY: + - LOW + - LOW + MODE_RX: + - HIGH + - LOW + MODE_TX: + - HIGH + - HIGH + MODE_TX_HP: + - LOW + - HIGH + MODE_TX_HF: + - LOW + - LOW + MODE_GNSS: + - LOW + - LOW + MODE_WIFI: + - LOW + - LOW + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml new file mode 100644 index 000000000..c2ab76d46 --- /dev/null +++ b/bin/config.d/femtofox/femtofox_WIO-LR1121.yaml @@ -0,0 +1,30 @@ +--- +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 + +rfswitch_table: + pins: [DIO5, DIO6, DIO7] + MODE_STBY: [LOW, LOW, LOW] + MODE_RX: [LOW, LOW, LOW] + MODE_TX: [LOW, HIGH, LOW] + MODE_TX_HP: [HIGH, LOW, LOW] + # MODE_TX_HF: [] + # MODE_GNSS: [] + MODE_WIFI: [LOW, LOW, LOW] + +General: + MACAddressSource: eth0 diff --git a/bin/config.d/lora-Adafruit-RFM9x.yaml b/bin/config.d/lora-Adafruit-RFM9x.yaml index 20295dc72..1258af4f5 100644 --- a/bin/config.d/lora-Adafruit-RFM9x.yaml +++ b/bin/config.d/lora-Adafruit-RFM9x.yaml @@ -1,3 +1,9 @@ +Meta: + name: Adafruit RFM9x + support: deprecated + compatible: + - raspberry-pi + Lora: Module: RF95 # Adafruit RFM9x Reset: 25 diff --git a/bin/config.d/lora-MeshAdv-900M30S.yaml b/bin/config.d/lora-MeshAdv-900M30S.yaml index 5c148bf68..c90391cb0 100644 --- a/bin/config.d/lora-MeshAdv-900M30S.yaml +++ b/bin/config.d/lora-MeshAdv-900M30S.yaml @@ -1,5 +1,11 @@ # MeshAdv-Pi E22-900M30S # https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 CS: 21 diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index b47b5c996..d878bce1b 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -1,5 +1,11 @@ # MeshAdv Mini E22-900M22S # https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Ebyte E22-900M22S CS: 8 diff --git a/bin/config.d/lora-RAK6421-13300-slot1.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml index 6f65f9ccd..a88544896 100644 --- a/bin/config.d/lora-RAK6421-13300-slot1.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -1,11 +1,20 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml index cbc794d39..40b0cea09 100644 --- a/bin/config.d/lora-RAK6421-13300-slot2.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -1,8 +1,17 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 2 pins + ### RAK13300 in Slot 2 pins IRQ: 18 #IO6 Reset: 24 # IO4 Busy: 19 # IO5 # Ant_sw: 23 # IO3 + Enable_Pins: + - 26 + - 23 spidev: spidev0.1 # CS: 7 \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13302-slot1.yaml b/bin/config.d/lora-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..85b934ce6 --- /dev/null +++ b/bin/config.d/lora-RAK6421-13302-slot1.yaml @@ -0,0 +1,22 @@ +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: official + compatible: + - raspberry-pi + +Lora: + + ### RAK13302 in Slot 1 + Module: sx1262 + IRQ: 22 #IO6 + Reset: 16 # IO4 + Busy: 24 # IO5 + # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: 8 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13302-slot2.yaml b/bin/config.d/lora-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..5aa23911f --- /dev/null +++ b/bin/config.d/lora-RAK6421-13302-slot2.yaml @@ -0,0 +1,18 @@ +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: official + compatible: + - raspberry-pi + +Lora: + ### RAK13302 in Slot 2 pins + IRQ: 18 #IO6 + Reset: 24 # IO4 + Busy: 19 # IO5 + # Ant_sw: 23 # IO3 + Enable_Pins: + - 26 + - 23 + spidev: spidev0.1 + # CS: 7 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] \ No newline at end of file diff --git a/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml b/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..25fd5e359 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-MeshAdv-900M30S.yaml @@ -0,0 +1,39 @@ +# MeshAdv-Pi E22-900M30S +# https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 + CS: # GPIO0_A1 (physical 40) + pin: 1 + gpiochip: 0 + line: 1 + IRQ: # GPIO0_A3 (physical 36) + pin: 3 + gpiochip: 0 + line: 3 + Busy: # GPIO0_A0 (physical 38) + pin: 0 + gpiochip: 0 + line: 0 + Reset: # GPIO0_B4 (physical 12) + pin: 12 + gpiochip: 0 + line: 12 + TXen: # GPIO1_D1 (physical 33) + pin: 57 + gpiochip: 1 + line: 25 + RXen: # GPIO1_B3 (physical 32) + pin: 43 + gpiochip: 1 + line: 11 + DIO3_TCXO_VOLTAGE: true + # Only for E22-900M33S: + # Limit the output power to 8 dBm + # SX126X_MAX_POWER: 8 + spidev: spidev0.0 diff --git a/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..e1f385312 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-MeshAdv-Mini-900M22S.yaml @@ -0,0 +1,33 @@ +# MeshAdv Mini E22-900M22S +# https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 # Ebyte E22-900M22S + CS: # GPIO0_B6 (physical 24, SPI1_CSN0) + pin: 14 + gpiochip: 0 + line: 14 + IRQ: # GPIO0_A3 (physical 36) + pin: 3 + gpiochip: 0 + line: 3 + Busy: # GPIO0_A0 (physical 38) + pin: 0 + gpiochip: 0 + line: 0 + Reset: # GPIO1_C3 (physical 18) + pin: 51 + gpiochip: 1 + line: 19 + RXen: # GPIO1_B3 (physical 32) + pin: 43 + gpiochip: 1 + line: 11 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..8782876ac --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot1.yaml @@ -0,0 +1,38 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6, physical 15) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO0_A3 (IO4, physical 36) + pin: 3 + gpiochip: 0 + line: 3 + Busy: # GPIO1_C3 (IO5, physical 18) + pin: 51 + gpiochip: 1 + line: 19 + # Ant_sw: # GPIO1_C2 (IO3, physical 16) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 43 # GPIO1_B3 (physical 32) + gpiochip: 1 + line: 11 + - pin: 57 # GPIO1_D1 (physical 33) + gpiochip: 1 + line: 25 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B6 (SPI1_CSN0, physical 24) + # pin: 14 + # gpiochip: 0 + # line: 14 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..e0ef946d9 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13300-slot2.yaml @@ -0,0 +1,36 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B4 (IO6, physical 12) + pin: 12 + gpiochip: 0 + line: 12 + Reset: # GPIO1_C3 (IO4, physical 18) + pin: 51 + gpiochip: 1 + line: 19 + Busy: # GPIO0_B3 (IO5, physical 35) + pin: 11 + gpiochip: 0 + line: 11 + # Ant_sw: # GPIO1_C2 (IO3, physical 16) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 2 # GPIO0_A2 (physical 37) + gpiochip: 0 + line: 2 + - pin: 50 # GPIO1_C2 (physical 16) + gpiochip: 1 + line: 18 + spidev: spidev0.1 + # CS: # GPIO0_A7 (SPI1_CSN1, physical 26) + # pin: 7 + # gpiochip: 0 + # line: 7 diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..374be8e35 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot1.yaml @@ -0,0 +1,39 @@ +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: community # Promote when tested + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6, physical 15) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO0_A3 (IO4, physical 36) + pin: 3 + gpiochip: 0 + line: 3 + Busy: # GPIO1_C3 (IO5, physical 18) + pin: 51 + gpiochip: 1 + line: 19 + # Ant_sw: # GPIO1_C2 (IO3, physical 16) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 43 # GPIO1_B3 (physical 32) + gpiochip: 1 + line: 11 + - pin: 57 # GPIO1_D1 (physical 33) + gpiochip: 1 + line: 25 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B6 (SPI1_CSN0, physical 24) + # pin: 14 + # gpiochip: 0 + # line: 14 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..5548bc5c7 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-RAK6421-13302-slot2.yaml @@ -0,0 +1,37 @@ +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: community # Promote when tested + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B4 (IO6, physical 12) + pin: 12 + gpiochip: 0 + line: 12 + Reset: # GPIO1_C3 (IO4, physical 18) + pin: 51 + gpiochip: 1 + line: 19 + Busy: # GPIO0_B3 (IO5, physical 35) + pin: 11 + gpiochip: 0 + line: 11 + # Ant_sw: # GPIO1_C2 (IO3, physical 16) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 2 # GPIO0_A2 (physical 37) + gpiochip: 0 + line: 2 + - pin: 50 # GPIO1_C2 (physical 16) + gpiochip: 1 + line: 18 + spidev: spidev0.1 + # CS: # GPIO0_A7 (SPI1_CSN1, physical 26) + # pin: 7 + # gpiochip: 0 + # line: 7 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml b/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml new file mode 100644 index 000000000..1253d3e31 --- /dev/null +++ b/bin/config.d/lora-ecb41-pge-waveshare-sxxx.yaml @@ -0,0 +1,30 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - ebyte-ecb41-pge # Armbian + +Lora: + Module: sx1262 # Waveshare SX126X XXXM + DIO2_AS_RF_SWITCH: true + CS: # GPIO0_A1 (physical 40) + pin: 1 + gpiochip: 0 + line: 1 + IRQ: # GPIO0_A3 (physical 36) + pin: 3 + gpiochip: 0 + line: 3 + Busy: # GPIO0_A0 (physical 38) + pin: 0 + gpiochip: 0 + line: 0 + Reset: # GPIO0_B4 (physical 12) + pin: 12 + gpiochip: 0 + line: 12 + SX126X_ANT_SW: # GPIO1_B2 (physical 31) + pin: 42 + gpiochip: 1 + line: 10 + spidev: spidev0.0 diff --git a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml similarity index 75% rename from bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml rename to bin/config.d/lora-femtofox_LR1121_TCXO.yaml index 7aa860f61..10166fa35 100644 --- a/bin/config.d/femtofox/femtofox_LR1121_TCXO.yaml +++ b/bin/config.d/lora-femtofox_LR1121_TCXO.yaml @@ -1,20 +1,22 @@ ---- -Lora: -## Ebyte E80-900M22S -## This is a bit experimental -## -## - Module: lr1121 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO3_TCXO_VOLTAGE: 1.8 - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - - - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox Ebyte E80-900M22S with TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E80-900M22S +## This is a bit experimental +## +## + Module: lr1121 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO3_TCXO_VOLTAGE: 1.8 + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml similarity index 87% rename from bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml rename to bin/config.d/lora-femtofox_SX1262_TCXO.yaml index a4dec870a..31012c0f6 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_TCXO.yaml +++ b/bin/config.d/lora-femtofox_SX1262_TCXO.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup -## HT-RA62 (Has internal switching, but whatever) -## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) -## Will work with any module with or without RF switching, and with TCXO - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: true - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 TCXO + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup +## HT-RA62 (Has internal switching, but whatever) +## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN) +## Will work with any module with or without RF switching, and with TCXO + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml similarity index 86% rename from bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml rename to bin/config.d/lora-femtofox_SX1262_XTAL.yaml index 6b956f3e3..7132f382e 100644 --- a/bin/config.d/femtofox/femtofox_SX1262_XTAL.yaml +++ b/bin/config.d/lora-femtofox_SX1262_XTAL.yaml @@ -1,21 +1,24 @@ ---- -Lora: -## Ebyte E22-900MM22S with no external RF switching setup -## Waveshare SX126X XXXM, AI Thinker RA-01SH -## Will work with any module with or without RF switching and no TCXO - - Module: sx1262 - gpiochip: 1 # subtract 32 from the gpio numbers - DIO2_AS_RF_SWITCH: true - DIO3_TCXO_VOLTAGE: false - CS: 16 #pin6 / GPIO48 1C0 - IRQ: 23 #pin17 / GPIO55 1C7 - Busy: 22 #pin16 / GPIO54 1C6 - Reset: 25 #pin13 / GPIO57 1D1 - RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? -# TXen: bridge to DIO2 on E22 module - spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) - spiSpeed: 2000000 - -General: - MACAddressSource: eth0 +--- +Meta: + name: Femtofox SX1262 XTAL + support: community + compatible: + - luckfox-pico-mini # Armbian + +Lora: +## Ebyte E22-900MM22S with no external RF switching setup +## Waveshare SX126X XXXM, AI Thinker RA-01SH +## Will work with any module with or without RF switching and no TCXO + + Module: sx1262 + gpiochip: 1 # subtract 32 from the gpio numbers + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: false + CS: 16 #pin6 / GPIO48 1C0 + IRQ: 23 #pin17 / GPIO55 1C7 + Busy: 22 #pin16 / GPIO54 1C6 + Reset: 25 #pin13 / GPIO57 1D1 + RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things? +# TXen: bridge to DIO2 on E22 module + spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml index 80df34ca7..b0ac0306a 100644 --- a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -1,13 +1,21 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 (Autoconf default) + support: official + compatible: + - raspberry-pi + Lora: - ### RAK13300in Slot 1 + ### RAK13300 in Slot 1 Module: sx1262 IRQ: 22 #IO6 Reset: 16 # IO4 Busy: 24 # IO5 - # Ant_sw: 13 # IO3 + Enable_Pins: + - 12 + - 13 DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 - GPIO_DETECT_PA: 13 - TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] \ No newline at end of file + # GPIO_DETECT_PA: 13 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] \ No newline at end of file diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml index 2fd128ce8..f944a7949 100644 --- a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Luckfox Lyra PicoCalc Wio LoRa SX1262 + support: official + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_1w.yaml b/bin/config.d/lora-lyra-ultra_1w.yaml index 0bdc05fef..71d05f84e 100644 --- a/bin/config.d/lora-lyra-ultra_1w.yaml +++ b/bin/config.d/lora-lyra-ultra_1w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 1 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 1W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ultra_2w.yaml b/bin/config.d/lora-lyra-ultra_2w.yaml index a1fe6d7f7..e3bb18c7b 100644 --- a/bin/config.d/lora-lyra-ultra_2w.yaml +++ b/bin/config.d/lora-lyra-ultra_2w.yaml @@ -2,6 +2,13 @@ # Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config # https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat # 2 Watt Lyra Ultra hat +Meta: + name: wehooper4 Luckfox Ultra 2W + support: community + compatible: + - luckfox-pico-ultra # Armbian + - luckfox-lyra-ultra # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml index 8425fc385..bdd98a41c 100644 --- a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml +++ b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml @@ -2,6 +2,12 @@ # Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config # Waveshare LoRa HAT for Raspberry Pi Pico # https://www.waveshare.com/wiki/Pico-LoRa-SX1262 +Meta: + name: Waveshare LoRa HAT for Raspberry Pi Pico + support: community + compatible: + - luckfox-lyra-plus # Armbian + Lora: Module: sx1262 DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml b/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..ea22c7953 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-MeshAdv-900M30S.yaml @@ -0,0 +1,39 @@ +# MeshAdv-Pi E22-900M30S +# https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + CS: # GPIO0_C2 (physical 40) + pin: 18 + gpiochip: 0 + line: 18 + IRQ: # GPIO1_D1 (physical 36) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1 (physical 38) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + TXen: # GPIO1_C2 (physical 33) + pin: 50 + gpiochip: 1 + line: 18 + RXen: # GPIO1_D2 (physical 32) + pin: 58 + gpiochip: 1 + line: 26 + DIO3_TCXO_VOLTAGE: true + # Only for E22-900M33S: + # Limit the output power to 8 dBm + # SX126X_MAX_POWER: 8 + spidev: spidev0.0 diff --git a/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..1fb150b15 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-MeshAdv-Mini-900M22S.yaml @@ -0,0 +1,33 @@ +# MeshAdv Mini E22-900M22S +# https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 # Ebyte E22-900M22S + CS: # GPIO0_B2_d (phys 24, RPi CE0) + pin: 10 + gpiochip: 0 + line: 10 + IRQ: # GPIO1_D1_d (phys 36, RPi GPIO16) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1_d (phys 38, RPi GPIO20) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B4_d (phys 18, RPi GPIO24) + pin: 12 + gpiochip: 0 + line: 12 + RXen: # GPIO1_D2_d (phys 32, RPi GPIO12) + pin: 58 + gpiochip: 1 + line: 26 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..b65a30c20 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot1.yaml @@ -0,0 +1,38 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO1_D1 (IO4) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_B4 (IO5) + pin: 12 + gpiochip: 0 + line: 12 + # Ant_sw: # GPIO1_C2 (IO3) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 58 # GPIO1_D2 + gpiochip: 1 + line: 26 + - pin: 50 # GPIO1_C2 + gpiochip: 1 + line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B2 + # pin: 10 + # gpiochip: 0 + # line: 10 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..255a3eca3 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13300-slot2.yaml @@ -0,0 +1,36 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO0_B4 (IO4) + pin: 12 + gpiochip: 0 + line: 12 + Busy: # GPIO1_C0 (IO5) + pin: 48 + gpiochip: 1 + line: 16 + # Ant_sw: # GPIO0_B5 (IO3) + # pin: 13 + # gpiochip: 0 + # line: 13 + Enable_Pins: + - pin: 51 # GPIO1_C3 + gpiochip: 1 + line: 19 + - pin: 13 # GPIO0_B5 + gpiochip: 0 + line: 13 + spidev: spidev0.1 + # CS: # GPIO0_B1 + # pin: 9 + # gpiochip: 0 + # line: 9 diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..c37c6fb3a --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot1.yaml @@ -0,0 +1,39 @@ +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_A5 (IO6) + pin: 5 + gpiochip: 0 + line: 5 + Reset: # GPIO1_D1 (IO4) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_B4 (IO5) + pin: 12 + gpiochip: 0 + line: 12 + # Ant_sw: # GPIO1_C2 (IO3) + # pin: 50 + # gpiochip: 1 + # line: 18 + Enable_Pins: + - pin: 58 # GPIO1_D2 + gpiochip: 1 + line: 26 + - pin: 50 # GPIO1_C2 + gpiochip: 1 + line: 18 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_B2 + # pin: 10 + # gpiochip: 0 + # line: 10 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..773a35ab0 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-RAK6421-13302-slot2.yaml @@ -0,0 +1,37 @@ +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO0_B4 (IO4) + pin: 12 + gpiochip: 0 + line: 12 + Busy: # GPIO1_C0 (IO5) + pin: 48 + gpiochip: 1 + line: 16 + # Ant_sw: # GPIO0_B5 (IO3) + # pin: 13 + # gpiochip: 0 + # line: 13 + Enable_Pins: + - pin: 51 # GPIO1_C3 + gpiochip: 1 + line: 19 + - pin: 13 # GPIO0_B5 + gpiochip: 0 + line: 13 + spidev: spidev0.1 + # CS: # GPIO0_B1 + # pin: 9 + # gpiochip: 0 + # line: 9 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml b/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml new file mode 100644 index 000000000..934945202 --- /dev/null +++ b/bin/config.d/lora-lyra-zero-waveshare-sxxx.yaml @@ -0,0 +1,30 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - luckfox-lyra-zero-w # Armbian + +Lora: + Module: sx1262 # Waveshare SX126X XXXM + DIO2_AS_RF_SWITCH: true + CS: # GPIO0_C2 (physical 40) + pin: 18 + gpiochip: 0 + line: 18 + IRQ: # GPIO1_D1 (physical 36) + pin: 57 + gpiochip: 1 + line: 25 + Busy: # GPIO0_C1 (physical 38) + pin: 17 + gpiochip: 0 + line: 17 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + SX126X_ANT_SW: # GPIO1_B3 (physical 31) + pin: 43 + gpiochip: 1 + line: 11 + spidev: spidev0.0 diff --git a/bin/config.d/lora-meshstick-1262.yaml b/bin/config.d/lora-meshstick-1262.yaml index 3f8d6c617..355d0bc0f 100644 --- a/bin/config.d/lora-meshstick-1262.yaml +++ b/bin/config.d/lora-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml b/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml new file mode 100644 index 000000000..c39a07ccd --- /dev/null +++ b/bin/config.d/lora-ok3506-MeshAdv-900M30S.yaml @@ -0,0 +1,41 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +# MeshAdv-Pi E22-900M30S +# https://github.com/chrismyers2000/MeshAdv-Pi-Hat +Meta: + name: MeshAdv-Pi E22-900M30S + support: community + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 + CS: # GPIO0_B0 (physical 40) + pin: 8 + gpiochip: 0 + line: 8 + IRQ: # GPIO3_B0 (physical 36) + pin: 104 + gpiochip: 3 + line: 8 + Busy: # GPIO0_B1 (physical 38) + pin: 9 + gpiochip: 0 + line: 9 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + TXen: # GPIO0_A3 (physical 33) + pin: 3 + gpiochip: 0 + line: 3 + RXen: # GPIO0_A2 (physical 32) + pin: 2 + gpiochip: 0 + line: 2 + DIO3_TCXO_VOLTAGE: true + # Only for E22-900M33S: + # Limit the output power to 8 dBm + # SX126X_MAX_POWER: 8 + spidev: spidev0.0 diff --git a/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml new file mode 100644 index 000000000..8f2c4a417 --- /dev/null +++ b/bin/config.d/lora-ok3506-MeshAdv-Mini-900M22S.yaml @@ -0,0 +1,35 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +# MeshAdv Mini E22-900M22S +# https://github.com/chrismyers2000/MeshAdv-Mini +Meta: + name: MeshAdv Mini E22-900M22S + support: community + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 # Ebyte E22-900M22S + CS: # GPIO0_C3 (physical 24, SPI0_CSN0) + pin: 19 + gpiochip: 0 + line: 19 + IRQ: # GPIO3_B0 (physical 36) + pin: 104 + gpiochip: 3 + line: 8 + Busy: # GPIO0_B1 (physical 38) + pin: 9 + gpiochip: 0 + line: 9 + Reset: # GPIO3_A6 (physical 18) + pin: 102 + gpiochip: 3 + line: 6 + RXen: # GPIO0_A2 (physical 32) + pin: 2 + gpiochip: 0 + line: 2 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 diff --git a/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml b/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml new file mode 100644 index 000000000..d97bdd327 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13300-slot1.yaml @@ -0,0 +1,40 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +Meta: + name: RAK6421 + RAK13300 Slot 1 + support: community # Promote when tested + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO3_B5 (IO6, physical 15) + pin: 109 + gpiochip: 3 + line: 13 + Reset: # GPIO3_B0 (IO4, physical 36) + pin: 104 + gpiochip: 3 + line: 8 + Busy: # GPIO3_A6 (IO5, physical 18) + pin: 102 + gpiochip: 3 + line: 6 + # Ant_sw: # GPIO0_A3 (IO3, physical 33) + # pin: 3 + # gpiochip: 0 + # line: 3 + Enable_Pins: + - pin: 2 # GPIO0_A2 (physical 32) + gpiochip: 0 + line: 2 + - pin: 3 # GPIO0_A3 (physical 33) + gpiochip: 0 + line: 3 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_C3 (SPI0_CSN0, physical 24) + # pin: 19 + # gpiochip: 0 + # line: 19 diff --git a/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..969c20ad3 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13300-slot2.yaml @@ -0,0 +1,38 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +Meta: + name: RAK6421 + RAK13300 Slot 2 + support: community # Promote when tested + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6, physical 12) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO3_A6 (IO4, physical 18) + pin: 102 + gpiochip: 3 + line: 6 + Busy: # GPIO0_B2 (IO5, physical 35) + pin: 10 + gpiochip: 0 + line: 10 + # Ant_sw: # GPIO3_A7 (IO3, physical 16) + # pin: 103 + # gpiochip: 3 + # line: 7 + Enable_Pins: + - pin: 106 # GPIO3_B2 (physical 37) + gpiochip: 3 + line: 10 + - pin: 103 # GPIO3_A7 (physical 16) + gpiochip: 3 + line: 7 + spidev: spidev0.1 + # CS: # GPIO0_B7 (SPI0_CSN1, physical 26) + # pin: 15 + # gpiochip: 0 + # line: 15 diff --git a/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml b/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml new file mode 100644 index 000000000..d73eaf7a0 --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13302-slot1.yaml @@ -0,0 +1,41 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +Meta: + name: RAK6421 + RAK13302 Slot 1 + support: community # Promote when tested + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO3_B5 (IO6, physical 15) + pin: 109 + gpiochip: 3 + line: 13 + Reset: # GPIO3_B0 (IO4, physical 36) + pin: 104 + gpiochip: 3 + line: 8 + Busy: # GPIO3_A6 (IO5, physical 18) + pin: 102 + gpiochip: 3 + line: 6 + # Ant_sw: # GPIO0_A3 (IO3, physical 33) + # pin: 3 + # gpiochip: 0 + # line: 3 + Enable_Pins: + - pin: 2 # GPIO0_A2 (physical 32) + gpiochip: 0 + line: 2 + - pin: 3 # GPIO0_A3 (physical 33) + gpiochip: 0 + line: 3 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: # GPIO0_C3 (SPI0_CSN0, physical 24) + # pin: 19 + # gpiochip: 0 + # line: 19 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml new file mode 100644 index 000000000..36b70658b --- /dev/null +++ b/bin/config.d/lora-ok3506-RAK6421-13302-slot2.yaml @@ -0,0 +1,39 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +Meta: + name: RAK6421 + RAK13302 Slot 2 + support: community # Promote when tested + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 + IRQ: # GPIO0_B6 (IO6, physical 12) + pin: 14 + gpiochip: 0 + line: 14 + Reset: # GPIO3_A6 (IO4, physical 18) + pin: 102 + gpiochip: 3 + line: 6 + Busy: # GPIO0_B2 (IO5, physical 35) + pin: 10 + gpiochip: 0 + line: 10 + # Ant_sw: # GPIO3_A7 (IO3, physical 16) + # pin: 103 + # gpiochip: 3 + # line: 7 + Enable_Pins: + - pin: 106 # GPIO3_B2 (physical 37) + gpiochip: 3 + line: 10 + - pin: 103 # GPIO3_A7 (physical 16) + gpiochip: 3 + line: 7 + spidev: spidev0.1 + # CS: # GPIO0_B7 (SPI0_CSN1, physical 26) + # pin: 15 + # gpiochip: 0 + # line: 15 + TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] diff --git a/bin/config.d/lora-ok3506-waveshare-sxxx.yaml b/bin/config.d/lora-ok3506-waveshare-sxxx.yaml new file mode 100644 index 000000000..1f5795f92 --- /dev/null +++ b/bin/config.d/lora-ok3506-waveshare-sxxx.yaml @@ -0,0 +1,32 @@ +# !! WARNING: Hats on the OK3506 board are installed "backwards" (facing outwards) + +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - forlinx-ok3506-s12 # Armbian + +Lora: + Module: sx1262 # Waveshare SX126X XXXM + DIO2_AS_RF_SWITCH: true + CS: # GPIO0_B0 (physical 40) + pin: 8 + gpiochip: 0 + line: 8 + IRQ: # GPIO3_B0 (physical 36) + pin: 104 + gpiochip: 3 + line: 8 + Busy: # GPIO0_B1 (physical 38) + pin: 9 + gpiochip: 0 + line: 9 + Reset: # GPIO0_B6 (physical 12) + pin: 14 + gpiochip: 0 + line: 14 + SX126X_ANT_SW: # GPIO3_B3 (physical 31) + pin: 107 + gpiochip: 3 + line: 11 + spidev: spidev0.0 diff --git a/bin/config.d/lora-piggystick-lr1121.yaml b/bin/config.d/lora-piggystick-lr1121.yaml index 348db61b1..e11c78dd3 100644 --- a/bin/config.d/lora-piggystick-lr1121.yaml +++ b/bin/config.d/lora-piggystick-lr1121.yaml @@ -1,3 +1,9 @@ +Meta: + name: Lora Meshstick SX1262 + support: community + compatible: + - usb + Lora: Module: lr1121 CS: 0 diff --git a/bin/config.d/lora-pinedio-usb-sx1262.yaml b/bin/config.d/lora-pinedio-usb-sx1262.yaml index 6b8a9fc95..b2351c05a 100644 --- a/bin/config.d/lora-pinedio-usb-sx1262.yaml +++ b/bin/config.d/lora-pinedio-usb-sx1262.yaml @@ -1,5 +1,11 @@ +Meta: + name: Pinedio USB SX1262 + support: deprecated + compatible: + - usb + Lora: Module: sx1262 CS: 0 IRQ: 10 - spidev: ch341 \ No newline at end of file + spidev: ch341 diff --git a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml index ea86a3728..7337f39ca 100644 --- a/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml +++ b/bin/config.d/lora-raxda-rock2f-starter-edition-hat.yaml @@ -1,3 +1,9 @@ +Meta: + name: raxda-rock2f-starter-edition-hat + support: community + compatible: + - rock-2f # Armbian + Lora: ### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx diff --git a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml index d9b64c7da..185417cce 100644 --- a/bin/config.d/lora-starter-edition-sx1262-i2c.yaml +++ b/bin/config.d/lora-starter-edition-sx1262-i2c.yaml @@ -1,5 +1,11 @@ # https://www.waveshare.com/core1262-868m.htm # https://github.com/markbirss/lora-starter-edition-sx1262-i2c +Meta: + name: lora-starter-edition-sx1262-i2c + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml index a539d76a1..79ca132df 100644 --- a/bin/config.d/lora-usb-meshstick-1262.yaml +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshstick-1262 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-meshtoad-e22.yaml b/bin/config.d/lora-usb-meshtoad-e22.yaml index b6cb61c6b..49182c83e 100644 --- a/bin/config.d/lora-usb-meshtoad-e22.yaml +++ b/bin/config.d/lora-usb-meshtoad-e22.yaml @@ -1,3 +1,9 @@ +Meta: + name: meshtoad-e22 + support: official + compatible: + - usb + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml index 8b32c5af2..9f30217e0 100644 --- a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1262-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1262 CS: 0 diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml index df772184c..45c8e21d0 100644 --- a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -1,3 +1,9 @@ +Meta: + name: umesh-1268-30dbm + support: community + compatible: + - clockwork-uconsole + Lora: Module: sx1268 CS: 0 diff --git a/bin/config.d/lora-waveshare-sxxx.yaml b/bin/config.d/lora-waveshare-sxxx.yaml index a9ff13653..641cf1e49 100644 --- a/bin/config.d/lora-waveshare-sxxx.yaml +++ b/bin/config.d/lora-waveshare-sxxx.yaml @@ -1,3 +1,9 @@ +Meta: + name: Waveshare SX1262 + support: deprecated + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare SX126X XXXM DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml index 1e1c325e7..b84e18d5b 100644 --- a/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml +++ b/bin/config.d/lora-ws-raspberry-pi-pico-to-rpi-adapter.yaml @@ -1,5 +1,12 @@ # https://www.waveshare.com/pico-lora-sx1262-868m.htm # https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter + +Meta: + name: ws-raspberry-pico-to-rpi-adapter + support: community + compatible: + - raspberry-pi + Lora: Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter DIO2_AS_RF_SWITCH: true diff --git a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml index 37d7e27d2..5743a9ed6 100644 --- a/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml +++ b/bin/config.d/lora-ws-raspberry-pico-to-orangepi-03.yaml @@ -15,6 +15,12 @@ # 5 CS 24 # 26 DIO1/IRQ 26 +Meta: + name: ws-raspberry-pico-to-orangepi-03 + support: community + compatible: + - orange-pi-zero-3 # Armbian + Lora: Module: sx1262 # Waveshare Raspberry Pico Lora module DIO2_AS_RF_SWITCH: true diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index b4c18c05b..1458e4390 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -43,7 +43,7 @@ for pio_env in pio_envs: env = { "ci": {"board": pio_env, "platform": env_platform}, "board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), - "board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), + "board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true", } all_envs.append(env) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 3566ad506..0642fdb07 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,12 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.22 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20 diff --git a/boards/heltec_mesh_node_t096.json b/boards/heltec_mesh_node_t096.json new file mode 100644 index 000000000..1e417c5b4 --- /dev/null +++ b/boards/heltec_mesh_node_t096.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x2886", "0x1667"] + ], + "usb_product": "HT-n5262G", + "mcu": "nrf52840", + "variant": "heltec_mesh_node_t096", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/", + "vendor": "Heltec" +} diff --git a/boards/t5-epaper-s3.json b/boards/t5-epaper-s3.json new file mode 100644 index 000000000..16106198e --- /dev/null +++ b/boards/t5-epaper-s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi", + "partitions": "default_16MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo T5-ePaper-S3", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://lilygo.cc/products/t5-e-paper-s3-pro", + "vendor": "LILYGO" +} diff --git a/boards/ttgo-tbeam.json b/boards/ttgo-tbeam.json new file mode 100644 index 000000000..a4c43d525 --- /dev/null +++ b/boards/ttgo-tbeam.json @@ -0,0 +1,29 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_T_Beam", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "tbeam" + }, + "connectivity": ["wifi", "bluetooth", "can", "ethernet"], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "TTGO T-Beam (PSRAM Disabled)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 1310720, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://github.com/LilyGO/TTGO-T-Beam", + "vendor": "TTGO" +} diff --git a/branding/README.md b/branding/README.md index 3a558bf20..b59936140 100644 --- a/branding/README.md +++ b/branding/README.md @@ -12,6 +12,6 @@ Ex: - `logo_320x480.png` - `logo_320x240.png` -This file is copied to `data/boot/logo.png` before filesytem image compilation. +This file is copied to `data/boot/logo.png` before filesystem image compilation. For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33). diff --git a/debian/changelog b/debian/changelog index 1e688ce80..b13a2ae9d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +meshtasticd (2.7.22.0) unstable; urgency=medium + + * Version 2.7.22 + + -- GitHub Actions Mon, 06 Apr 2026 11:34:12 +0000 + +meshtasticd (2.7.21.0) unstable; urgency=medium + + * Version 2.7.21 + + -- GitHub Actions Wed, 11 Mar 2026 11:45:36 +0000 + meshtasticd (2.7.20.0) unstable; urgency=medium * Version 2.7.20 diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh index 81e681e0c..7b2418ff6 100755 --- a/debian/ci_pack_sdeb.sh +++ b/debian/ci_pack_sdeb.sh @@ -3,10 +3,17 @@ export DEBEMAIL="jbennett@incomsystems.biz" export PLATFORMIO_LIBDEPS_DIR=pio/libdeps export PLATFORMIO_PACKAGES_DIR=pio/packages export PLATFORMIO_CORE_DIR=pio/core +export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 +export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 +export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Download libraries to `pio` platformio pkg install -e native-tft platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0 +# Mangle PlatformIO cache to prevent internet access at build-time +# Simply adds 1 to all expiry (epoch) timestamps, adding ~500 years to expiry date +cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak +jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak >pio/core/.cache/downloads/usage.db # Compress `pio` directory to prevent dh_clean from sanitizing it tar -cf pio.tar pio/ rm -rf pio @@ -20,5 +27,10 @@ rm -rf debian/changelog dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \ "GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES" -# Build the source deb -debuild -S -nc -k"$GPG_KEY_ID" +if [[ -n $GPG_KEY_ID ]]; then + # Build and sign the source deb + debuild -S -nc -k"$GPG_KEY_ID" +else + # Build the source deb without signing (forks) + debuild -S -nc +fi diff --git a/debian/rules b/debian/rules index ebb572153..68af9a9a5 100755 --- a/debian/rules +++ b/debian/rules @@ -9,7 +9,10 @@ PIO_ENV:=\ PLATFORMIO_CORE_DIR=pio/core \ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \ - PLATFORMIO_PACKAGES_DIR=pio/packages + PLATFORMIO_PACKAGES_DIR=pio/packages \ + PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \ + PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \ + PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240 # Raspbian armhf builds should be compatible with armv6-hardfloat # https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags diff --git a/platformio.ini b/platformio.ini index 67018c27a..478d39b4e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -77,10 +77,10 @@ lib_deps = https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip - # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb - nanopb/Nanopb@0.4.91 - # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 - erriez/ErriezCRC32@1.0.1 + # renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb + https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip + # renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32 + https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip ; Used for the code analysis in PIO Home / Inspect check_tool = cppcheck @@ -95,139 +95,153 @@ check_flags = framework = arduino lib_deps = ${env.lib_deps} - # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL - end2endzone/NonBlockingRTTTL@1.4.0 + # renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL + https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip +build_unflags = + -std=c++11 + -std=gnu++11 build_flags = ${env.build_flags} -Os + -std=gnu++17 build_src_filter = ${env.build_src_filter} - - - ; Common libs for communicating over TCP/IP networks such as MQTT [networking_base] lib_deps = - # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient - thingsboard/TBPubSubClient@2.12.1 - # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient - arduino-libraries/NTPClient@3.2.1 + # renovate: datasource=github-tags depName=TBPubSubClient packageName=thingsboard/pubsubclient + https://github.com/thingsboard/pubsubclient/archive/refs/tags/v2.12.1.zip + # renovate: datasource=github-tags depName=NTPClient packageName=arduino-libraries/NTPClient + https://github.com/arduino-libraries/NTPClient/archive/refs/tags/3.2.1.zip ; Extra TCP/IP networking libs for supported devices [networking_extra] lib_deps = - # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog - arcao/Syslog@2.0.0 + # renovate: datasource=github-tags depName=Syslog packageName=arcao/Syslog + https://github.com/arcao/Syslog/archive/refs/tags/v2.0.zip [radiolib_base] lib_deps = - # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - jgromes/RadioLib@7.6.0 + # renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib + https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip [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/6c75195e9987b7a49563232234f2f868dd343cae.zip + https://github.com/meshtastic/device-ui/archive/1897dd17fceb1f101bb1a3245680aa3439edcfdd.zip ; Common libs for environmental measurements in telemetry module [environmental_base] lib_deps = - # renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO - adafruit/Adafruit BusIO@1.17.4 - # 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 - adafruit/Adafruit BMP280 Library@3.0.0 - # renovate: datasource=custom.pio depName=Adafruit BMP085 packageName=adafruit/library/Adafruit BMP085 Library - adafruit/Adafruit BMP085 Library@1.2.4 - # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library - adafruit/Adafruit BME280 Library@2.3.0 - # renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310 - adafruit/Adafruit DPS310@1.1.5 - # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library - adafruit/Adafruit MCP9808 Library@2.0.2 - # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library - adafruit/Adafruit INA260 Library@1.5.3 - # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 - adafruit/Adafruit INA219@1.2.3 - # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 - adafruit/Adafruit MPU6050@2.2.9 - # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH - adafruit/Adafruit LIS3DH@1.3.0 - # renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0 - adafruit/Adafruit AHTX0@2.0.5 - # renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS - adafruit/Adafruit LSM6DS@4.7.4 - # renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library - adafruit/Adafruit TSL2591 Library@1.4.5 - # renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632 - emotibit/EmotiBit MLX90632@1.0.8 - # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library - adafruit/Adafruit MLX90614 Library@2.1.5 + # renovate: datasource=github-tags depName=Adafruit BusIO packageName=adafruit/Adafruit_BusIO + https://github.com/adafruit/Adafruit_BusIO/archive/refs/tags/1.17.4.zip + # renovate: datasource=github-tags depName=Adafruit Unified Sensor packageName=adafruit/Adafruit_Sensor + https://github.com/adafruit/Adafruit_Sensor/archive/refs/tags/1.1.15.zip + # renovate: datasource=github-tags depName=Adafruit GFX packageName=adafruit/Adafruit-GFX-Library + https://github.com/adafruit/Adafruit-GFX-Library/archive/refs/tags/1.12.6.zip + # renovate: datasource=github-tags depName=NeoPixel packageName=adafruit/Adafruit_NeoPixel + https://github.com/adafruit/Adafruit_NeoPixel/archive/refs/tags/1.15.4.zip + # renovate: datasource=github-tags depName=Adafruit SSD1306 packageName=adafruit/Adafruit_SSD1306 + https://github.com/adafruit/Adafruit_SSD1306/archive/refs/tags/2.5.16.zip + # renovate: datasource=github-tags depName=Adafruit BMP280 packageName=adafruit/Adafruit_BMP280_Library + https://github.com/adafruit/Adafruit_BMP280_Library/archive/refs/tags/3.0.0.zip + # renovate: datasource=github-tags depName=Adafruit BMP085 packageName=adafruit/Adafruit-BMP085-Library + https://github.com/adafruit/Adafruit-BMP085-Library/archive/refs/tags/1.2.4.zip + # renovate: datasource=github-tags depName=Adafruit BME280 packageName=adafruit/Adafruit_BME280_Library + https://github.com/adafruit/Adafruit_BME280_Library/archive/refs/tags/2.3.0.zip + # renovate: datasource=github-tags depName=Adafruit DPS310 packageName=adafruit/Adafruit_DPS310 + https://github.com/adafruit/Adafruit_DPS310/archive/refs/tags/1.1.6.zip + # renovate: datasource=github-tags depName=Adafruit SH110x packageName=adafruit/Adafruit_SH110x + https://github.com/adafruit/Adafruit_SH110x/archive/refs/tags/2.1.14.zip + # renovate: datasource=github-tags depName=Adafruit MCP9808 packageName=adafruit/Adafruit_MCP9808_Library + https://github.com/adafruit/Adafruit_MCP9808_Library/archive/refs/tags/2.0.2.zip + # renovate: datasource=github-tags depName=Adafruit INA260 packageName=adafruit/Adafruit_INA260 + https://github.com/adafruit/Adafruit_INA260/archive/refs/tags/1.5.3.zip + # renovate: datasource=github-tags depName=Adafruit INA219 packageName=adafruit/Adafruit_INA219 + https://github.com/adafruit/Adafruit_INA219/archive/refs/tags/1.2.3.zip + # renovate: datasource=github-tags depName=Adafruit MPU6050 packageName=adafruit/Adafruit_MPU6050 + https://github.com/adafruit/Adafruit_MPU6050/archive/refs/tags/2.2.9.zip + # renovate: datasource=github-tags depName=Adafruit LIS3DH packageName=adafruit/Adafruit_LIS3DH + https://github.com/adafruit/Adafruit_LIS3DH/archive/refs/tags/1.3.0.zip + # renovate: datasource=github-tags depName=Adafruit AHTX0 packageName=adafruit/Adafruit_AHTX0 + https://github.com/adafruit/Adafruit_AHTX0/archive/refs/tags/2.0.6.zip + # renovate: datasource=github-tags depName=Adafruit LSM6DS packageName=adafruit/Adafruit_LSM6DS + https://github.com/adafruit/Adafruit_LSM6DS/archive/refs/tags/4.7.4.zip + # renovate: datasource=github-tags depName=Adafruit TSL2591 packageName=adafruit/Adafruit_TSL2591_Library + https://github.com/adafruit/Adafruit_TSL2591_Library/archive/refs/tags/1.4.5.zip + # renovate: datasource=github-tags depName=EmotiBit MLX90632 packageName=emotibit/EmotiBit_MLX90632 + https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip + # renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614 + https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip # renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip - # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass - mprograms/QMC5883LCompass@1.2.3 - # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU - dfrobot/DFRobot_RTU@1.0.6 + # renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass + https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip + # renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU + https://github.com/DFRobot/DFRobot_RTU/archive/refs/tags/V1.0.6.zip # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip - # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 - robtillaart/INA226@0.6.6 - # renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library - sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2 - # renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library - sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@1.3.2 - # renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library - adafruit/Adafruit LTR390 Library@1.1.2 - # renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075 - adafruit/Adafruit PCT2075@1.0.6 - # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 - dfrobot/DFRobot_BMM150@1.0.0 - # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 - adafruit/Adafruit TSL2561@1.1.2 - # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE - wollewald/BH1750_WE@1.1.10 + # renovate: datasource=github-tags depName=INA226 packageName=robtillaart/INA226 + https://github.com/RobTillaart/INA226/archive/refs/tags/0.6.6.zip + # renovate: datasource=github-tags depName=SparkFun MAX3010x packageName=sparkfun/SparkFun_MAX3010x_Sensor_Library + https://github.com/sparkfun/SparkFun_MAX3010x_Sensor_Library/archive/refs/tags/v1.1.2.zip + # renovate: datasource=github-tags depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/SparkFun_ICM-20948_ArduinoLibrary + https://github.com/sparkfun/SparkFun_ICM-20948_ArduinoLibrary/archive/refs/tags/v1.3.2.zip + # renovate: datasource=github-tags depName=Adafruit LTR390 Library packageName=adafruit/Adafruit_LTR390 + https://github.com/adafruit/Adafruit_LTR390/archive/refs/tags/1.1.2.zip + # renovate: datasource=github-tags depName=Adafruit PCT2075 packageName=adafruit/Adafruit_PCT2075 + https://github.com/adafruit/Adafruit_PCT2075/archive/refs/tags/1.0.6.zip + # renovate: datasource=github-tags depName=DFRobot_BMM150 packageName=dfrobot/DFRobot_BMM150 + https://github.com/DFRobot/DFRobot_BMM150/archive/refs/tags/V1.0.0.zip + # renovate: datasource=github-tags depName=Adafruit_TSL2561 packageName=adafruit/Adafruit_TSL2561 + https://github.com/adafruit/Adafruit_TSL2561/archive/refs/tags/1.1.3.zip + # renovate: datasource=github-tags depName=BH1750_WE packageName=wollewald/BH1750_WE + https://github.com/wollewald/BH1750_WE/archive/refs/tags/1.1.10.zip ; Common environmental sensor libraries (not included in native / portduino) [environmental_extra_common] lib_deps = - # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library - adafruit/Adafruit BMP3XX Library@2.1.6 - # renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X - adafruit/Adafruit MAX1704X@1.0.3 - # renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library - adafruit/Adafruit SHTC3 Library@1.0.2 - # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X - adafruit/Adafruit LPS2X@2.0.6 - # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library - adafruit/Adafruit SHT31 Library@2.2.2 - # renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library - adafruit/Adafruit VEML7700 Library@2.1.6 - # renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library - adafruit/Adafruit SHT4x Library@1.0.5 - # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library - sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 + # renovate: datasource=github-tags depName=Adafruit BMP3XX packageName=adafruit/Adafruit_BMP3XX + https://github.com/adafruit/Adafruit_BMP3XX/archive/refs/tags/2.1.6.zip + # renovate: datasource=github-tags depName=Adafruit MAX1704X packageName=adafruit/Adafruit_MAX1704X + https://github.com/adafruit/Adafruit_MAX1704X/archive/refs/tags/1.0.3.zip + # renovate: datasource=github-tags depName=Adafruit SHTC3 packageName=adafruit/Adafruit_SHTC3 + https://github.com/adafruit/Adafruit_SHTC3/archive/refs/tags/1.0.2.zip + # renovate: datasource=github-tags depName=Adafruit LPS2X packageName=adafruit/Adafruit_LPS2X + https://github.com/adafruit/Adafruit_LPS2X/archive/refs/tags/2.0.6.zip + # renovate: datasource=github-tags depName=Adafruit SHT31 packageName=adafruit/Adafruit_SHT31 + https://github.com/adafruit/Adafruit_SHT31/archive/refs/tags/2.2.2.zip + # renovate: datasource=github-tags depName=Adafruit VEML7700 packageName=adafruit/Adafruit_VEML7700 + https://github.com/adafruit/Adafruit_VEML7700/archive/refs/tags/2.1.6.zip + # renovate: datasource=github-tags depName=Adafruit SHT4x packageName=adafruit/Adafruit_SHT4X + https://github.com/adafruit/Adafruit_SHT4X/archive/refs/tags/1.0.5.zip + # renovate: datasource=github-tags depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library + https://github.com/sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library/archive/refs/tags/v1.0.6.zip # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 closedcube/ClosedCube OPT3001@1.1.2 # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip - # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core - sensirion/Sensirion Core@0.7.3 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 - # renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x - sensirion/Sensirion I2C SFA3x@1.0.0 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30 - sensirion/Sensirion I2C SCD30@1.0.0 + # renovate: datasource=github-tags depName=Sensirion Core packageName=sensirion/arduino-core + https://github.com/Sensirion/arduino-core/archive/refs/tags/0.7.3.zip + # renovate: datasource=github-tags depName=Sensirion I2C SCD4x packageName=sensirion/arduino-i2c-scd4x + https://github.com/Sensirion/arduino-i2c-scd4x/archive/refs/tags/1.1.0.zip + # renovate: datasource=github-tags depName=Sensirion I2C SFA3x packageName=sensirion/arduino-i2c-sfa3x + https://github.com/Sensirion/arduino-i2c-sfa3x/archive/refs/tags/1.0.0.zip + # renovate: datasource=github-tags depName=Sensirion I2C SCD30 packageName=sensirion/arduino-i2c-scd30 + https://github.com/Sensirion/arduino-i2c-scd30/archive/refs/tags/1.0.0.zip + # renovate: datasource=github-tags depName=arduino-sht packageName=sensirion/arduino-sht + https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip ; Environmental sensors with BSEC2 (Bosch proprietary IAQ) [environmental_extra] lib_deps = ${environmental_extra_common.lib_deps} - # renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2 - boschsensortec/bsec2@1.10.2610 - # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library - boschsensortec/BME68x Sensor Library@1.3.40408 + # renovate: datasource=github-tags depName=Bosch BSEC2 packageName=boschsensortec/Bosch-BSEC2-Library + https://github.com/boschsensortec/Bosch-BSEC2-Library/archive/refs/tags/1.10.2610.zip + # renovate: datasource=github-tags depName=Bosch BME68x packageName=boschsensortec/Bosch-BME68x-Library + https://github.com/boschsensortec/Bosch-BME68x-Library/archive/refs/tags/v1.3.40408.zip ; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets) [environmental_extra_no_bsec] lib_deps = ${environmental_extra_common.lib_deps} - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680 + https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip diff --git a/protobufs b/protobufs index f7f7c8d2e..e30092e61 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit f7f7c8d2e4bf27013efe833d322a2306f2514c39 +Subproject commit e30092e6168b13341c2b7ec4be19c789ad5cd77f diff --git a/renovate.json b/renovate.json index 187cdc600..60c51b59e 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,8 @@ ":dependencyDashboard", ":semanticCommitTypeAll(chore)", ":ignoreModulesAndTests", + ":noUnscheduledUpdates", + "schedule:daily", "group:recommended", "replacements:all", "workarounds:all" diff --git a/src/DebugConfiguration.cpp b/src/DebugConfiguration.cpp index 08c7abc04..207feb8c0 100644 --- a/src/DebugConfiguration.cpp +++ b/src/DebugConfiguration.cpp @@ -98,7 +98,6 @@ Syslog &Syslog::logMask(uint8_t priMask) void Syslog::enable() { - this->_client->begin(this->_port); this->_enabled = true; } @@ -166,14 +165,21 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess if ((pri & LOG_FACMASK) == 0) pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri); + // W5100S: acquire UDP socket on-demand to avoid permanent socket consumption + if (!this->_client->begin(this->_port)) { + return false; + } + if (this->_server != NULL) { result = this->_client->beginPacket(this->_server, this->_port); } else { result = this->_client->beginPacket(this->_ip, this->_port); } - if (result != 1) + if (result != 1) { + this->_client->stop(); return false; + } this->_client->print('<'); this->_client->print(pri); @@ -193,6 +199,8 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess this->_client->print(message); this->_client->endPacket(); + this->_client->stop(); // W5100S: release UDP socket for other services + return true; } diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index d88f9fc9f..fdcf840dc 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -4,7 +4,8 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC bool usePreset) { - // If use_preset is false, always return "Custom" + // If use_preset is false, always return "Custom" โ€” callers such as RadioInterface and Channels + // rely on this being a stable literal for channel-name hashing and default-channel detection. if (!usePreset) { return "Custom"; } diff --git a/src/Power.cpp b/src/Power.cpp index ea4fcf42a..d82c870ed 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -35,6 +35,11 @@ #include "nrfx_power.h" #endif +#if defined(ARCH_NRF52) +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" +#endif + #if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #include "target_specific.h" @@ -328,6 +333,9 @@ class AnalogBatteryLevel : public HasBatteryLevel scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs); scaled *= operativeAdcMultiplier; #else // block for all other platforms +#ifdef ARCH_NRF52 + concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock); +#endif for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) { raw += analogRead(BATTERY_PIN); } diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 2153dbfd5..b11f37cf0 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -38,7 +38,10 @@ static bool isPowered() return true; #endif - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) + ? 1 + : 0); // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON // We assume routers might be powered all the time, but from a low current (solar) source @@ -262,7 +265,10 @@ Fsm powerFSM(&stateBOOT); void PowerFSM_setup() { - bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0); + bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) + ? 1 + : 0); bool hasPower = isPowered(); LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 672c8334c..9450f8990 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -137,7 +137,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, if (color) { ::printf("\u001b[0m"); } - ::printf("| %02d:%02d:%02d %u ", hour, min, sec, millis() / 1000); + ::printf("| %02d:%02d:%02d %u.%03u ", hour, min, sec, millis() / 1000, millis() % 1000); #else printf("%s ", logLevel); if (color) { @@ -151,7 +151,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, if (color) { ::printf("\u001b[0m"); } - ::printf("| ??:??:?? %u ", millis() / 1000); + ::printf("| ??:??:?? %u.%03u ", millis() / 1000, millis() % 1000); #else printf("%s ", logLevel); if (color) { @@ -345,7 +345,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) return; } -void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len) +void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len) { const char alphabet[17] = "0123456789abcdef"; log(logLevel, " +------------------------------------------------+ +----------------+"); diff --git a/src/RedirectablePrint.h b/src/RedirectablePrint.h index 45b62b7af..c66226171 100644 --- a/src/RedirectablePrint.h +++ b/src/RedirectablePrint.h @@ -44,7 +44,7 @@ class RedirectablePrint : public Print /** like printf but va_list based */ size_t vprintf(const char *logLevel, const char *format, va_list arg); - void hexDump(const char *logLevel, unsigned char *buf, uint16_t len); + void hexDump(const char *logLevel, const unsigned char *buf, uint16_t len); std::string mt_sprintf(const std::string fmt_str, ...); diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6fb28a6ac..6692d996d 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -6,6 +6,11 @@ #include "Tone.h" #endif +#if defined(HAS_I2S) +#include "main.h" +#include +#endif + #if !defined(ARCH_PORTDUINO) extern "C" void delay(uint32_t dwMs); #endif @@ -50,6 +55,50 @@ const int DURATION_1_2 = 500; // 1/2 note const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note +#ifdef HAS_I2S +void playTonesRTTTL(const ToneDuration *tone_durations, int size) +{ + // translate ToneDuration[] to RTTTL string and play using audioThread + static std::unordered_map freqToNote = { + {NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"}, + {NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"}, + {NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"}, + {NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}}; + + char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave + for (int i = 0; i < size; i++) { + const auto &td = tone_durations[i]; + std::string note = "b4"; + if (freqToNote.find(td.frequency_khz) != freqToNote.end()) { + note = freqToNote[td.frequency_khz]; + } + int dur = 32; // default duration + if (td.duration_ms >= 1000) + dur = 1; + else if (td.duration_ms >= 500) + dur = 2; + else if (td.duration_ms >= 250) + dur = 4; + else if (td.duration_ms >= 125) + dur = 8; + else if (td.duration_ms >= 62) + dur = 16; + else + dur = 32; + + char noteStr[64]; + snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur); + strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1); + + audioThread->beginRttl(rtttl, strlen(rtttl)); + while (audioThread->isPlaying()) { + delay(10); + } + return; + } +} +#endif + void playTones(const ToneDuration *tone_durations, int size) { if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || @@ -57,7 +106,13 @@ void playTones(const ToneDuration *tone_durations, int size) // Buzzer is disabled or not set to system tones return; } -#ifdef PIN_BUZZER +#ifdef HAS_I2S + if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) { + playTonesRTTTL(tone_durations, size); + return; + } +#endif +#if defined(PIN_BUZZER) if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; #endif diff --git a/src/concurrency/BinarySemaphorePosix.cpp b/src/concurrency/BinarySemaphorePosix.cpp index dc49a489b..4bc60c31f 100644 --- a/src/concurrency/BinarySemaphorePosix.cpp +++ b/src/concurrency/BinarySemaphorePosix.cpp @@ -1,10 +1,85 @@ #include "concurrency/BinarySemaphorePosix.h" #include "configuration.h" +#include +#include + #ifndef HAS_FREE_RTOS namespace concurrency { +#ifdef ARCH_PORTDUINO + +BinarySemaphorePosix::BinarySemaphorePosix() +{ + if (pthread_mutex_init(&mutex, NULL) != 0) { + throw std::runtime_error("pthread_mutex_init failed"); + } + if (pthread_cond_init(&cond, NULL) != 0) { + pthread_mutex_destroy(&mutex); + throw std::runtime_error("pthread_cond_init failed"); + } + signaled = false; +} + +BinarySemaphorePosix::~BinarySemaphorePosix() +{ + pthread_cond_destroy(&cond); + pthread_mutex_destroy(&mutex); +} + +/** + * Returns false if we timed out + */ +bool BinarySemaphorePosix::take(uint32_t msec) +{ + pthread_mutex_lock(&mutex); + + if (!signaled) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + + ts.tv_sec += msec / 1000; + ts.tv_nsec += (msec % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000L; + } + + while (!signaled) { + int rc = pthread_cond_timedwait(&cond, &mutex, &ts); + if (rc == ETIMEDOUT) + break; + if (rc != 0) { + // Some other error occurred + pthread_mutex_unlock(&mutex); + throw std::runtime_error("pthread_cond_timedwait failed: " + std::to_string(rc)); + } + } + } + + bool wasSignaled = signaled; + signaled = false; // consume the signal (binary semaphore) + + pthread_mutex_unlock(&mutex); + return wasSignaled; +} + +void BinarySemaphorePosix::give() +{ + pthread_mutex_lock(&mutex); + signaled = true; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&mutex); +} + +IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) +{ + give(); + if (pxHigherPriorityTaskWoken) + *pxHigherPriorityTaskWoken = true; +} +#else BinarySemaphorePosix::BinarySemaphorePosix() {} @@ -22,7 +97,8 @@ bool BinarySemaphorePosix::take(uint32_t msec) void BinarySemaphorePosix::give() {} IRAM_ATTR void BinarySemaphorePosix::giveFromISR(BaseType_t *pxHigherPriorityTaskWoken) {} +#endif } // namespace concurrency -#endif \ No newline at end of file +#endif diff --git a/src/concurrency/BinarySemaphorePosix.h b/src/concurrency/BinarySemaphorePosix.h index 475b29874..80edb567b 100644 --- a/src/concurrency/BinarySemaphorePosix.h +++ b/src/concurrency/BinarySemaphorePosix.h @@ -2,6 +2,10 @@ #include "../freertosinc.h" +#ifdef ARCH_PORTDUINO +#include +#endif + namespace concurrency { @@ -9,7 +13,12 @@ namespace concurrency class BinarySemaphorePosix { - // SemaphoreHandle_t semaphore; + +#ifdef ARCH_PORTDUINO + pthread_mutex_t mutex; + pthread_cond_t cond; + bool signaled; +#endif public: BinarySemaphorePosix(); @@ -27,4 +36,4 @@ class BinarySemaphorePosix #endif -} // namespace concurrency \ No newline at end of file +} // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h index 53ae30d51..84dabee4e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -78,6 +78,11 @@ along with this program. If not, see . // Configuration // ----------------------------------------------------------------------------- +// Pre-hop drop handling (compile-time flag). +#ifndef MESHTASTIC_PREHOP_DROP +#define MESHTASTIC_PREHOP_DROP 0 +#endif + /// Convert a preprocessor name into a quoted string #define xstr(s) ystr(s) #define ystr(s) #s @@ -149,6 +154,23 @@ along with this program. If not, see . #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #endif +#ifdef USE_KCT8103L_PA +// Power Amps are often non-linear, so we can use an array of values for the power curve +#if defined(HELTEC_WIRELESS_TRACKER_V2) +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7 +#elif defined(HELTEC_MESH_NODE_T096) +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7 +#else +// If a board enables USE_KCT8103L_PA but does not match a known variant and has +// not already provided a PA curve, fail at compile time to avoid unsafe defaults. +#if !defined(NUM_PA_POINTS) || !defined(TX_GAIN_LORA) +#error "USE_KCT8103L_PA is defined, but no PA gain curve (NUM_PA_POINTS / TX_GAIN_LORA) is configured for this board." +#endif +#endif +#endif + #ifdef RAK13302 #define NUM_PA_POINTS 22 #define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 @@ -163,6 +185,10 @@ along with this program. If not, see . #define TX_GAIN_LORA 0 #endif +#ifndef HAS_LORA_FEM +#define HAS_LORA_FEM 0 +#endif + // ----------------------------------------------------------------------------- // Feature toggles // ----------------------------------------------------------------------------- @@ -205,7 +231,7 @@ along with this program. If not, see . #define BME_ADDR 0x76 #define BME_ADDR_ALTERNATE 0x77 #define MCP9808_ADDR 0x18 -#define INA_ADDR 0x40 +#define INA_ADDR 0x40 // same as SHT2X #define INA_ADDR_ALTERNATE 0x41 #define INA_ADDR_WAVESHARE_UPS 0x43 #define INA3221_ADDR 0x42 @@ -218,8 +244,8 @@ along with this program. If not, see . #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D #define SFA30_ADDR 0x5D -#define SHT31_4x_ADDR 0x44 -#define SHT31_4x_ADDR_ALT 0x45 +#define SHTXX_ADDR 0x44 +#define SHTXX_ADDR_ALT 0x45 #define PMSA003I_ADDR 0x12 #define QMA6100P_ADDR 0x12 #define AHT10_ADDR 0x38 @@ -261,6 +287,8 @@ along with this program. If not, see . #define BHI260AP_ADDR 0x28 #define BMM150_ADDR 0x13 #define DA217_ADDR 0x26 +#define BMI270_ADDR 0x68 +#define BMI270_ADDR_ALT 0x69 // ----------------------------------------------------------------------------- // LED @@ -487,6 +515,7 @@ along with this program. If not, see . #define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1 #define MESHTASTIC_EXCLUDE_STOREFORWARD 1 #define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1 +#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1 #define MESHTASTIC_EXCLUDE_ATAK 1 #define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1 #define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1 diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 3389cce91..75eabc954 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; - return firstOfOrNONE(9, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270}; + return firstOfOrNONE(10, types); } ScanI2C::FoundDevice ScanI2C::firstAQI() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 7b575dd63..d451d3948 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -31,9 +31,6 @@ class ScanI2C INA3221, MAX17048, MCP9808, - SHT31, - SHT4X, - SHTC3, LPS22HB, QMC6310U, QMC6310N, @@ -89,10 +86,13 @@ class ScanI2C DA217, CHSC6X, CST226SE, + BMI270, SEN5X, SFA30, CW2015, - SCD30 + SCD30, + ADS1115, + SHTXX } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index c58a58518..052b2245a 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -136,7 +136,9 @@ bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint1 return match; } -/// for SEN5X detection +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +// FIXME Move to a separate file for detection of sensors that require more complex interactions? +// For SEN5X detection // Note, this code needs to be called before setting the I2C bus speed // for the screen at high speed. The speed needs to be at 100kHz, otherwise // detection will not work @@ -174,6 +176,46 @@ String readSEN5xProductName(TwoWire *i2cBus, uint8_t address) return String(productName); } +#endif + +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +bool detectSHT21SerialNumber(TwoWire *i2cBus, uint8_t address) +{ + + i2cBus->beginTransmission(address); + i2cBus->write(0xFA); + i2cBus->write(0x0F); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)8) != 8) + return false; + + // Just flush the data + while (i2cBus->available() < 8) { + i2cBus->read(); + } + + i2cBus->beginTransmission(address); + i2cBus->write(0xFC); + i2cBus->write(0xC9); + + if (i2cBus->endTransmission() != 0) + return false; + + if (i2cBus->requestFrom(address, (uint8_t)6) != 6) + return false; + + // Just flush the data + while (i2cBus->available() < 6) { + i2cBus->read(); + } + + // Assume we detect the SHT21 if something came back from the request + return true; +} +#endif #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ @@ -341,7 +383,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = DPS310; break; } - break; + if (type == DPS310) { + break; + } default: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID switch (registerValue) { @@ -369,7 +413,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; #endif #if !defined(M5STACK_UNITC6L) - case INA_ADDR: + case INA_ADDR: // Same as SHT2X case INA_ADDR_ALTERNATE: case INA_ADDR_WAVESHARE_UPS: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2); @@ -385,7 +429,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("INA260", (uint8_t)addr.address); type = INA260; } - } else { // Assume INA219 if INA260 ID is not found +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + } else if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) { + logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address); + type = SHTXX; +#endif + } else { // Assume INA219 if none of the above ones are found logFoundDevice("INA219", (uint8_t)addr.address); type = INA219; } @@ -446,22 +495,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; } - case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT - case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR + case SHTXX_ADDR: // same as OPT3001_ADDR_ALT + case SHTXX_ADDR_ALT: // same as OPT3001_ADDR if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC) - type = SHT4X; - logFoundDevice("SHT4X", (uint8_t)addr.address); - } else { - type = SHT31; - logFoundDevice("SHT31", (uint8_t)addr.address); + } else { // SHTXX + type = SHTXX; + logFoundDevice("SHTXX", (uint8_t)addr.address); } break; - SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTC3, "SHTC3", (uint8_t)addr.address) + SCAN_SIMPLE_CASE(SHTC3_ADDR, SHTXX, "SHTXX", (uint8_t)addr.address) case RCWL9620_ADDR: // get MAX30102 PARTID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 1); @@ -652,9 +698,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; - case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR - case ICM20948_ADDR_ALT: // same as MPU6050_ADDR - // ICM20948 Register check + case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR + case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 type = ICM20948; @@ -665,7 +710,16 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; + } else if (registerValue == 0x24) { + type = BMI270; + logFoundDevice("BMI270", (uint8_t)addr.address); + break; + } else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00 + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; } else { +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR String prod = ""; prod = readSEN5xProductName(i2cBus, addr.address); if (prod.startsWith("SEN55")) { @@ -681,6 +735,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("Sensirion SEN50", addr.address); break; } +#endif if (addr.address == BMX160_ADDR) { type = BMX160; logFoundDevice("BMX160", (uint8_t)addr.address); @@ -718,11 +773,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (len == 5 && memcmp(expectedInfo, info, len) == 0) { LOG_INFO("NXP SE050 crypto chip found"); type = NXP_SE050; - - } else { - LOG_INFO("FT6336U touchscreen found"); - type = FT6336U; + break; } + + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2); + if (registerValue == 0x8583 || registerValue == 0x8580) { + type = ADS1115; + logFoundDevice("ADS1115 ADC", (uint8_t)addr.address); + break; + } + + LOG_INFO("FT6336U touchscreen found"); + type = FT6336U; break; } diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 2beaeb127..cf5511a33 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -93,7 +93,7 @@ static const char *getGPSPowerStateString(GPSPowerState state) #ifdef PIN_GPS_SWITCH // If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping -// idefinitely +// indefinitely int lastState = LOW; bool firstrun = true; @@ -103,6 +103,14 @@ static int32_t gpsSwitch() if (gps) { int currentState = digitalRead(PIN_GPS_SWITCH); + // Respect explicit NOT_PRESENT mode and do not let the hardware switch re-enable GPS. + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { + gps->disable(); + lastState = currentState; + firstrun = false; + return 1000; + } + // if the switch is set to zero, disable the GPS Thread if (firstrun) if (currentState == LOW) @@ -586,14 +594,14 @@ bool GPS::setup() _serial_gps->write("$PMTK301,2*2E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_ATGM336H) { - // Set the intial configuration of the device - these _should_ work for most AT6558 devices + // Set the initial configuration of the device - these _should_ work for most AT6558 devices msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { LOG_WARN("ATGM336H: Could not set Config"); } - // Set the update frequence to 1Hz + // Set the update frequency to 1Hz msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); _serial_gps->write(UBXscratch, msglen); if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { @@ -700,7 +708,7 @@ bool GPS::setup() } else { // 8,9 LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); } - // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next + // Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next // commands for the M8 it tends to be more... 1 sec should be enough ;>) delay(1000); } @@ -733,7 +741,7 @@ bool GPS::setup() SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); - // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats. + // For M8 we want to enable NMEA version 4.10 so we can see the additional sats. if (gnssModel == GNSS_MODEL_UBLOX8) { clearBuffer(); SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); @@ -1211,7 +1219,7 @@ int32_t GPS::runOnce() return disable(); // This should trigger when we have a fixed position, and get that first position // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms - // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake. + // if not awake we can run super infrequently (once every 5 secs?) to see if we need to wake. return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; } diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 6d1f2da6d..04c25caa3 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -12,7 +12,7 @@ GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _lon GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -20,7 +20,7 @@ GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) { - // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -467,10 +467,10 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB) } /** - * Create a new point bassed on the passed in poin + * Create a new point based on the passed-in point * Ported from http://www.edwilliams.org/avform147.htm#LL * @param bearing - * The bearing in raidans + * The bearing in radians * @param range_meters * range in meters * @return GeoCoord object of point at bearing and range from initial point @@ -593,4 +593,4 @@ double GeoCoord::toRadians(double deg) double GeoCoord::toDegrees(double r) { return r * 180 / PI; -} \ No newline at end of file +} diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index e67bef53e..a8288a069 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -2,6 +2,7 @@ #include "configuration.h" #include "detect/ScanI2C.h" #include "main.h" +#include "modules/NodeInfoModule.h" #include #include #include @@ -12,6 +13,14 @@ uint32_t lastSetFromPhoneNtpOrGps = 0; static uint32_t lastTimeValidationWarning = 0; static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds +static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality) +{ + if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) { + LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality)); + nodeInfoModule->triggerImmediateNodeInfoCheck(); + } +} + RTCQuality getRTCQuality() { return currentQuality; @@ -61,9 +70,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -105,9 +116,11 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } else { @@ -139,9 +152,11 @@ RTCSetResult readFromRTC() } #endif if (currentQuality == RTCQualityNone) { + RTCQuality oldQuality = currentQuality; timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); } return RTCSetResultSuccess; } @@ -214,6 +229,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd } if (shouldSet) { + RTCQuality oldQuality = currentQuality; currentQuality = q; lastSetMsec = now; if (currentQuality >= RTCQualityNTP) { @@ -223,7 +239,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd // This delta value works on all platforms timeStartMsec = now; zeroOffsetSecs = tv->tv_sec; - // If this platform has a setable RTC, set it + // If this platform has a settable RTC, set it #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { Melopero_RV3028 rtc; @@ -281,6 +297,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #endif readFromRTC(); + triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality); return RTCSetResultSuccess; } else { return RTCSetResultNotSet; // RTC was already set with a higher quality time @@ -397,12 +414,23 @@ uint32_t getValidTime(RTCQuality minQuality, bool local) return (currentQuality >= minQuality) ? getTime(local) : 0; } +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot) +{ + currentQuality = RTCQualityNone; + zeroOffsetSecs = 0; + timeStartMsec = millis() - (secondsSinceBoot * 1000); + lastSetFromPhoneNtpOrGps = 0; + lastTimeValidationWarning = 0; +} +#endif + time_t gm_mktime(const struct tm *tm) { #if !MESHTASTIC_EXCLUDE_TZ time_t result = 0; - // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. + // First, get us to the start of tm->year, by calculating the number of days since the Unix epoch. int year = 1900 + tm->tm_year; // tm_year is years since 1900 int year_minus_one = year - 1; int days_before_this_year = 0; diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 16ecd8245..cd1e1d002 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -54,6 +54,10 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false); RTCSetResult readFromRTC(); +#ifdef PIO_UNIT_TESTING +void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot); +#endif + time_t gm_mktime(const struct tm *tm); #define SEC_PER_DAY 86400 diff --git a/src/gps/cas.h b/src/gps/cas.h index 725fd07b3..2a30fd586 100644 --- a/src/gps/cas.h +++ b/src/gps/cas.h @@ -37,7 +37,7 @@ static const uint8_t _message_CAS_CFG_RATE_1HZ[] = { // CFG-NAVX (0x06, 0x07) // Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system -// Qwirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable +// Quirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable // and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used. static const uint8_t _message_CAS_CFG_NAVX_CONF[] = { 0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings diff --git a/src/gps/ubx.h b/src/gps/ubx.h index 0fe2f01fb..8c32ee151 100644 --- a/src/gps/ubx.h +++ b/src/gps/ubx.h @@ -57,7 +57,7 @@ static const uint8_t _message_CFG_PM2[] PROGMEM = { 0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11 }; -// Constallation setup, none required for Neo-6 +// Constellation setup, none required for Neo-6 // For Neo-7 GPS & SBAS static const uint8_t _message_GNSS_7[] = { @@ -157,7 +157,7 @@ static const uint8_t _message_NAVX5[] = { 0x00, 0x00, 0x00, 0x00, // Reserved 9 0x00, // Reserved 10 0x00, // Reserved 11 - 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled) 0x00, // Reserved 12 0x00, // Reserved 13 @@ -185,7 +185,7 @@ static const uint8_t _message_NAVX5_8[] = { 0x00, // Reserved 4 0x00, 0x00, // Reserved 5 0x00, 0x00, // Reserved 6 - 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true) + 0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled) 0x00, 0x00, // Reserved 7 0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default @@ -314,7 +314,7 @@ static const uint8_t _message_DISABLE_TXT_INFO[] = { // This command applies to M8 products static const uint8_t _message_PMS[] = { 0x00, // Version (0) - 0x03, // Power setup value 3 = Agresssive 1Hz + 0x03, // Power setup value 3 = Agressive 1Hz 0x00, 0x00, // period: not applicable, set to 0 0x00, 0x00, // onTime: not applicable, set to 0 0x00, 0x00 // reserved, generated by u-center @@ -337,7 +337,7 @@ static const uint8_t _message_SAVE_10[] = { // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // BBR will survive a restart, and power off for a while, but modules with small backup // batteries or super caps will not retain the config for a long power off time. -// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after +// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast acquisition after // sleep // VALSET Commands for M10 @@ -462,7 +462,7 @@ Default GNSS configuration is: GPS, Galileo, BDS B1l, with QZSS and SBAS enabled The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need the PM config. Lets try without it. PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats. -The defination of "Got Fix" doesn't seem to include SBAS. Much more too this... +The definition of "Got Fix" doesn't seem to include SBAS. Much more too this... Even if it was, it can take minutes (up to 12.5), even under good sat visibility conditions to re-acquire the SBAS data. diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index c05864349..704487bc8 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "EInkDisplay2.h" #include "SPILock.h" #include "main.h" @@ -101,7 +101,7 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) return true; } -// End the update process - virtual method, overriden in derived class +// End the update process - virtual method, overridden in derived class void EInkDisplay::endUpdate() { // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) @@ -143,6 +143,10 @@ bool EInkDisplay::connect() #ifdef ELECROW_ThinkNode_M1 // ThinkNode M1 has a hardware dimmable backlight. Start enabled digitalWrite(PIN_EINK_EN, HIGH); +#elif defined(MINI_EPAPER_S3) + // T-Mini Epaper S3 requires panel power rail enabled before SPI transfer. + digitalWrite(PIN_EINK_EN, HIGH); + delay(10); #else digitalWrite(PIN_EINK_EN, LOW); #endif @@ -202,7 +206,8 @@ bool EInkDisplay::connect() } #elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ - defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) + defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \ + defined(MINI_EPAPER_S3) { // Start HSPI hspi = new SPIClass(HSPI); @@ -216,9 +221,13 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); +#if defined(MINI_EPAPER_S3) + adafruitDisplay->setRotation(3); +#else adafruitDisplay->setRotation(3); #if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) adafruitDisplay->setRotation(0); +#endif #endif } #elif defined(PCA10059) || defined(ME25LS01) @@ -259,17 +268,6 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(3); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } -#elif defined(MINI_EPAPER_S3) - spi1 = new SPIClass(HSPI); - spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS); - - // Create GxEPD2 objects - auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); - adafruitDisplay = new GxEPD2_BW(*lowLevel); - - // Init GxEPD2 - adafruitDisplay->init(); - adafruitDisplay->setRotation(1); #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) // Detect display model, before starting SPI diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index 14adeda12..645a3f2d0 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_EINK +#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #include "GxEPD2_BW.h" #include @@ -89,12 +89,12 @@ class EInkDisplay : public OLEDDisplay // If display uses HSPI #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ - defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) + defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) || \ + defined(MINI_EPAPER_S3) SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \ - defined(MINI_EPAPER_S3) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index 892a4a885..a48ba5c93 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -95,7 +95,7 @@ void EInkDynamicDisplay::adjustRefreshCounters() // Trigger the display update by calling base class bool EInkDynamicDisplay::update() { - // Detemine the refresh mode to use, and start the update + // Determine the refresh mode to use, and start the update bool refreshApproved = determineMode(); if (refreshApproved) { EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system @@ -317,7 +317,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious() LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); } -// Have too many fast-refreshes occured consecutively, since last full refresh? +// Have too many fast-refreshes occurred consecutively, since last full refresh? void EInkDynamicDisplay::checkConsecutiveFastRefreshes() { // If a decision was already reached, don't run the check @@ -561,4 +561,4 @@ void EInkDynamicDisplay::awaitRefresh() } #endif // HAS_EINK_ASYNCFULL -#endif // USE_EINK_DYNAMICDISPLAY \ No newline at end of file +#endif // USE_EINK_DYNAMICDISPLAY diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp new file mode 100644 index 000000000..293f57e81 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.cpp @@ -0,0 +1,427 @@ +#include "EInkParallelDisplay.h" + +#ifdef USE_EINK_PARALLELDISPLAY + +#include "Wire.h" +#include "variant.h" +#include +#include +#include +#include + +#include "FastEPD.h" + +// Thresholds for choosing partial vs full update +#ifndef EPD_PARTIAL_THRESHOLD_ROWS +#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial +#endif +#ifndef EPD_FULLSLOW_PERIOD +#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh +#endif +#ifndef EPD_RESPONSIVE_MIN_MS +#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates +#endif + +EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot) +{ + LOG_INFO("init EInkParallelDisplay"); + // Set dimensions in OLEDDisplay base class + this->geometry = GEOMETRY_RAWMODE; + this->displayWidth = width; + this->displayHeight = height; + + // Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer + uint16_t shortSide = min(width, height); + uint16_t longSide = max(width, height); + if (shortSide % 8 != 0) + shortSide = (shortSide | 7) + 1; + + this->displayBufferSize = longSide * (shortSide / 8); + +#ifdef EINK_LIMIT_GHOSTING_PX + // allocate dirty pixel buffer same size as epaper buffers (rowBytes * height) + size_t rowBytes = (this->displayWidth + 7) / 8; + dirtyPixelsSize = rowBytes * this->displayHeight; + dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1); + ghostPixelCount = 0; +#endif +} + +EInkParallelDisplay::~EInkParallelDisplay() +{ +#ifdef EINK_LIMIT_GHOSTING_PX + if (dirtyPixels) { + free(dirtyPixels); + dirtyPixels = nullptr; + } +#endif + // If an async full update is running, wait for it to finish + if (asyncFullRunning.load()) { + // wait a short while for task to finish + for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) { + delay(50); + } + if (asyncTaskHandle) { + // Let it finish or delete it + vTaskDelete(asyncTaskHandle); + asyncTaskHandle = nullptr; + } + } + + delete epaper; +} + +/* + * Called by the OLEDDisplay::init() path. + */ +bool EInkParallelDisplay::connect() +{ + LOG_INFO("Do EPD init"); + if (!epaper) { + epaper = new FASTEPD; +#if defined(T5_S3_EPAPER_PRO_V1) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); +#elif defined(T5_S3_EPAPER_PRO_V2) + epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + // initialize all port 0 pins (0-7) as outputs / HIGH + for (int i = 0; i < 8; i++) { + epaper->ioPinMode(i, OUTPUT); + epaper->ioWrite(i, HIGH); + } +#else +#error "unsupported EPD device!" +#endif + } + + // epaper->setRotation(rotation); // does not work, messes up width/height + epaper->setMode(BB_MODE_1BPP); + epaper->clearWhite(); + epaper->fullUpdate(true); + +#ifdef EINK_LIMIT_GHOSTING_PX + // After a full/clear the dirty tracking should be reset + resetGhostPixelTracking(); +#endif + + return true; +} + +/* + * sendCommand - simple passthrough (not required for epd_driver-based path) + */ +void EInkParallelDisplay::sendCommand(uint8_t com) +{ + LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com); +} + +/* + * Start a background task that will perform a blocking fullUpdate(). This lets + * display() return quickly while the heavy refresh runs in the background. + */ +void EInkParallelDisplay::startAsyncFullUpdate(int clearMode) +{ + if (asyncFullRunning.load()) + return; // already running + + asyncFullRunning.store(true); + // pass 'this' as parameter + BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t), + this, 2, &asyncTaskHandle, +#if CONFIG_FREERTOS_UNICORE + 0 +#else + 1 +#endif + ); + if (rc != pdPASS) { + LOG_WARN("Failed to create async full-update task, falling back to blocking update"); + epaper->fullUpdate(clearMode, false); + epaper->backupPlane(); + asyncFullRunning.store(false); + asyncTaskHandle = nullptr; + } +} + +/* + * FreeRTOS task entry: runs the full update and then backs up plane. + */ +void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters) +{ + EInkParallelDisplay *self = static_cast(pvParameters); + if (!self) { + vTaskDelete(nullptr); + return; + } + + // choose CLEAR_SLOW occasionally + int clearMode = CLEAR_FAST; + if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) { + clearMode = CLEAR_SLOW; + self->fastRefreshCount = 0; + } else { + // when running async full, treat it as a full so reset fast count + self->fastRefreshCount = 0; + } + + self->epaper->fullUpdate(clearMode, false); + self->epaper->backupPlane(); + +#ifdef EINK_LIMIT_GHOSTING_PX + // A full refresh clears ghosting state + self->resetGhostPixelTracking(); +#endif + + self->asyncFullRunning.store(false); + self->asyncTaskHandle = nullptr; + + // delete this task + vTaskDelete(nullptr); +} + +/* + * Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes + * buffer used by the FASTEPD library. For performance we write directly into FASTEPD's + * currentBuffer() while comparing against previousBuffer() to detect changed rows. + * After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according + * to a heuristic so only the minimal region is refreshed. + */ +void EInkParallelDisplay::display(void) +{ + const uint16_t w = this->displayWidth; + const uint16_t h = this->displayHeight; + + // Simple rate limiting: avoid very-frequent responsive updates + uint32_t nowMs = millis(); + if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) { + LOG_DEBUG("rate-limited, skipping update"); + return; + } + + // bytes per row in epd format (one byte = 8 horizontal pixels) + const uint32_t rowBytes = (w + 7) / 8; + + // Get pointers to internal buffers + uint8_t *cur = epaper->currentBuffer(); + const uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init + + // Track changed row range while converting + int newTop = h; // min changed row (initialized to out-of-range) + int newBottom = -1; // max changed row + +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Track changed byte column range (for clipped fullUpdate fallback) + int newLeftByte = (int)rowBytes; + int newRightByte = -1; +#endif + + // Compute a quick hash of the incoming OLED buffer (so we can skip identical frames) + uint32_t imageHash = 0; + uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size + for (uint32_t bi = 0; bi < bufBytes; ++bi) { + imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31); + } + if (imageHash == previousImageHash) { + // LOG_DEBUG("image identical to previous, skipping update"); + return; + } + +#ifdef EINK_LIMIT_GHOSTING_PX + // reset ghost count for this conversion pass; we'll mark bits that change + ghostPixelCount = 0; +#endif + + // Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur, + // comparing against prev when available to detect changes. + for (uint32_t y = 0; y < h; ++y) { + const uint32_t base = (y >> 3) * w; // (y/8) * width + const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout + const uint32_t rowBase = y * rowBytes; + + // process full 8-pixel bytes + for (uint32_t xb = 0; xb < rowBytes; ++xb) { + uint32_t x0 = xb * 8; + // read up to 8 source bytes (vertical-byte per column) + uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0; + uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0; + uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0; + uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0; + uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0; + uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0; + uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0; + uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0; + + // build output byte: MSB = leftmost pixel + uint8_t out = 0; + out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00); + out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00); + out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00); + out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00); + out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00); + out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00); + out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00); + out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00); + + // handle partial byte at end of row by masking off invalid bits + uint8_t mask = 0xFF; + uint32_t bitsRemain = (w > x0) ? (w - x0) : 0; + if (bitsRemain > 0 && bitsRemain < 8) { + mask = (uint8_t)(0xFF << (8 - bitsRemain)); + out &= mask; + } + + // invert to FASTEPD polarity + out = (~out) & mask; + + uint32_t pos = rowBase + xb; + uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00; + // Consider this byte changed if previous buffer differs (or prev is null) + bool changed = (prev == nullptr) || (prevVal != out); + +#ifdef EINK_LIMIT_GHOSTING_PX + if (changed && prev) + markDirtyBits(prev, pos, mask, out); +#endif + + // mark row changed only if the previous buffer differs + if (changed) { + if (y < (uint32_t)newTop) + newTop = y; + if ((int)y > newBottom) + newBottom = y; +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // record changed column bytes + if ((int)xb < newLeftByte) + newLeftByte = (int)xb; + if ((int)xb > newRightByte) + newRightByte = (int)xb; +#endif + } + + // Always write the computed value into the current buffer (avoid leaving stale bytes) + cur[pos] = (cur[pos] & ~mask) | out; + } + } + + // If nothing changed, avoid any panel update + if (newBottom < 0) { + LOG_DEBUG("no pixel changes detected, skipping update (conv)"); + previousImageHash = imageHash; // still remember that frame + return; + } + + // Choose partial vs full update using heuristic + // Decide if we should force a full update after many fast updates + bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD); + +#ifdef EINK_LIMIT_GHOSTING_PX + // If ghost pixels exceed limit, force a full update to clear ghosting + if (ghostPixelCount > ghostPixelLimit) { + LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit); + forceFull = true; + } +#endif + + // Compute pixel bounds from newTop/newBottom + int startRow = (newTop / 8) * 8; + int endRow = (newBottom / 8) * 8 + 7; + + LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes); + + if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) { + // Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback. +#ifdef FAST_EPD_PARTIAL_UPDATE_BUG + // Workaround for FastEPD partial update bug: use clipped fullUpdate instead + // Build a pixel rectangle for a clipped fullUpdate using the changed columns + int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0; + int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1); + + BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1}; + // LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h); + epaper->fullUpdate(CLEAR_FAST, false, &rect); +#else + // Use rows for partial update + LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow); + epaper->partialUpdate(true, startRow, endRow); +#endif + epaper->backupPlane(); + fastRefreshCount++; + } else { + // Full update: run async if possible (startAsyncFullUpdate will fall back to blocking) + startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST); + } + + lastUpdateMs = millis(); + previousImageHash = imageHash; + + // Keep same behavior as before + lastDrawMsec = millis(); +} + +#ifdef EINK_LIMIT_GHOSTING_PX +// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount +void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out) +{ + // defensive: need dirtyPixels allocated and prevBuf valid + if (!dirtyPixels || !prevBuf) + return; + + // 'out' is in FASTEPD polarity (1 = black, 0 = white) + uint8_t newBlack = out & mask; // bits that will be black now + uint8_t newWhite = (~out) & mask; // bits that will be white now + + // previously recorded dirty bits for this byte + uint8_t before = dirtyPixels[pos]; + + // Ghost bits: bits that were previously marked dirty and are now being driven white + uint8_t ghostBits = before & newWhite; + if (ghostBits) { + ghostPixelCount += __builtin_popcount((unsigned)ghostBits); + } + + // Only mark bits dirty when they turn black now (accumulate until a full refresh) + uint8_t newlyDirty = newBlack & (~before); + if (newlyDirty) { + dirtyPixels[pos] |= newlyDirty; + } +} + +// reset ghost tracking (call after a full refresh) +void EInkParallelDisplay::resetGhostPixelTracking() +{ + if (!dirtyPixels) + return; + memset(dirtyPixels, 0, dirtyPixelsSize); + ghostPixelCount = 0; +} +#endif + +/* + * forceDisplay: use lastDrawMsec + */ +bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) +{ + uint32_t now = millis(); + if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { + display(); + return true; + } + return false; +} + +void EInkParallelDisplay::endUpdate() +{ + { + // ensure any async full update is started/completed + if (asyncFullRunning.load()) { + // nothing to do; background task will run and call backupPlane when done + } else { + epaper->fullUpdate(CLEAR_FAST, false); + epaper->backupPlane(); +#ifdef EINK_LIMIT_GHOSTING_PX + resetGhostPixelTracking(); +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h new file mode 100644 index 000000000..81189e400 --- /dev/null +++ b/src/graphics/EInkParallelDisplay.h @@ -0,0 +1,69 @@ +#pragma once + +#include "configuration.h" + +#ifdef USE_EINK_PARALLELDISPLAY +#include + +#include +#include +#include + +class FASTEPD; + +/** + * Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library + */ +class EInkParallelDisplay : public OLEDDisplay +{ + public: + enum EpdRotation { + EPD_ROT_LANDSCAPE = 0, + EPD_ROT_PORTRAIT = 90, + EPD_ROT_INVERTED_LANDSCAPE = 180, + EPD_ROT_INVERTED_PORTRAIT = 270, + }; + + EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation); + virtual ~EInkParallelDisplay(); + + // OLEDDisplay virtuals + bool connect() override; + void sendCommand(uint8_t com) override; + int getBufferOffset(void) override { return 0; } + + void display(void) override; + bool forceDisplay(uint32_t msecLimit = 1000); + void endUpdate(); + + protected: + uint32_t lastDrawMsec = 0; + FASTEPD *epaper; + + private: + // Async full-refresh support + std::atomic asyncFullRunning{false}; + TaskHandle_t asyncTaskHandle = nullptr; + void startAsyncFullUpdate(int clearMode); + static void asyncFullUpdateTask(void *pvParameters); + +#ifdef EINK_LIMIT_GHOSTING_PX + // helpers + void resetGhostPixelTracking(); + void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out); + void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull); + + // per-bit dirty buffer (same format as epaper buffers): one bit == one pixel + uint8_t *dirtyPixels = nullptr; + size_t dirtyPixelsSize = 0; + uint32_t ghostPixelCount = 0; + uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX; +#endif + + EpdRotation rotation; + uint32_t previousImageHash = 0; + uint32_t lastUpdateMs = 0; + int fastRefreshCount = 0; +}; + +#endif diff --git a/src/graphics/EmoteRenderer.cpp b/src/graphics/EmoteRenderer.cpp new file mode 100644 index 000000000..6fa0adb4c --- /dev/null +++ b/src/graphics/EmoteRenderer.cpp @@ -0,0 +1,434 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/EmoteRenderer.h" +#include +#include + +namespace graphics +{ +namespace EmoteRenderer +{ + +static inline int getStringWidth(OLEDDisplay *display, const char *text, size_t len) +{ +#if defined(OLED_UA) || defined(OLED_RU) + return display->getStringWidth(text, len, true); +#else + (void)len; + return display->getStringWidth(text); +#endif +} + +size_t utf8CharLen(uint8_t c) +{ + if ((c & 0xE0) == 0xC0) + return 2; + if ((c & 0xF0) == 0xE0) + return 3; + if ((c & 0xF8) == 0xF0) + return 4; + return 1; +} + +static inline bool isPossibleEmoteLead(uint8_t c) +{ + // All supported emoji labels in emotes.cpp are currently in these UTF-8 lead ranges. + return c == 0xE2 || c == 0xF0; +} + +static inline int getUtf8ChunkWidth(OLEDDisplay *display, const char *text, size_t len) +{ + char chunk[5] = {0, 0, 0, 0, 0}; + if (len > 4) + len = 4; + memcpy(chunk, text, len); + return getStringWidth(display, chunk, len); +} + +static inline bool isFE0FAt(const char *s, size_t pos, size_t len) +{ + return pos + 2 < len && static_cast(s[pos]) == 0xEF && static_cast(s[pos + 1]) == 0xB8 && + static_cast(s[pos + 2]) == 0x8F; +} + +static inline bool isSkinToneAt(const char *s, size_t pos, size_t len) +{ + return pos + 3 < len && static_cast(s[pos]) == 0xF0 && static_cast(s[pos + 1]) == 0x9F && + static_cast(s[pos + 2]) == 0x8F && + (static_cast(s[pos + 3]) >= 0xBB && static_cast(s[pos + 3]) <= 0xBF); +} + +static inline size_t ignorableModifierLenAt(const char *s, size_t pos, size_t len) +{ + // Skip modifiers that do not change which bitmap we render. + if (isFE0FAt(s, pos, len)) + return 3; + if (isSkinToneAt(s, pos, len)) + return 4; + return 0; +} + +const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet, int emoteCount) +{ + if (!label || !*label) + return nullptr; + + for (int i = 0; i < emoteCount; ++i) { + if (emoteSet[i].label && strcmp(label, emoteSet[i].label) == 0) + return &emoteSet[i]; + } + + return nullptr; +} + +static bool matchAtIgnoringModifiers(const char *text, size_t textLen, size_t pos, const char *label, size_t &textConsumed, + size_t &matchScore) +{ + // Treat FE0F and skin-tone modifiers as optional while matching. + textConsumed = 0; + matchScore = 0; + if (!label || !*label || pos >= textLen) + return false; + + const size_t labelLen = strlen(label); + size_t ti = pos; + size_t li = 0; + + while (true) { + while (ti < textLen) { + const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); + if (!skipLen) + break; + ti += skipLen; + } + while (li < labelLen) { + const size_t skipLen = ignorableModifierLenAt(label, li, labelLen); + if (!skipLen) + break; + li += skipLen; + } + + if (li >= labelLen) { + while (ti < textLen) { + const size_t skipLen = ignorableModifierLenAt(text, ti, textLen); + if (!skipLen) + break; + ti += skipLen; + } + textConsumed = ti - pos; + return textConsumed > 0; + } + + if (ti >= textLen) + return false; + + const uint8_t tc = static_cast(text[ti]); + const uint8_t lc = static_cast(label[li]); + const size_t tlen = utf8CharLen(tc); + const size_t llen = utf8CharLen(lc); + + if (tlen != llen || ti + tlen > textLen || li + llen > labelLen) + return false; + if (memcmp(text + ti, label + li, tlen) != 0) + return false; + + ti += tlen; + li += llen; + matchScore += llen; + } +} + +const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet, int emoteCount) +{ + // Prefer the longest matching label at this byte offset. + const Emote *matched = nullptr; + matchLen = 0; + size_t bestScore = 0; + if (!text || pos >= textLen) + return nullptr; + + if (!isPossibleEmoteLead(static_cast(text[pos]))) + return nullptr; + + for (int i = 0; i < emoteCount; ++i) { + const char *label = emoteSet[i].label; + if (!label || !*label) + continue; + if (static_cast(label[0]) != static_cast(text[pos])) + continue; + + const size_t labelLen = strlen(label); + if (labelLen == 0) + continue; + + size_t candidateLen = 0; + size_t candidateScore = 0; + if (pos + labelLen <= textLen && memcmp(text + pos, label, labelLen) == 0) { + candidateLen = labelLen; + candidateScore = labelLen; + } else if (matchAtIgnoringModifiers(text, textLen, pos, label, candidateLen, candidateScore)) { + // Matched with FE0F/skin tone modifiers treated as optional. + } else { + continue; + } + + if (candidateScore > bestScore) { + matched = &emoteSet[i]; + matchLen = candidateLen; + bestScore = candidateScore; + } + } + + return matched; +} + +static LineMetrics analyzeLineInternal(OLEDDisplay *display, const char *line, size_t lineLen, int fallbackHeight, + const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + // Scan once to collect width and tallest emote for this line. + LineMetrics metrics{0, fallbackHeight, false}; + if (!line) + return metrics; + + for (size_t i = 0; i < lineLen;) { + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + metrics.hasEmote = true; + metrics.tallestHeight = std::max(metrics.tallestHeight, matched->height); + if (display) + metrics.width += matched->width + emoteSpacing; + i += matchLen; + continue; + } + + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + continue; + } + + const size_t charLen = utf8CharLen(static_cast(line[i])); + if (display) + metrics.width += getUtf8ChunkWidth(display, line + i, charLen); + i += charLen; + } + + return metrics; +} + +LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight, const Emote *emoteSet, int emoteCount, + int emoteSpacing) +{ + return analyzeLineInternal(display, line, line ? strlen(line) : 0, fallbackHeight, emoteSet, emoteCount, emoteSpacing); +} + +int maxEmoteHeight(const Emote *emoteSet, int emoteCount) +{ + int tallest = 0; + for (int i = 0; i < emoteCount; ++i) { + if (emoteSet[i].label && *emoteSet[i].label) + tallest = std::max(tallest, emoteSet[i].height); + } + return tallest; +} + +int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + if (!display) + return 0; + + if (!line || !*line) + return 0; + + return analyzeLine(display, line, 0, emoteSet, emoteCount, emoteSpacing).width; +} + +static int appendTextSpanAndMeasure(OLEDDisplay *display, int cursorX, int fontY, const char *text, size_t len, bool draw, + bool fauxBold) +{ + // Draw plain-text runs in chunks so UTF-8 stays intact. + if (!text || len == 0) + return cursorX; + + char chunk[33]; + size_t pos = 0; + while (pos < len) { + size_t chunkLen = 0; + while (pos + chunkLen < len) { + const size_t charLen = utf8CharLen(static_cast(text[pos + chunkLen])); + if (chunkLen + charLen >= sizeof(chunk)) + break; + chunkLen += charLen; + } + + if (chunkLen == 0) { + chunkLen = std::min(len - pos, sizeof(chunk) - 1); + } + + memcpy(chunk, text + pos, chunkLen); + chunk[chunkLen] = '\0'; + if (draw) { + if (fauxBold) + display->drawString(cursorX + 1, fontY, chunk); + display->drawString(cursorX, fontY, chunk); + } + cursorX += getStringWidth(display, chunk, chunkLen); + pos += chunkLen; + } + + return cursorX; +} + +size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis, + const Emote *emoteSet, int emoteCount, int emoteSpacing) +{ + if (!out || outSize == 0) + return 0; + + out[0] = '\0'; + if (!display || !line || maxWidth <= 0) + return 0; + + const size_t lineLen = strlen(line); + const int suffixWidth = + (ellipsis && *ellipsis) ? measureStringWithEmotes(display, ellipsis, emoteSet, emoteCount, emoteSpacing) : 0; + const char *suffix = (ellipsis && suffixWidth <= maxWidth) ? ellipsis : ""; + const size_t suffixLen = strlen(suffix); + const int availableWidth = maxWidth - (*suffix ? suffixWidth : 0); + + if (measureStringWithEmotes(display, line, emoteSet, emoteCount, emoteSpacing) <= maxWidth) { + strncpy(out, line, outSize - 1); + out[outSize - 1] = '\0'; + return strlen(out); + } + + int used = 0; + size_t cut = 0; + for (size_t i = 0; i < lineLen;) { + // Keep whole emotes together when deciding where to cut. + int tokenWidth = 0; + size_t advance = 0; + + if (isPossibleEmoteLead(static_cast(line[i]))) { + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + tokenWidth = matched->width + emoteSpacing; + advance = matchLen; + } + } + + if (advance == 0) { + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + cut = i; + continue; + } + + const size_t charLen = utf8CharLen(static_cast(line[i])); + tokenWidth = getUtf8ChunkWidth(display, line + i, charLen); + advance = charLen; + } + + if (used + tokenWidth > availableWidth) + break; + + used += tokenWidth; + i += advance; + cut = i; + } + + if (cut == 0) { + strncpy(out, suffix, outSize - 1); + out[outSize - 1] = '\0'; + return strlen(out); + } + + size_t copyLen = cut; + if (copyLen > outSize - 1) + copyLen = outSize - 1; + if (suffixLen > 0 && copyLen + suffixLen > outSize - 1) { + copyLen = (outSize - 1 > suffixLen) ? (outSize - 1 - suffixLen) : 0; + } + + memcpy(out, line, copyLen); + size_t totalLen = copyLen; + if (suffixLen > 0 && totalLen < outSize - 1) { + memcpy(out + totalLen, suffix, std::min(suffixLen, outSize - 1 - totalLen)); + totalLen += std::min(suffixLen, outSize - 1 - totalLen); + } + out[totalLen] = '\0'; + return totalLen; +} + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet, + int emoteCount, int emoteSpacing, bool fauxBold) +{ + if (!line) + return; + + const size_t lineLen = strlen(line); + // Center text vertically when any emote is taller than the font. + const int maxIconHeight = + analyzeLineInternal(nullptr, line, lineLen, fontHeight, emoteSet, emoteCount, emoteSpacing).tallestHeight; + const int lineHeight = std::max(fontHeight, maxIconHeight); + const int fontY = y + (lineHeight - fontHeight) / 2; + + int cursorX = x; + bool inBold = false; + + for (size_t i = 0; i < lineLen;) { + // Toggle faux bold. + if (fauxBold && i + 1 < lineLen && line[i] == '*' && line[i + 1] == '*') { + inBold = !inBold; + i += 2; + continue; + } + + const size_t skipLen = ignorableModifierLenAt(line, i, lineLen); + if (skipLen) { + i += skipLen; + continue; + } + + size_t matchLen = 0; + const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount); + if (matched) { + const int iconY = y + (lineHeight - matched->height) / 2; + display->drawXbm(cursorX, iconY, matched->width, matched->height, matched->bitmap); + cursorX += matched->width + emoteSpacing; + i += matchLen; + continue; + } + + size_t next = i; + while (next < lineLen) { + // Stop the text run before the next emote or bold marker. + if (fauxBold && next + 1 < lineLen && line[next] == '*' && line[next + 1] == '*') + break; + + if (ignorableModifierLenAt(line, next, lineLen)) + break; + + size_t nextMatchLen = 0; + if (findEmoteAt(line, lineLen, next, nextMatchLen, emoteSet, emoteCount) != nullptr) + break; + + next += utf8CharLen(static_cast(line[next])); + } + + if (next == i) + next += utf8CharLen(static_cast(line[i])); + + cursorX = appendTextSpanAndMeasure(display, cursorX, fontY, line + i, next - i, true, fauxBold && inBold); + i = next; + } +} + +} // namespace EmoteRenderer +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/graphics/EmoteRenderer.h b/src/graphics/EmoteRenderer.h new file mode 100644 index 000000000..93cee4b25 --- /dev/null +++ b/src/graphics/EmoteRenderer.h @@ -0,0 +1,79 @@ +#pragma once +#include "configuration.h" + +#if HAS_SCREEN +#include "graphics/emotes.h" +#include +#include +#include +#include + +namespace graphics +{ +namespace EmoteRenderer +{ + +struct LineMetrics { + int width; + int tallestHeight; + bool hasEmote; +}; + +size_t utf8CharLen(uint8_t c); + +const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet = emotes, int emoteCount = numEmotes); +const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, + int emoteCount = numEmotes); +inline const Emote *findEmoteAt(const std::string &text, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes, + int emoteCount = numEmotes) +{ + return findEmoteAt(text.c_str(), text.length(), pos, matchLen, emoteSet, emoteCount); +} + +LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight = 0, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1); +inline LineMetrics analyzeLine(OLEDDisplay *display, const std::string &line, int fallbackHeight = 0, + const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1) +{ + return analyzeLine(display, line.c_str(), fallbackHeight, emoteSet, emoteCount, emoteSpacing); +} +int maxEmoteHeight(const Emote *emoteSet = emotes, int emoteCount = numEmotes); + +int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet = emotes, int emoteCount = numEmotes, + int emoteSpacing = 1); +inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1) +{ + return measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing); +} +size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis = "...", const Emote *emoteSet = emotes, int emoteCount = numEmotes, + int emoteSpacing = 1); +inline std::string truncateToWidth(OLEDDisplay *display, const std::string &line, int maxWidth, + const std::string &ellipsis = "...", const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1) +{ + if (!display || maxWidth <= 0) + return ""; + if (measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing) <= maxWidth) + return line; + + std::vector out(line.length() + ellipsis.length() + 1, '\0'); + truncateToWidth(display, line.c_str(), out.data(), out.size(), maxWidth, ellipsis.c_str(), emoteSet, emoteCount, + emoteSpacing); + return std::string(out.data()); +} + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet = emotes, + int emoteCount = numEmotes, int emoteSpacing = 1, bool fauxBold = true); +inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, + const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1, + bool fauxBold = true) +{ + drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSet, emoteCount, emoteSpacing, fauxBold); +} + +} // namespace EmoteRenderer +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c76019fd3..55ec93db5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -27,6 +27,7 @@ along with this program. If not, see . #include "configuration.h" #include "meshUtils.h" #if HAS_SCREEN +#include "EInkParallelDisplay.h" #include #include "DisplayFormatters.h" @@ -364,12 +365,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY) dispdev = new EInkDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#elif defined(USE_EINK_PARALLELDISPLAY) + dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT); #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -433,12 +436,15 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) PMU->enablePowerOutput(XPOWERS_ALDO2); #endif -#if defined(MUZI_BASE) +// some screens seem to need a kick in the pants to turn back on +#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV) dispdev->init(); dispdev->setBrightness(brightness); dispdev->flipScreenVertically(); dispdev->resetDisplay(); +#ifdef SCREEN_12V_ENABLE digitalWrite(SCREEN_12V_ENABLE, HIGH); +#endif delay(100); #endif #if !ARCH_PORTDUINO @@ -462,9 +468,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) ui->init(); #endif -#ifdef USE_ST7789 +#if defined(USE_ST7789) && defined(VTFT_LEDA) +#ifdef VTFT_CTRL pinMode(VTFT_CTRL, OUTPUT); digitalWrite(VTFT_CTRL, LOW); +#endif ui->init(); #ifdef ESP_PLATFORM analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); @@ -506,8 +514,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) #ifdef USE_ST7789 SPI1.end(); #if defined(ARCH_ESP32) +#ifdef VTFT_LEDA pinMode(VTFT_LEDA, ANALOG); +#endif +#ifdef VTFT_CTRL pinMode(VTFT_CTRL, ANALOG); +#endif pinMode(ST7789_RESET, ANALOG); pinMode(ST7789_RS, ANALOG); pinMode(ST7789_NSS, ANALOG); @@ -750,7 +762,11 @@ void Screen::forceDisplay(bool forceUiUpdate) } // Tell EInk class to update the display +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(); +#elif defined(USE_EINK) static_cast(dispdev)->forceDisplay(); +#endif #else // No delay between UI frame rendering if (forceUiUpdate) { @@ -989,8 +1005,10 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) ui->update(); } while (ui->getUiState()->lastUpdate < startUpdate); +#if defined(USE_EINK_PARALLELDISPLAY) + static_cast(dispdev)->forceDisplay(0); +#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) // Old EInkDisplay class -#if !defined(USE_EINK_DYNAMICDISPLAY) static_cast(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit #endif @@ -1002,7 +1020,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) #ifdef EINK_HASQUIRK_GHOSTING EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused" #else - EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh + EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh #endif } #endif diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 31ddf1c84..e259f7691 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -765,7 +765,11 @@ class Screen : public concurrency::OSThread DebugInfo debugInfo; /// Display device +#ifdef USE_ST7789 + ST7789Spi *dispdev; +#else OLEDDisplay *dispdev; +#endif /// UI helper for rendering to frames and switching between them OLEDDisplayUi *ui; diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index ed2e200bb..26276edb2 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -20,7 +20,7 @@ #include "graphics/fonts/OLEDDisplayFontsGR.h" #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK) #include "graphics/fonts/EinkDisplayFonts.h" #endif @@ -90,7 +90,7 @@ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 @@ -106,7 +106,7 @@ #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #endif -#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) +#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO) #undef FONT_SMALL #undef FONT_MEDIUM #undef FONT_LARGE diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index b86f3e32c..ec50654ae 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -121,11 +121,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } // === Screen Title === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(SCREEN_WIDTH / 2, y, titleStr); - if (config.display.heading_bold) { - display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); - } + const char *headerTitle = titleStr ? titleStr : ""; + const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle); + const int titleX = (SCREEN_WIDTH - titleWidth) / 2; + UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -515,4 +514,4 @@ std::string sanitizeString(const std::string &input) } } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 1c2eb72d4..4c8272955 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1254,14 +1254,14 @@ void TFTDisplay::display(bool fromBlank) // Did we find a pixel that needs updating on this row? if (x_FirstPixelUpdate < displayWidth) { + // Align the first pixel for update to an even number so the total alignment of + // the data will be at 32-bit boundary, which is required by GDMA SPI transfers. + x_FirstPixelUpdate &= ~1; - // Quickly write out the first changed pixel (saves another array lookup) - linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; - x_LastPixelUpdate = x_FirstPixelUpdate; - - // Step 3: copy all remaining pixels in this row into the pixel line buffer, - // while also recording the last pixel in the row that needs updating - for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + // Step 3a: copy rest of the pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating. + // Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set. + for (x = x_FirstPixelUpdate; x < displayWidth; x++) { isset = buffer[x + y_byteIndex] & y_byteMask; linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; @@ -1274,6 +1274,14 @@ void TFTDisplay::display(bool fromBlank) x_LastPixelUpdate = x; } } + // Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs. + // Most displays will have even number of pixels in a row -- this will be in bounds + // of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.) + x_LastPixelUpdate |= 1; + // Ensure the last pixel index does not exceed the display width. + if (x_LastPixelUpdate >= displayWidth) { + x_LastPixelUpdate = displayWidth - 1; + } #if defined(HACKADAY_COMMUNICATOR) tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); @@ -1348,7 +1356,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOn(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->wakeup(); tft->powerSaveOff(); #endif @@ -1359,7 +1367,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) #elif !defined(M5STACK) && !defined(ST7789_CS) && \ !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); @@ -1375,7 +1383,7 @@ void TFTDisplay::sendCommand(uint8_t com) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); #elif defined(HACKADAY_COMMUNICATOR) tft->displayOff(); -#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) +#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) tft->sleep(); tft->powerSaveOn(); #endif @@ -1386,7 +1394,7 @@ void TFTDisplay::sendCommand(uint8_t com) #ifdef UNPHONE unphone.backlight(false); // using unPhone library #endif -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) #elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif @@ -1401,7 +1409,7 @@ void TFTDisplay::sendCommand(uint8_t com) void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) // todo #elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); @@ -1421,7 +1429,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->touch() != nullptr; #else return false; @@ -1440,7 +1448,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) return tft->getTouch(x, y); #else return false; @@ -1457,7 +1465,7 @@ bool TFTDisplay::connect() { concurrency::LockGuard g(spiLock); LOG_INFO("Do TFT init"); -#ifdef RAK14014 +#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) tft = new TFT_eSPI; #elif defined(HACKADAY_COMMUNICATOR) bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */); @@ -1494,7 +1502,7 @@ bool TFTDisplay::connect() ft6336u.begin(); pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); -#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) +#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) || defined(HELTEC_MESH_NODE_T096) tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 2069c71ec..7a12650ca 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -408,7 +408,16 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, 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); + // For custom modem settings show the actual parameters; for presets use the preset name. + char modeStr[16]; + if (!config.lora.use_preset) { + snprintf(modeStr, sizeof(modeStr), "BW%u-SF%u-CR%u", static_cast(config.lora.bandwidth), + static_cast(config.lora.spread_factor), static_cast(config.lora.coding_rate)); + } else { + strncpy(modeStr, DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, true), + sizeof(modeStr) - 1); + modeStr[sizeof(modeStr) - 1] = '\0'; + } char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; @@ -416,7 +425,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, if (currentResolution == ScreenResolution::UltraLow) { snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); } else { - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, modeStr); } } textWidth = display->getStringWidth(regionradiopreset); @@ -535,6 +544,9 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #ifndef T_DECK_PRO barsOffset -= 12; #endif +#if defined(T5_S3_EPAPER_PRO) + barsOffset += 60; +#endif #endif int barX = x + barsOffset; if (currentResolution == ScreenResolution::UltraLow) { @@ -584,11 +596,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); uint32_t heapTotal = memGet.getHeapSize(); - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - uint32_t flashUsed = 0, flashTotal = 0; #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); +#endif flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif @@ -607,10 +620,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 +#ifndef T5_S3_EPAPER_PRO if (psramUsed > 0) { line += 1; drawUsageRow("PSRAM:", psramUsed, psramTotal); } +#endif if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f57c39512..e92ba4839 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -18,6 +18,7 @@ #include "main.h" #include "mesh/Default.h" #include "mesh/MeshTypes.h" +#include "mesh/RadioLibInterface.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" @@ -25,6 +26,7 @@ #include "modules/TraceRouteModule.h" #include #include +#include #include #include @@ -159,31 +161,22 @@ void menuHandler::LoraRegionPicker(uint32_t duration) return; } + // Guard: without a reboot, reconfigure() applies the region directly. + // Reject LORA_24 on sub-GHz-only hardware โ€” getRadio() used to catch this post-reboot. + // TODO: change this to either use the validateLoraConfig() logic or at least check the region for wideLora + // rather than a hardcoded check for LORA_24. + if (selectedRegion == meshtastic_Config_LoRaConfig_RegionCode_LORA_24 && + !(RadioLibInterface::instance && RadioLibInterface::instance->wideLora())) { + LOG_WARN("Radio hardware does not support 2.4 GHz; ignoring region selection"); + return; + } + config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; - // FIXME: This should be a method consolidated with the same logic in the admin message as well - // This is needed as we wait til picking the LoRa region to generate keys for the first time. #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); } #endif config.lora.tx_enabled = true; @@ -199,7 +192,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration) } service->reloadConfig(changes); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); bannerOptions.durationMs = duration; @@ -265,13 +257,24 @@ void menuHandler::FrequencySlotPicker() optionsEnumArray[options++] = 0; // Calculate number of channels (copied from RadioInterface::applyModemConfig()) + meshtastic_Config_LoRaConfig &loraConfig = config.lora; double bw = loraConfig.use_preset ? modemPresetToBwKHz(loraConfig.modem_preset, myRegion->wideLora) : bwCodeToKHz(loraConfig.bandwidth); uint32_t numChannels = 0; if (myRegion) { - numChannels = (uint32_t)floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000.0))); + // Match RadioInterface::applyModemConfig(): include padding, add spacing in numerator, and use round() + const double spacing = myRegion->profile->spacing; + const double padding = myRegion->profile->padding; + const double channelBandwidthMHz = bw / 1000.0; + const double numerator = (myRegion->freqEnd - myRegion->freqStart) + spacing; + const double denominator = spacing + (padding * 2) + channelBandwidthMHz; + if (denominator > 0.0) { + numChannels = static_cast(round(numerator / denominator)); + } else { + LOG_WARN("Invalid region configuration: non-positive channel spacing/width"); + } } else { LOG_WARN("Region not set, cannot calculate number of channels"); return; @@ -307,7 +310,6 @@ void menuHandler::FrequencySlotPicker() config.lora.channel_num = selected; service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }; screen->showOverlayBanner(bannerOptions); @@ -346,7 +348,6 @@ void menuHandler::radioPresetPicker() config.lora.channel_num = 0; // Reset to default channel for the preset config.lora.override_frequency = 0; // Clear any custom frequency service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); }); screen->showOverlayBanner(bannerOptions); @@ -1222,9 +1223,11 @@ void menuHandler::positionBaseMenu() }; constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); - constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array baseLabels{}; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); static std::array calibrateLabels{}; +#endif auto onSelection = [](const PositionMenuOption &option, int) -> void { if (option.action == OptionsAction::Back) { @@ -1250,9 +1253,11 @@ void menuHandler::positionBaseMenu() screen->runNow(); break; case PositionAction::CompassCalibrate: +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { accelerometerThread->calibrate(30); } +#endif break; case PositionAction::GPSSmartPosition: menuQueue = GpsSmartPositionMenu; @@ -1270,11 +1275,15 @@ void menuHandler::positionBaseMenu() }; BannerOverlayOptions bannerOptions; +#if !MESHTASTIC_EXCLUDE_ACCELEROMETER if (accelerometerThread) { bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); } else { bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); } +#else + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); +#endif screen->showOverlayBanner(bannerOptions); } @@ -2204,9 +2213,9 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - enum optionsNumbers { Back, NumberPicker, ShowChirpy }; - static const char *optionsArray[4] = {"Back"}; - static int optionsEnumArray[4] = {Back}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce }; + static const char *optionsArray[5] = {"Back"}; + static int optionsEnumArray[5] = {Back}; int options = 1; optionsArray[options] = "Number Picker"; @@ -2214,6 +2223,10 @@ void menuHandler::testMenu() optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; optionsEnumArray[options++] = ShowChirpy; +#ifdef HAS_I2S + optionsArray[options] = "Test Announce"; + optionsEnumArray[options++] = TestAnnounce; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "Hidden Test Menu"; @@ -2228,6 +2241,10 @@ void menuHandler::testMenu() screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); + } else if (selected == TestAnnounce) { +#ifdef HAS_I2S + audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test."); +#endif } else { menuQueue = SystemBaseMenu; screen->runNow(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 79d8b1ccd..501a7ae2c 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -7,6 +7,7 @@ #include "NodeDB.h" #include "UIRenderer.h" #include "gps/RTC.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -34,44 +35,6 @@ static std::vector cachedLines; static std::vector cachedHeights; static bool manualScrolling = false; -// UTF-8 skip helper -static inline size_t utf8CharLen(uint8_t c) -{ - if ((c & 0xE0) == 0xC0) - return 2; - if ((c & 0xF0) == 0xE0) - return 3; - if ((c & 0xF8) == 0xF0) - return 4; - return 1; -} - -// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels -static std::string normalizeEmoji(const std::string &s) -{ - std::string out; - for (size_t i = 0; i < s.size();) { - uint8_t c = static_cast(s[i]); - size_t len = utf8CharLen(c); - - if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) { - i += 3; - continue; - } - - // Skip skin tone modifiers - if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F && - ((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) { - i += 4; - continue; - } - - out.append(s, i, len); - i += len; - } - return out; -} - // Scroll state (file scope so we can reset on new message) float scrollY = 0.0f; uint32_t lastTime = 0; @@ -110,102 +73,7 @@ void scrollDown() void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { - int cursorX = x; - const int fontHeight = FONT_HEIGHT_SMALL; - - // Step 1: Find tallest emote in the line - int maxIconHeight = fontHeight; - for (size_t i = 0; i < line.length();) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (line.compare(i, emojiLen, emotes[e].label) == 0) { - if (emotes[e].height > maxIconHeight) - maxIconHeight = emotes[e].height; - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - i += utf8CharLen(static_cast(line[i])); - } - } - - // Step 2: Baseline alignment - int lineHeight = std::max(fontHeight, maxIconHeight); - int baselineOffset = (lineHeight - fontHeight) / 2; - int fontY = y + baselineOffset; - - // Step 3: Render line in segments - size_t i = 0; - bool inBold = false; - - while (i < line.length()) { - // Check for ** start/end for faux bold - if (line.compare(i, 2, "**") == 0) { - inBold = !inBold; - i += 2; - continue; - } - - // Look ahead for the next emote match - size_t nextEmotePos = std::string::npos; - const Emote *matchedEmote = nullptr; - size_t emojiLen = 0; - - for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].label, i); - if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { - nextEmotePos = pos; - matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].label); - } - } - - // Render normal text segment up to the emote or bold toggle - size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) - nextControl = line.length(); - - if (nextControl > i) { - std::string textChunk = line.substr(i, nextControl - i); - if (inBold) { - // Faux bold: draw twice, offset by 1px - display->drawString(cursorX + 1, fontY, textChunk.c_str()); - } - display->drawString(cursorX, fontY, textChunk.c_str()); -#if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true); -#else - cursorX += display->getStringWidth(textChunk.c_str()); -#endif - i = nextControl; - continue; - } - - // Render the emote (if found) - if (matchedEmote && i == nextEmotePos) { - int iconY = y + (lineHeight - matchedEmote->height) / 2; - display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); - cursorX += matchedEmote->width + 1; - i += emojiLen; - continue; - } else { - // No more emotes โ€” render the rest of the line - std::string remaining = line.substr(i); - if (inBold) { - display->drawString(cursorX + 1, fontY, remaining.c_str()); - } - display->drawString(cursorX, fontY, remaining.c_str()); -#if defined(OLED_UA) || defined(OLED_RU) - cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true); -#else - cursorX += display->getStringWidth(remaining.c_str()); -#endif - break; - } - } + graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, emoteCount); } // Reset scroll state when new messages arrive @@ -377,32 +245,7 @@ static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { - std::string normalized = normalizeEmoji(line); - int totalWidth = 0; - - size_t i = 0; - while (i < normalized.length()) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (normalized.compare(i, emojiLen, emotes[e].label) == 0) { - totalWidth += emotes[e].width + 1; // +1 spacing - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - size_t charLen = utf8CharLen(static_cast(normalized[i])); -#if defined(OLED_UA) || defined(OLED_RU) - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true); -#else - totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str()); -#endif - i += charLen; - } - } - return totalWidth; + return graphics::EmoteRenderer::analyzeLine(display, line, 0, emotes, emoteCount).width; } struct MessageBlock { @@ -417,13 +260,7 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i return lineTopY + (FONT_HEIGHT_SMALL - 1); } - int tallest = FONT_HEIGHT_SMALL; - for (int e = 0; e < numEmotes; ++e) { - if (line.find(emotes[e].label) != std::string::npos) { - if (emotes[e].height > tallest) - tallest = emotes[e].height; - } - } + const int tallest = graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes).tallestHeight; const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest); const int iconTop = lineTopY + (lineHeight - tallest) / 2; @@ -536,30 +373,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; // Title string depending on mode - static char titleBuf[32]; - const char *titleStr = "Messages"; + char titleStr[48]; + snprintf(titleStr, sizeof(titleStr), "Messages"); switch (currentMode) { case ThreadMode::ALL: - titleStr = "Messages"; + snprintf(titleStr, sizeof(titleStr), "Messages"); break; case ThreadMode::CHANNEL: { const char *cname = channels.getName(currentChannel); if (cname && cname[0]) { - snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); + snprintf(titleStr, sizeof(titleStr), "#%s", cname); } else { - snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel); + snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel); } - titleStr = titleBuf; break; } case ThreadMode::DIRECT: { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); - if (node && node->has_user) { - snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name); + if (node && node->has_user && node->user.short_name[0]) { + snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); } else { - snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); } - titleStr = titleBuf; break; } } @@ -666,44 +501,50 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); - char senderBuf[48] = ""; + char senderName[64] = ""; if (node && node->has_user) { - // Use long name if present - strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); - senderBuf[sizeof(senderBuf) - 1] = '\0'; - } else { - // No long/short name โ†’ show NodeID in parentheses - snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender); + if (node->user.long_name[0]) { + strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); + } else if (node->user.short_name[0]) { + strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); + } + senderName[sizeof(senderName) - 1] = '\0'; + } + if (!senderName[0]) { + snprintf(senderName, sizeof(senderName), "(%08x)", m.sender); } - // If this is *our own* message, override senderBuf to who the recipient was + // If this is *our own* message, override senderName to who the recipient was bool mine = (m.sender == nodeDB->getNodeNum()); if (mine && node_recipient && node_recipient->has_user) { - strcpy(senderBuf, node_recipient->user.long_name); + if (node_recipient->user.long_name[0]) { + strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1); + senderName[sizeof(senderName) - 1] = '\0'; + } else if (node_recipient->user.short_name[0]) { + strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1); + senderName[sizeof(senderName) - 1] = '\0'; + } + } + // If recipient info is missing/empty, prefer a recipient identifier for outbound messages. + if (mine && (!node_recipient || !node_recipient->has_user || + (!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) { + snprintf(senderName, sizeof(senderName), "(%08x)", m.dest); } // Shrink Sender name if needed int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - - display->getStringWidth(chanType) - display->getStringWidth(" @..."); + display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @..."); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(senderBuf); - while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) { - senderBuf[strlen(senderBuf) - 1] = '\0'; - } - - // If we actually truncated, append "..." - if (strlen(senderBuf) < origLen) { - strcat(senderBuf, "..."); - } + char truncatedSender[64]; + graphics::UIRenderer::truncateStringWithEmotes(display, senderName, truncatedSender, sizeof(truncatedSender), availWidth); // Final header line - char headerStr[96]; + char headerStr[128]; if (mine) { if (currentMode == ThreadMode::ALL) { if (strcmp(chanType, "(DM)") == 0) { - snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender); } else { snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); } @@ -711,11 +552,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); } } else { - snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + snprintf(headerStr, sizeof(headerStr), chanType[0] ? "%s @%s %s" : "%s @%s", timeBuf, truncatedSender, chanType); } // Push header line - allLines.push_back(std::string(headerStr)); + allLines.push_back(headerStr); isMine.push_back(mine); isHeader.push_back(true); ackForLine.push_back(m.ackStatus); @@ -816,13 +657,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 topY = visualTop - BUBBLE_PAD_TOP_HEADER; } else { // Body start - bool thisLineHasEmote = false; - for (int e = 0; e < numEmotes; ++e) { - if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { - thisLineHasEmote = true; - break; - } - } + const bool thisLineHasEmote = + graphics::EmoteRenderer::analyzeLine(nullptr, cachedLines[b.start].c_str(), 0, emotes, numEmotes).hasEmote; if (thisLineHasEmote) { constexpr int EMOTE_PADDING_ABOVE = 4; visualTop -= EMOTE_PADDING_ABOVE; @@ -851,7 +687,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 for (size_t i = b.start; i <= b.end; ++i) { int w = 0; if (isHeader[i]) { - w = display->getStringWidth(cachedLines[i].c_str()); + w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); if (b.mine) w += 12; // room for ACK/NACK/relay mark } else { @@ -907,7 +743,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (lineY > -cachedHeights[i] && lineY < scrollBottom) { if (isHeader[i]) { - int w = display->getStringWidth(cachedLines[i].c_str()); + int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); int headerX; if (isMine[i]) { // push header left to avoid overlap with scrollbar @@ -917,7 +753,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } else { headerX = x + textIndent; } - display->drawString(headerX, lineY, cachedLines[i].c_str()); + graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1, + false); // Draw underline just under header text int underlineY = lineY + FONT_HEIGHT_SMALL; @@ -1005,11 +842,7 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS } else { word += ch; std::string test = line + word; -#if defined(OLED_UA) || defined(OLED_RU) - uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); -#else - uint16_t strWidth = display->getStringWidth(test.c_str()); -#endif + uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str()); if (strWidth > textWidth) { if (!line.empty()) lines.push_back(line); @@ -1038,31 +871,20 @@ std::vector calculateLineHeights(const std::vector &lines, con std::vector rowHeights; rowHeights.reserve(lines.size()); + std::vector lineMetrics; + lineMetrics.reserve(lines.size()); + + for (const auto &line : lines) { + lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes)); + } for (size_t idx = 0; idx < lines.size(); ++idx) { - const auto &line = lines[idx]; const int baseHeight = FONT_HEIGHT_SMALL; int lineHeight = baseHeight; - // Detect if THIS line or NEXT line contains an emote - bool hasEmote = false; - int tallestEmote = baseHeight; - for (int i = 0; i < numEmotes; ++i) { - if (line.find(emotes[i].label) != std::string::npos) { - hasEmote = true; - tallestEmote = std::max(tallestEmote, emotes[i].height); - } - } - - bool nextHasEmote = false; - if (idx + 1 < lines.size()) { - for (int i = 0; i < numEmotes; ++i) { - if (lines[idx + 1].find(emotes[i].label) != std::string::npos) { - nextHasEmote = true; - break; - } - } - } + const int tallestEmote = lineMetrics[idx].tallestHeight; + const bool hasEmote = lineMetrics[idx].hasEmote; + const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote; if (isHeaderVec[idx]) { // Header line spacing @@ -1112,22 +934,22 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht // Banner logic const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); - char longName[48] = "?"; - if (node && node->user.long_name) { - strncpy(longName, node->user.long_name, sizeof(longName) - 1); - longName[sizeof(longName) - 1] = '\0'; + char longName[64] = "?"; + if (node && node->has_user) { + if (node->user.long_name[0]) { + strncpy(longName, node->user.long_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } else if (node->user.short_name[0]) { + strncpy(longName, node->user.short_name, sizeof(longName) - 1); + longName[sizeof(longName) - 1] = '\0'; + } } int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(longName); - while (longName[0] && display->getStringWidth(longName) > availWidth) { - longName[strlen(longName) - 1] = '\0'; - } - if (strlen(longName) < origLen) { - strcat(longName, "..."); - } + char truncatedLongName[64]; + graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName), + availWidth); const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); char banner[256]; @@ -1145,8 +967,8 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht } if (isAlert) { - if (longName && longName[0]) - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + if (truncatedLongName[0]) + snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName); else strcpy(banner, "Alert Received"); } else { @@ -1154,11 +976,11 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht if (isChannelMuted) return; - if (longName && longName[0]) { + if (truncatedLongName[0]) { if (currentResolution == ScreenResolution::UltraLow) { strcpy(banner, "New Message"); } else { - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName); } } else strcpy(banner, "New Message"); @@ -1221,4 +1043,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) } // namespace MessageRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index b36a5057c..98644ee3b 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -79,13 +79,15 @@ void scrollDown() // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) +std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { - static char nodeName[25]; // single static buffer we return - nodeName[0] = '\0'; + (void)display; + (void)columnWidth; - auto writeFallbackId = [&] { - std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + auto fallbackId = [&] { + char id[12]; + std::snprintf(id, sizeof(id), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + return std::string(id); }; // 1) Choose target candidate (long vs short) only if present @@ -94,42 +96,10 @@ const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; } - // 2) Sanitize (empty if raw is null/empty) - std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - - // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed) - if (s.empty() || s == "ยฟ" || s.find_first_not_of("ยฟ") == std::string::npos) { - writeFallbackId(); - } else { - // %.*s ensures null-termination and safe truncation to buffer size - 1 - std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast(sizeof(nodeName) - 1), s.c_str()); - } - - // 4) Width-based truncation + ellipsis (long-name mode only) - if (config.display.use_long_node_name && display) { - int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); - if (availWidth < 0) - availWidth = 0; - - const size_t beforeLen = std::strlen(nodeName); - - // Trim from the end until it fits or is empty - size_t len = beforeLen; - while (len && display->getStringWidth(nodeName) > availWidth) { - nodeName[--len] = '\0'; - } - - // If truncated, append "..." (respect buffer size) - if (len < beforeLen) { - // Make sure there's room for "..." and '\0' - const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' - const size_t needed = 3; // "..." - if (len > capForText - needed) { - len = capForText - needed; - nodeName[len] = '\0'; - } - std::strcat(nodeName, "..."); - } + // 2) Preserve UTF-8 names so emotes can be detected and rendered. + std::string nodeName = (raw && *raw) ? std::string(raw) : std::string{}; + if (nodeName.empty()) { + nodeName = fallbackId(); } return nodeName; @@ -163,6 +133,15 @@ const char *getCurrentModeTitle_Location(int screenWidth) } } +static int getNodeNameMaxWidth(int columnWidth, int baseWidth) +{ + if (!config.display.use_long_node_name) + return baseWidth; + + const int legacyLongNameWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38); + return std::max(0, std::min(baseWidth, legacyLongNameWidth)); +} + // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { @@ -205,10 +184,13 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; + int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char timeStr[10]; @@ -228,7 +210,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -255,19 +237,22 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - 25; + int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -312,9 +297,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) + : (isLeftCol ? 20 : 22))); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; char distStr[10] = ""; @@ -368,7 +357,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -414,14 +403,18 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Adjust max text width depending on column and screen width int nameMaxWidth = - columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) + : (isLeftCol ? 20 : 22))); - const char *nodeName = getSafeNodeName(display, node, columnWidth); + const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); + char nodeName[96]; + UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName), + nameMaxWidth); bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); + UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); if (node->is_favorite) { if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -828,4 +821,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields } // namespace NodeListRenderer } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index e212c031b..be80a7d80 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -4,6 +4,7 @@ #include "mesh/generated/meshtastic/mesh.pb.h" #include #include +#include namespace graphics { @@ -56,7 +57,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, // Utility functions const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth); -const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); +std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); // Scrolling controls diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 8d76b4592..31eb2c3c8 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -4,6 +4,7 @@ #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" +#include "UIRenderer.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" @@ -43,7 +44,7 @@ InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever -uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are selectable options const char **NotificationRenderer::optionsArrayPtr = nullptr; const int *NotificationRenderer::optionsEnumPtr = nullptr; std::function NotificationRenderer::alertBannerCallback = NULL; @@ -95,7 +96,7 @@ void NotificationRenderer::resetBanner() inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.kbchar = 0; curSelected = 0; - alertBannerOptions = 0; // last x lines are seelctable options + alertBannerOptions = 0; // last x lines are selectable options optionsArrayPtr = nullptr; optionsEnumPtr = nullptr; alertBannerCallback = NULL; @@ -299,7 +300,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta for (int i = 0; i < lineCount; i++) { linePointers[i] = lineStarts[i]; } - char scratchLineBuffer[visibleTotalLines - lineCount][40]; + char scratchLineBuffer[visibleTotalLines - lineCount][64]; uint8_t firstOptionToShow = 0; if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { @@ -312,28 +313,47 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } int scratchLineNum = 0; for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { - char temp_name[16] = {0}; - if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { - std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); - strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); + char tempName[48] = {0}; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); + if (node && node->has_user) { + const char *rawName = nullptr; + if (node->user.long_name[0]) { + rawName = node->user.long_name; + } else if (node->user.short_name[0]) { + rawName = node->user.short_name; + } + if (rawName) { + const int arrowWidth = (currentResolution == ScreenResolution::High) + ? UIRenderer::measureStringWithEmotes(display, "> <") + : UIRenderer::measureStringWithEmotes(display, "><"); + const int maxTextWidth = std::max(0, display->getWidth() - 28 - arrowWidth); + UIRenderer::truncateStringWithEmotes(display, rawName, tempName, sizeof(tempName), maxTextWidth); + } } else { - snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); + snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); + } + if (!tempName[0]) { + snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); } if (i == curSelected) { - selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; + selectedNodenum = node ? node->num : 0; if (currentResolution == ScreenResolution::High) { strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); - strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); + strncpy(scratchLineBuffer[scratchLineNum] + 2, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 3); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; + const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); + strncpy(scratchLineBuffer[scratchLineNum] + used, " <", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } else { strncpy(scratchLineBuffer[scratchLineNum], ">", 2); - strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37); - strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2); + strncpy(scratchLineBuffer[scratchLineNum] + 1, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 2); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; + const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1); + strncpy(scratchLineBuffer[scratchLineNum] + used, "<", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1); } - scratchLineBuffer[scratchLineNum][39] = '\0'; + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } else { - strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); - scratchLineBuffer[scratchLineNum][39] = '\0'; + strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1); + scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; } linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; } @@ -501,7 +521,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay else // if the newline wasn't found, then pull string length from strlen lineLengths[lineCount] = strlen(lines[lineCount]); - lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + if (current_notification_type == notificationTypeEnum::node_picker) { + char measureBuffer[64] = {0}; + strncpy(measureBuffer, lines[lineCount], std::min(lineLengths[lineCount], sizeof(measureBuffer) - 1)); + lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer); + } else { + lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); + } // Consider extra width for signal bars on lines that contain "Signal:" uint16_t potentialWidth = lineWidths[lineCount]; @@ -607,7 +633,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); display->setColor(BLACK); int yOffset = 3; - display->drawString(textX, lineY - yOffset, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(textX, lineY - yOffset, lineBuffer); + } display->setColor(WHITE); lineY += (effectiveLineHeight - 2 - background_yOffset); } else { @@ -626,7 +656,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay int totalWidth = textWidth + barsWidth; int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; - display->drawString(groupStartX, lineY, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(groupStartX, lineY, lineBuffer); + } int baseX = groupStartX + textWidth + gap; int baseY = lineY + effectiveLineHeight - 1; @@ -642,7 +676,11 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay } } } else { - display->drawString(textX, lineY, lineBuffer); + if (current_notification_type == notificationTypeEnum::node_picker) { + UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false); + } else { + display->drawString(textX, lineY, lineBuffer); + } } lineY += (effectiveLineHeight); } @@ -781,4 +819,4 @@ void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, } } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index e51bfa5ab..45b05be9c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -22,7 +22,7 @@ class NotificationRenderer static uint32_t alertBannerUntil; // 0 is a special case meaning forever static const char **optionsArrayPtr; static const int *optionsEnumPtr; - static uint8_t alertBannerOptions; // last x lines are seelctable options + static uint8_t alertBannerOptions; // last x lines are selectable options static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 25a70f16d..e3a4d13a2 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -8,6 +8,7 @@ #include "UIRenderer.h" #include "airtime.h" #include "gps/GeoCoord.h" +#include "graphics/EmoteRenderer.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" @@ -313,8 +314,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i #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"; - char titlestr[32] = {0}; + const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; + char titlestr[40]; snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); // === Draw battery/time/mail header (common across screens) === @@ -328,7 +329,6 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i // 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) === const char *username; if (currentResolution == ScreenResolution::UltraLow) { @@ -338,9 +338,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i } if (username) { - usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case // Print node's long name (e.g. "Backpack Node") - display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); + UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); } // === 2. Signal and Hops (combined on one line, if available) === @@ -802,14 +801,12 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === 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 : ""); + const char *shortName = owner.short_name ? owner.short_name : ""; // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); + textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false); #else if (powerStatus->getHasBattery()) { char batStr[20]; @@ -904,36 +901,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int textWidth = 0; int nameX = 0; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; - std::string longNameStr; - - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longNameStr = sanitizeString(ourNode->user.long_name); + const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; + const char *shortName = owner.short_name ? owner.short_name : ""; + char combinedName[96]; + if (longName[0] && shortName[0]) { + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortName); + } else if (longName[0]) { + strncpy(combinedName, longName, sizeof(combinedName) - 1); + combinedName[sizeof(combinedName) - 1] = '\0'; + } else { + strncpy(combinedName, shortName, sizeof(combinedName) - 1); + combinedName[sizeof(combinedName) - 1] = '\0'; } - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s", - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); - if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) { - size_t len = strlen(combinedName); - if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(combinedName); + if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) { + textWidth = UIRenderer::measureStringWithEmotes(display, combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString( - nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + UIRenderer::drawStringWithEmotes( + display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, + combinedName, FONT_HEIGHT_SMALL, 1, false); } else { // === LongName Centered === - textWidth = display->getStringWidth(longNameStr.c_str()); + textWidth = UIRenderer::measureStringWithEmotes(display, longName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1, + false); // === ShortName Centered === - textWidth = display->getStringWidth(shortnameble); + textWidth = UIRenderer::measureStringWithEmotes(display, shortName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, + false); } #endif graphics::drawCommonFooter(display, x, y); @@ -1045,12 +1042,12 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState display->setTextAlignment(TEXT_ALIGN_LEFT); const char *pauseText = "Screen Paused"; const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); + const bool useId = (idText && idText[0]); constexpr uint8_t padding = 2; constexpr uint8_t dividerGap = 1; // Text widths - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); + const uint16_t idTextWidth = useId ? UIRenderer::measureStringWithEmotes(display, idText) : 0; const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); @@ -1075,7 +1072,7 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState // Draw: text if (useId) - display->drawString(idTextLeft, idTextTop, idText); + UIRenderer::drawStringWithEmotes(display, idTextLeft, idTextTop, idText, FONT_HEIGHT_SMALL, 1, false); display->drawString(pauseTextLeft, pauseTextTop, pauseText); display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold @@ -1108,11 +1105,16 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED 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); + char footer[64]; + if (owner.short_name && owner.short_name[0]) { + snprintf(footer, sizeof(footer), "%s %s", xstr(APP_VERSION_SHORT), owner.short_name); + } else { + snprintf(footer, sizeof(footer), "%s", xstr(APP_VERSION_SHORT)); + } + int footerW = UIRenderer::measureStringWithEmotes(display, footer); + int footerX = x + ((SCREEN_WIDTH - footerW) / 2); + UIRenderer::drawStringWithEmotes(display, footerX, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, 1, + false); screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -1130,12 +1132,15 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED display->drawString(x + 0, y + 0, upperMsg); // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), - graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); + const char *version = xstr(APP_VERSION_SHORT); + int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); + display->drawString(versionX, y + 0, version); + if (owner.short_name && owner.short_name[0]) { + const char *shortName = owner.short_name; + int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); + int shortNameX = x + SCREEN_WIDTH - shortNameW; + UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -1365,11 +1370,15 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O display->drawString(x + 0, y + 0, upperMsg); // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); + const char *version = xstr(APP_VERSION_SHORT); + int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); + display->drawString(versionX, y + 0, version); + if (owner.short_name && owner.short_name[0]) { + const char *shortName = owner.short_name; + int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); + int shortNameX = x + SCREEN_WIDTH - shortNameW; + UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false); + } screen->forceDisplay(); display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code @@ -1558,6 +1567,25 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi return uptime; } +int UIRenderer::measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing) +{ + return graphics::EmoteRenderer::measureStringWithEmotes(display, line, graphics::emotes, graphics::numEmotes, emoteSpacing); +} + +size_t UIRenderer::truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis, int emoteSpacing) +{ + return graphics::EmoteRenderer::truncateToWidth(display, line, out, outSize, maxWidth, ellipsis, graphics::emotes, + graphics::numEmotes, emoteSpacing); +} + +void UIRenderer::drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing, + bool fauxBold) +{ + graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, fontHeight, graphics::emotes, graphics::numEmotes, + emoteSpacing, fauxBold); +} + } // namespace graphics #endif // HAS_SCREEN diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 8f0d07881..a0bb0d849 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "NodeDB.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/emotes.h" #include @@ -80,6 +81,28 @@ class UIRenderer static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + // Shared BaseUI emote helpers. + static int measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing = 1); + static inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, int emoteSpacing = 1) + { + return measureStringWithEmotes(display, line.c_str(), emoteSpacing); + } + static size_t truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, + const char *ellipsis = "...", int emoteSpacing = 1); + static inline std::string truncateStringWithEmotes(OLEDDisplay *display, const std::string &line, int maxWidth, + const std::string &ellipsis = "...", int emoteSpacing = 1) + { + return graphics::EmoteRenderer::truncateToWidth(display, line, maxWidth, ellipsis, graphics::emotes, graphics::numEmotes, + emoteSpacing); + } + static void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing = 1, + bool fauxBold = true); + static inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight, + int emoteSpacing = 1, bool fauxBold = true) + { + drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold); + } + // Check if the display can render a string (detect special chars; emoji) static bool haveGlyphs(const char *str); }; // namespace UIRenderer diff --git a/src/graphics/fonts/OLEDDisplayFontsRU.cpp b/src/graphics/fonts/OLEDDisplayFontsRU.cpp index 3a1159511..9766d36b2 100644 --- a/src/graphics/fonts/OLEDDisplayFontsRU.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsRU.cpp @@ -10,7 +10,7 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = { 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1766,4 +1766,4 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = { 0x3F, // 255 }; -#endif // OLED_RU \ No newline at end of file +#endif // OLED_RU diff --git a/src/graphics/fonts/OLEDDisplayFontsUA.cpp b/src/graphics/fonts/OLEDDisplayFontsUA.cpp index 8bc56ea94..deafa77aa 100644 --- a/src/graphics/fonts/OLEDDisplayFontsUA.cpp +++ b/src/graphics/fonts/OLEDDisplayFontsUA.cpp @@ -9,7 +9,7 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = { 0x20, // First char: 32 0xE0, // Number of chars: 224 // Jump Table: - 0xFF, 0xFF, 0x00, 0x0A, // 32 + 0xFF, 0xFF, 0x00, 0x03, // 32 0x00, 0x00, 0x04, 0x03, // 33 0x00, 0x04, 0x05, 0x04, // 34 0x00, 0x09, 0x09, 0x06, // 35 @@ -1924,4 +1924,4 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = { 0xFF, // 1103 }; -#endif // OLED_UA \ No newline at end of file +#endif // OLED_UA diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index 3ce16e473..e37969edf 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -37,8 +37,8 @@ class DEPG0213BNS800 : public SSD16XX void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers -#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index 257fed1a6..761cf772a 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -35,8 +35,8 @@ class DEPG0290BNS800 : public SSD16XX void configWaveform() override; void configUpdateSequence() override; void detachFromUpdate() override; - void finalizeUpdate() override; // Only overriden for a slight optimization + void finalizeUpdate() override; // Only overridden for a slight optimization }; } // namespace NicheGraphics::Drivers -#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp b/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp new file mode 100644 index 000000000..a670db0d0 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp @@ -0,0 +1,178 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./GDEW0102T4.h" + +#include + +using namespace NicheGraphics::Drivers; + +// LUTs from GxEPD2_102.cpp (GDEW0102T4 / UC8175). +static const uint8_t LUT_W_FULL[] = { + 0x60, 0x5A, 0x5A, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_B_FULL[] = { + 0x90, 0x5A, 0x5A, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_W_FAST[] = { + 0x60, 0x01, 0x01, 0x00, 0x00, 0x01, // + 0x80, 0x12, 0x00, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +static const uint8_t LUT_B_FAST[] = { + 0x90, 0x01, 0x01, 0x00, 0x00, 0x01, // + 0x40, 0x14, 0x00, 0x00, 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // +}; + +GDEW0102T4::GDEW0102T4() : UC8175(width, height, supported) {} + +void GDEW0102T4::setFastConfig(FastConfig cfg) +{ + // Clamp out only clearly invalid PLL settings. + if (cfg.reg30 < 0x05) + cfg.reg30 = 0x05; + fastConfig = cfg; +} + +GDEW0102T4::FastConfig GDEW0102T4::getFastConfig() const +{ + return fastConfig; +} + +void GDEW0102T4::configCommon() +{ + // Init path aligned with GxEPD2_GDEW0102T4 (UC8175 family). + sendCommand(0xD2); + sendData(0x3F); + + sendCommand(0x00); + sendData(0x6F); + + sendCommand(0x01); + sendData(0x03); + sendData(0x00); + sendData(0x2B); + sendData(0x2B); + + sendCommand(0x06); + sendData(0x3F); + + sendCommand(0x2A); + sendData(0x00); + sendData(0x00); + + sendCommand(0x30); // PLL / drive clock + sendData(0x13); + + sendCommand(0x50); // Last border/data interval; subtle but can affect artifacts + sendData(0x57); + + sendCommand(0x60); + sendData(0x22); + + sendCommand(0x61); + sendData(width); + sendData(height); + + sendCommand(0x82); // VCOM DC setting + sendData(0x12); + + sendCommand(0xE3); + sendData(0x33); +} + +void GDEW0102T4::configFull() +{ + sendCommand(0x23); + sendData(LUT_W_FULL, sizeof(LUT_W_FULL)); + sendCommand(0x24); + sendData(LUT_B_FULL, sizeof(LUT_B_FULL)); + + powerOn(); +} + +void GDEW0102T4::configFast() +{ + uint8_t lutW[sizeof(LUT_W_FAST)]; + uint8_t lutB[sizeof(LUT_B_FAST)]; + memcpy(lutW, LUT_W_FAST, sizeof(LUT_W_FAST)); + memcpy(lutB, LUT_B_FAST, sizeof(LUT_B_FAST)); + + // Second stage duration bytes are the main "darkness vs ghosting" control for this panel. + lutW[7] = fastConfig.lutW2; + lutB[7] = fastConfig.lutB2; + + sendCommand(0x30); + sendData(fastConfig.reg30); + + sendCommand(0x50); + sendData(fastConfig.reg50); + + sendCommand(0x82); + sendData(fastConfig.reg82); + + sendCommand(0x23); + sendData(lutW, sizeof(lutW)); + sendCommand(0x24); + sendData(lutB, sizeof(lutB)); + + powerOn(); +} + +void GDEW0102T4::writeOldImage() +{ + // On this panel, FULL refresh is most reliable when "old image" is all white. + if (updateType == FULL) { + sendCommand(0x10); + // Use buffered writes of 0xFF to avoid per-byte SPI transactions. + const uint16_t chunkSize = 64; + uint8_t ffBuf[chunkSize]; + memset(ffBuf, 0xFF, sizeof(ffBuf)); + + uint32_t remaining = bufferSize; + while (remaining > 0) { + uint16_t toSend = remaining > chunkSize ? chunkSize : static_cast(remaining); + sendData(ffBuf, toSend); + remaining -= toSend; + } + return; + } + + // FAST refresh uses differential data (previous frame as old image). + if (previousBuffer) { + writeImage(0x10, previousBuffer); + } else { + writeImage(0x10, buffer); + } +} + +void GDEW0102T4::finalizeUpdate() +{ + // Keep panel out of deep-sleep between updates for better reliability of repeated FAST refresh. + powerOff(); +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/GDEW0102T4.h b/src/graphics/niche/Drivers/EInk/GDEW0102T4.h new file mode 100644 index 000000000..02df8b4fe --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEW0102T4.h @@ -0,0 +1,55 @@ +/* + +E-Ink display driver + - GDEW0102T4 + - Controller: UC8175 + - Size: 1.02 inch + - Resolution: 80px x 128px + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./UC8175.h" + +namespace NicheGraphics::Drivers +{ + +class GDEW0102T4 : public UC8175 +{ + private: + static constexpr uint16_t width = 80; + static constexpr uint16_t height = 128; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + struct FastConfig { + uint8_t reg30; + uint8_t reg50; + uint8_t reg82; + uint8_t lutW2; + uint8_t lutB2; + }; + + GDEW0102T4(); + void setFastConfig(FastConfig cfg); + FastConfig getFastConfig() const; + + protected: + void configCommon() override; + void configFull() override; + void configFast() override; + void writeOldImage() override; + void finalizeUpdate() override; + + private: + FastConfig fastConfig = {0x13, 0xF2, 0x12, 0x0E, 0x14}; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/README.md b/src/graphics/niche/Drivers/EInk/README.md index eca91c6a8..33833f0cd 100644 --- a/src/graphics/niche/Drivers/EInk/README.md +++ b/src/graphics/niche/Drivers/EInk/README.md @@ -19,7 +19,7 @@ void setupNicheGraphics() SPIClass *hspi = new SPIClass(HSPI); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); - // Setup Enk driver + // Setup EInk driver Drivers::EInk *driver = new Drivers::DEPG0290BNS800; driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); diff --git a/src/graphics/niche/Drivers/EInk/UC8175.cpp b/src/graphics/niche/Drivers/EInk/UC8175.cpp new file mode 100644 index 000000000..576b645bd --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/UC8175.cpp @@ -0,0 +1,203 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./UC8175.h" + +#include + +#include "SPILock.h" + +using namespace NicheGraphics::Drivers; + +UC8175::UC8175(uint16_t width, uint16_t height, UpdateTypes supported) : EInk(width, height, supported) +{ + bufferRowSize = ((width - 1) / 8) + 1; + bufferSize = bufferRowSize * height; +} + +void UC8175::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) +{ + this->spi = spi; + this->pin_dc = pin_dc; + this->pin_cs = pin_cs; + this->pin_busy = pin_busy; + this->pin_rst = pin_rst; + + pinMode(pin_dc, OUTPUT); + pinMode(pin_cs, OUTPUT); + pinMode(pin_busy, INPUT); + + // Reset is active LOW, hold HIGH when idle. + if (pin_rst != (uint8_t)-1) { + pinMode(pin_rst, OUTPUT); + digitalWrite(pin_rst, HIGH); + } + + if (!previousBuffer) { + previousBuffer = new uint8_t[bufferSize]; + if (previousBuffer) + memset(previousBuffer, 0xFF, bufferSize); + } +} + +void UC8175::update(uint8_t *imageData, UpdateTypes type) +{ + buffer = imageData; + updateType = (type == UpdateTypes::UNSPECIFIED) ? UpdateTypes::FULL : type; + + if (updateType == FAST && hasPreviousBuffer && previousBuffer && memcmp(previousBuffer, buffer, bufferSize) == 0) + return; + + reset(); + configCommon(); + + if (updateType == FAST) + configFast(); + else + configFull(); + + writeOldImage(); + writeNewImage(); + sendCommand(0x12); // Display refresh. + + if (previousBuffer) { + memcpy(previousBuffer, buffer, bufferSize); + hasPreviousBuffer = true; + } + + detachFromUpdate(); +} + +void UC8175::wait(uint32_t timeoutMs) +{ + if (failed) + return; + + uint32_t started = millis(); + while (digitalRead(pin_busy) == BUSY_ACTIVE) { + if ((millis() - started) > timeoutMs) { + failed = true; + break; + } + yield(); + } +} + +void UC8175::reset() +{ + if (pin_rst != (uint8_t)-1) { + digitalWrite(pin_rst, LOW); + delay(20); + digitalWrite(pin_rst, HIGH); + delay(20); + } else { + sendCommand(0x12); // Software reset. + delay(10); + } + + wait(3000); +} + +void UC8175::sendCommand(uint8_t command) +{ + if (failed) + return; + + spiLock->lock(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, LOW); + digitalWrite(pin_cs, LOW); + spi->transfer(command); + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); + spiLock->unlock(); +} + +void UC8175::sendData(uint8_t data) +{ + sendData(&data, 1); +} + +void UC8175::sendData(const uint8_t *data, uint32_t size) +{ + if (failed) + return; + + spiLock->lock(); + spi->beginTransaction(spiSettings); + digitalWrite(pin_dc, HIGH); + digitalWrite(pin_cs, LOW); + +#if defined(ARCH_ESP32) + spi->transferBytes(data, NULL, size); +#elif defined(ARCH_NRF52) + spi->transfer(data, NULL, size); +#else + for (uint32_t i = 0; i < size; ++i) + spi->transfer(data[i]); +#endif + + digitalWrite(pin_cs, HIGH); + digitalWrite(pin_dc, HIGH); + spi->endTransaction(); + spiLock->unlock(); +} + +void UC8175::powerOn() +{ + sendCommand(0x04); + wait(2000); +} + +void UC8175::powerOff() +{ + sendCommand(0x02); // Power off. + wait(1500); +} + +void UC8175::writeImage(uint8_t command, const uint8_t *image) +{ + sendCommand(command); + sendData(image, bufferSize); +} + +void UC8175::writeOldImage() +{ + if (updateType == FAST && previousBuffer) + writeImage(0x10, previousBuffer); + else + writeImage(0x10, buffer); +} + +void UC8175::writeNewImage() +{ + writeImage(0x13, buffer); +} + +void UC8175::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 400); + case FULL: + default: + return beginPolling(100, 2000); + } +} + +bool UC8175::isUpdateDone() +{ + return digitalRead(pin_busy) != BUSY_ACTIVE; +} + +void UC8175::finalizeUpdate() +{ + powerOff(); + + if (pin_rst != (uint8_t)-1) { + sendCommand(0x07); // Deep sleep. + sendData(0xA5); + } +} + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/Drivers/EInk/UC8175.h b/src/graphics/niche/Drivers/EInk/UC8175.h new file mode 100644 index 000000000..b248d4bea --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/UC8175.h @@ -0,0 +1,62 @@ +// E-Ink base class for displays based on UC8175 / UC8176 style controller ICs. + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./EInk.h" + +namespace NicheGraphics::Drivers +{ + +class UC8175 : public EInk +{ + public: + UC8175(uint16_t width, uint16_t height, UpdateTypes supported); + void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) override; + void update(uint8_t *imageData, UpdateTypes type) override; + + protected: + virtual void wait(uint32_t timeoutMs = 1000); + virtual void reset(); + virtual void sendCommand(uint8_t command); + virtual void sendData(uint8_t data); + virtual void sendData(const uint8_t *data, uint32_t size); + + virtual void configCommon() = 0; // Always run + virtual void configFull() = 0; // Run when updateType == FULL + virtual void configFast() = 0; // Run when updateType == FAST + + virtual void powerOn(); + virtual void powerOff(); + virtual void writeOldImage(); + virtual void writeNewImage(); + virtual void writeImage(uint8_t command, const uint8_t *image); + + virtual void detachFromUpdate(); + virtual bool isUpdateDone() override; + virtual void finalizeUpdate() override; + + protected: + static constexpr uint8_t BUSY_ACTIVE = LOW; + + uint16_t bufferRowSize = 0; + uint32_t bufferSize = 0; + uint8_t *buffer = nullptr; + uint8_t *previousBuffer = nullptr; + bool hasPreviousBuffer = false; + UpdateTypes updateType = UpdateTypes::UNSPECIFIED; + + uint8_t pin_dc = (uint8_t)-1; + uint8_t pin_cs = (uint8_t)-1; + uint8_t pin_busy = (uint8_t)-1; + uint8_t pin_rst = (uint8_t)-1; + SPIClass *spi = nullptr; + SPISettings spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0); +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 84fd86465..3c14c2607 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -3,7 +3,7 @@ /* Base class for InkHUD applets - Must be overriden + Must be overridden An applet is one "program" which may show info on the display. @@ -15,6 +15,7 @@ #include // GFXRoot drawing lib +#include "mesh/MeshModule.h" #include "mesh/MeshTypes.h" #include "./AppletFont.h" @@ -208,4 +209,4 @@ class Applet : public GFX }; // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp index 4cf83966b..06ddd5bb0 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.cpp @@ -525,7 +525,7 @@ void InkHUD::MapApplet::calculateAllMarkers() } // Determine the conversion factor between metres, and pixels on screen -// May be overriden by derived applet, if custom scale required (fixed map size?) +// May be overridden by derived applet, if custom scale required (fixed map size?) void InkHUD::MapApplet::calculateMapScale() { // Aspect ratio of map and screen @@ -555,4 +555,4 @@ void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) drawLine(x0, y1, x1, y0, BLACK); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index a063c08b5..607fd4ef7 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -120,9 +120,26 @@ void InkHUD::NodeListApplet::onRender(bool full) // Draw the main node list // ======================== - // Imaginary vertical line dividing left-side and right-side info - // Long-name will crop here - const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops"); + // Leave a small gutter between long-name text and right-side card content + constexpr uint8_t rightContentGap = 2; + + // Truncate with trailing "...", sized using the current font. + auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) { + constexpr const char *ellipsis = "..."; + const uint16_t ellipsisW = getTextWidth(ellipsis); + uint16_t textW = getTextWidth(text); + if (maxWidth == 0) + return std::string(); + if (textW <= maxWidth) + return text; + if (ellipsisW > maxWidth) + return std::string(); + while (!text.empty() && (textW + ellipsisW > maxWidth)) { + text.pop_back(); + textW = getTextWidth(text); + } + return text + ellipsis; + }; // Y value (top) of the current card. Increases as we draw. uint16_t cardTopY = headerDivY + padDivH; @@ -141,7 +158,7 @@ void InkHUD::NodeListApplet::onRender(bool full) SignalStrength &signal = card->signal; std::string longName; // handled below std::string shortName; // handled below - std::string distance; // handled below; + std::string distance; // handled below const uint8_t &hopsAway = card->hopsAway; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); @@ -185,41 +202,49 @@ void InkHUD::NodeListApplet::onRender(bool full) setFont(fontMedium); printAt(0, lineAY, shortName, LEFT, MIDDLE); - // Print the distance + // Right-side labels and long name are rendered in small font. setFont(fontSmall); - printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + uint16_t rightContentW = 0; - // If we have a direct connection to the node, draw the signal indicator + // Bottom row right: distance. + if (!distance.empty()) { + rightContentW = std::max(rightContentW, getTextWidth(distance)); + printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE); + } + + // Top row right: direct-link signal only. if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { - uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label + uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label uint16_t signalH = fontMedium.lineHeight() * 0.75; int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); int16_t signalX = width() - signalW; + rightContentW = std::max(rightContentW, signalW); drawSignalIndicator(signalX, signalY, signalW, signalH, signal); - } - // Otherwise, print "hops away" info, if available - else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) { - std::string hopString = to_string(node->hops_away); - hopString += " Hop"; - if (node->hops_away != 1) - hopString += "s"; // Append s for "Hops", rather than "Hop" - + } else if (hopsAway != CardInfo::HOPS_UNKNOWN) { + std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops"); + rightContentW = std::max(rightContentW, getTextWidth(hopString)); printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); } - // Print the long name, cropping to prevent overflow onto the right-side info - setCrop(0, 0, dividerX - 1, height()); - printAt(0, lineBY, longName, LEFT, MIDDLE); + // Give long names as much room as possible while still avoiding right side signal and hop space + const uint16_t longNameMaxW = + (rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0; + const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW); - // GFX effect: "hatch" the right edge of longName area - // If a longName has been cropped, it will appear to fade out, - // creating a soft barrier with the right-side info - const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight()); - const int16_t hatchWidth = fontSmall.lineHeight(); - hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE); + // Safety crop + setCrop(0, cardTopY, longNameMaxW, cardH); + printAt(0, lineBY, longNameShown, LEFT, MIDDLE); + + resetCrop(); + + // Draw separator between cards + const int16_t separatorY = cardTopY + cardH - 1; + if (separatorY < height() - 1 && (card + 1) != cards.end()) { + for (int16_t xSep = 0; xSep < width(); xSep += 2) + drawPixel(xSep, separatorY, BLACK); + } // Prepare to draw the next card - resetCrop(); cardTopY += cardH; // Once we've run out of screen, stop drawing cards @@ -288,4 +313,4 @@ void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t } } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h index 8babdba03..baee3f0f4 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h @@ -65,10 +65,10 @@ class NodeListApplet : public Applet, public MeshModule // Card Dimensions // - for rendering and for maxCards calc - uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards + uint8_t cardMarginH = 1; // Gap between cards (minimal to fit more rows) uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card }; } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index c0850b742..0b9607133 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -6,7 +6,7 @@ using namespace NicheGraphics; InkHUD::BatteryIconApplet::BatteryIconApplet() { - alwaysRender = true; // render everytime the screen is updated + alwaysRender = true; // render every time the screen is updated // Show at boot, if user has previously enabled the feature if (settings->optionalFeatures.batteryIcon) @@ -91,4 +91,4 @@ void InkHUD::BatteryIconApplet::onRender(bool full) drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h index 306a8d8e3..0ae181a2c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Keyboard/KeyboardApplet.h @@ -2,7 +2,7 @@ /* -System Applet to render an on-screeen keyboard +System Applet to render an on-screen keyboard */ diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index b2c58fc60..1f3109413 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -45,7 +45,7 @@ void InkHUD::LogoApplet::onRender(bool full) int16_t logoCY = Y(0.5 - 0.05); // Invert colors if black-on-white - // Used during shutdown, to resport display health + // Used during shutdown, to report display health // Todo: handle this in InkHUD::Renderer instead if (inverted) { fillScreen(BLACK); @@ -186,4 +186,4 @@ int32_t InkHUD::LogoApplet::runOnce() return OSThread::disable(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 520a6de97..d489d21ee 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -177,24 +177,8 @@ static void applyLoRaRegion(meshtastic_Config_LoRaConfig_RegionCode region) auto changes = SEGMENT_CONFIG; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); } #endif @@ -349,13 +333,13 @@ void InkHUD::MenuApplet::execute(MenuItem item) handleFreeText = true; cm.freeTextItem.rawText.erase(); // clear the previous freetext message freeTextMode = true; // render input field instead of normal menu - // Open the on-screen keyboard if the joystick is enabled - if (settings->joystick.enabled) + // Open the on-screen keyboard only for full joystick devices + if (settings->joystick.enabled && !inkhud->twoWayRocker) inkhud->openKeyboard(); break; case STORE_CANNEDMESSAGE_SELECTION: - if (!settings->joystick.enabled) + if (!settings->joystick.enabled || inkhud->twoWayRocker) cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry else cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry @@ -922,7 +906,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) if (settings->userTiles.maxCount > 1) items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); - if (settings->joystick.enabled) + if (settings->joystick.enabled && !inkhud->twoWayRocker) items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); @@ -1751,7 +1735,7 @@ void InkHUD::MenuApplet::populateSendPage() items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); // If joystick is available, include the Free Text option - if (settings->joystick.enabled) + if (settings->joystick.enabled && !inkhud->twoWayRocker) items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); // One menu item for each canned message @@ -2028,7 +2012,7 @@ void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone } -// Free up any heap mmemory we'd used while selecting / sending canned messages +// Free up any heap memory we'd used while selecting / sending canned messages void InkHUD::MenuApplet::freeCannedMessageResources() { cm.selectedMessageItem = nullptr; diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index 6cac2644b..a45e8d9b5 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -152,6 +152,11 @@ void InkHUD::TipsApplet::onRender(bool full) drawBullet("User Button"); drawBullet("- short press: next"); drawBullet("- long press: select or open menu"); + } else if (inkhud->twoWayRocker) { + drawBullet("Rocker + Button"); + drawBullet("- center press: open menu or select"); + drawBullet("- left/right: applet nav"); + drawBullet("- in menu: up/down"); } else { drawBullet("Joystick"); drawBullet("- press: open menu or select"); diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 045e2a6fc..2cd2c4163 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -7,7 +7,7 @@ Displays a thread-view of incoming and outgoing message for a specific channel The channel for this applet is set in the constructor, when the applet is added to WindowManager in the setupNicheGraphics method. -Several messages are saved to flash at shutdown, to preseve applet between reboots. +Several messages are saved to flash at shutdown, to preserve applet between reboots. This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer. If the amount of flash usage is unacceptable, we could keep these in RAM only. @@ -55,4 +55,4 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 0e25b0900..abd53951a 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -88,6 +88,9 @@ class InkHUD // Used by TipsApplet to force menu to start on Region selection bool forceRegionMenu = false; + // Input mode hint for devices that use a left/right rocker plus center button + bool twoWayRocker = false; + // Updating the display // - called by various InkHUD components @@ -130,4 +133,4 @@ class InkHUD } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Renderer.h b/src/graphics/niche/InkHUD/Renderer.h index 5cfb79277..1ab94b70b 100644 --- a/src/graphics/niche/InkHUD/Renderer.h +++ b/src/graphics/niche/InkHUD/Renderer.h @@ -53,7 +53,7 @@ class Renderer : protected concurrency::OSThread uint16_t height(); private: - // Make attemps to render / update, once triggered by requestUpdate or forceUpdate + // Make attempts to render / update, once triggered by requestUpdate or forceUpdate int32_t runOnce() override; // Apply the display rotation to handled pixels @@ -95,4 +95,4 @@ class Renderer : protected concurrency::OSThread } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index fce3c9770..c4a0813d8 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -143,7 +143,7 @@ void InkHUD::WindowManager::openMenu() // Bring the AlignStick applet to the foreground void InkHUD::WindowManager::openAlignStick() { - if (settings->joystick.enabled) { + if (settings->joystick.enabled && !inkhud->twoWayRocker) { AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); alignStick->bringToForeground(); } @@ -151,6 +151,9 @@ void InkHUD::WindowManager::openAlignStick() void InkHUD::WindowManager::openKeyboard() { + if (!settings->joystick.enabled || inkhud->twoWayRocker) + return; + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); if (keyboard) { @@ -162,6 +165,9 @@ void InkHUD::WindowManager::openKeyboard() void InkHUD::WindowManager::closeKeyboard() { + if (!settings->joystick.enabled || inkhud->twoWayRocker) + return; + KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); if (keyboard) { @@ -477,7 +483,7 @@ void InkHUD::WindowManager::createSystemApplets() addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile); - if (settings->joystick.enabled) { + if (settings->joystick.enabled && !inkhud->twoWayRocker) { addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("Keyboard", new KeyboardApplet, new Tile); } @@ -503,7 +509,7 @@ void InkHUD::WindowManager::placeSystemTiles() inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); - if (settings->joystick.enabled) { + if (settings->joystick.enabled && !inkhud->twoWayRocker) { inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); inkhud->getSystemApplet("Keyboard") @@ -649,7 +655,7 @@ void InkHUD::WindowManager::refocusTile() } } -// Seach for any applets which believe they are foreground, but no longer have a valid tile +// Search for any applets which believe they are foreground, but no longer have a valid tile // Tidies up after layout changes at runtime void InkHUD::WindowManager::findOrphanApplets() { @@ -679,4 +685,4 @@ void InkHUD::WindowManager::findOrphanApplets() } } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index 8c30aba58..7cd468d73 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -733,7 +733,7 @@ To add support for additional encodings, add to the `AppletFont::Encodings` enum #### Custom Line Height -Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritcs. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. +Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritics. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. ```cpp // -2 px of padding above, +1 px of padding below diff --git a/src/graphics/niche/Inputs/TwoButton.cpp b/src/graphics/niche/Inputs/TwoButton.cpp index bd29f981d..1a27e039b 100644 --- a/src/graphics/niche/Inputs/TwoButton.cpp +++ b/src/graphics/niche/Inputs/TwoButton.cpp @@ -59,7 +59,7 @@ void TwoButton::stop() } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings -// This helper method isn't used by the TweButton class itself, it could be moved elsewhere. +// This helper method isn't used by the TwoButton class itself, it could be moved elsewhere. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. uint8_t TwoButton::getUserButtonPin() { @@ -308,4 +308,4 @@ int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) #endif -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index 287fb943f..f979faca9 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -156,6 +156,24 @@ void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lP pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); } +// Configures only left/right joystick directions for a two-way rocker +void TwoButtonExtended::setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup) +{ + if (leftPin == rightPin) { + LOG_WARN("Attempted reuse of TwoWayRocker GPIO. Ignoring assignment"); + return; + } + + joystick[Direction::UP].pin = 0xFF; + joystick[Direction::DOWN].pin = 0xFF; + joystick[Direction::LEFT].pin = leftPin; + joystick[Direction::RIGHT].pin = rightPin; + joystickActiveLogic = LOW; + + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) { assert(whichButton < 2); @@ -229,6 +247,13 @@ void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPres joystick[Direction::RIGHT].onPress = rPress; } +// Set press handlers for a two-way rocker mapped to left/right directions +void TwoButtonExtended::setTwoWayRockerPressHandlers(Callback lPress, Callback rPress) +{ + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; +} + // Handle the start of a press to the primary button // Wakes our button thread void TwoButtonExtended::isrPrimary() diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index 23fd78a2a..eb536907d 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -45,6 +45,7 @@ class TwoButtonExtended : protected concurrency::OSThread void stop(); // Stop handling button input (disconnect ISRs for sleep) void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup = false); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setJoystickDebounce(uint32_t debounceMs); void setHandlerDown(uint8_t whichButton, Callback onDown); @@ -54,6 +55,7 @@ class TwoButtonExtended : protected concurrency::OSThread void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress); // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index 5654fa02a..708cd8967 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -16,6 +16,10 @@ DeviceScreen *deviceScreen = nullptr; +#ifndef TFT_TASK_STACK_SIZE +#define TFT_TASK_STACK_SIZE 16384 +#endif + #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver tftSleepObserver = @@ -127,7 +131,7 @@ void tftSetup(void) #ifdef ARCH_ESP32 tftSleepObserver.observe(¬ifyLightSleep); endSleepObserver.observe(¬ifyLightSleepEnd); - xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0); + xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); #endif diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 3e4aa4bcd..df8de4905 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -271,8 +271,8 @@ int32_t ButtonThread::runOnce() break; } // end multipress event - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediately. case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); @@ -347,4 +347,4 @@ int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) void ButtonThread::storeClickCount() { multipressClickCount = userButton.getNumberClicks(); -} \ No newline at end of file +} diff --git a/src/input/CardputerKeyboard.cpp b/src/input/CardputerKeyboard.cpp new file mode 100644 index 000000000..ec1ed383a --- /dev/null +++ b/src/input/CardputerKeyboard.cpp @@ -0,0 +1,200 @@ +#if defined(M5STACK_CARDPUTER_ADV) + +#include "CardputerKeyboard.h" +#include "main.h" + +#define _TCA8418_COLS 8 +#define _TCA8418_ROWS 7 +#define _TCA8418_NUM_KEYS 56 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierFnKey = 2; +constexpr uint8_t modifierFn = 0b0010; +constexpr uint8_t modifierCtrlKey = 3; +constexpr uint8_t modifierShiftKey = 6; +constexpr uint8_t modifierShift = 0b0001; +constexpr uint8_t modifierOptKey = 7; +constexpr uint8_t modifierAltKey = 11; + +// Num chars per key, Modulus for rotating through characters +static uint8_t CardputerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char CardputerTapMap[_TCA8418_NUM_KEYS][3] = {{'`', '~', Key::ESC}, + {Key::TAB, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {'1', '!', 0x00}, + {'q', 'Q', Key::REBOOT}, + {0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00}, + {'2', '@', 0x00}, + {'w', 'W', 0x00}, + {'a', 'A', 0x00}, + {0x00, 0x00, 0x00}, + {'3', '#', 0x00}, + {'e', 'E', 0x00}, + {'s', 'S', 0x00}, + {'z', 'Z', 0x00}, + {'4', '$', 0x00}, + {'r', 'R', 0x00}, + {'d', 'D', 0x00}, + {'x', 'X', 0x00}, + {'5', '%', 0x00}, + {'t', 'T', 0x00}, + {'f', 'F', 0x00}, + {'c', 'C', 0x00}, + {'6', '^', 0x00}, + {'y', 'Y', 0x00}, + {'g', 'G', Key::GPS_TOGGLE}, + {'v', 'V', 0x00}, + {'7', '&', 0x00}, + {'u', 'U', 0x00}, + {'h', 'H', 0x00}, + {'b', 'B', Key::BT_TOGGLE}, + {'8', '*', 0x00}, + {'i', 'I', 0x00}, + {'j', 'J', 0x00}, + {'n', 'N', 0x00}, + {'9', '(', 0x00}, + {'o', 'O', 0x00}, + {'k', 'K', 0x00}, + {'m', 'M', Key::MUTE_TOGGLE}, + {'0', ')', 0x00}, + {'p', 'P', Key::SEND_PING}, + {'l', 'L', 0x00}, + {',', '<', Key::LEFT}, + {'_', '-', 0x00}, + {'[', '{', 0x00}, + {';', ':', Key::UP}, + {'.', '>', Key::DOWN}, + {'=', '+', 0x00}, + {']', '}', 0x00}, + {'\'', '"', 0x00}, + {'/', '?', Key::RIGHT}, + {Key::BSP, 0x00, 0x00}, + {'\\', '|', 0x00}, + {Key::SELECT, 0x00, 0x00}, + {' ', ' ', ' '}}; + +CardputerKeyboard::CardputerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ + reset(); +} + +void CardputerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); +} + +// handle multi-key presses (shift and alt) +void CardputerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void CardputerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void CardputerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (inputBroker->menuMode && modifierFlag == 0) { + if (last_key == 0 || last_key == 43 || last_key == 46 || last_key == 47 || + last_key == 51) { // esc, left, up, down, right key + modifierFlag = modifierFn; + } + } + + queueEvent(CardputerTapMap[last_key][modifierFlag % CardputerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void CardputerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierShiftKey) { + modifierFlag ^= modifierShift; + } else if (key == modifierFnKey) { + modifierFlag ^= modifierFn; + } +} + +bool CardputerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierShiftKey || key == modifierFnKey); +} + +#endif \ No newline at end of file diff --git a/src/input/CardputerKeyboard.h b/src/input/CardputerKeyboard.h new file mode 100644 index 000000000..c9de1f36b --- /dev/null +++ b/src/input/CardputerKeyboard.h @@ -0,0 +1,26 @@ +#include "TCA8418KeyboardBase.h" + +class CardputerKeyboard : public TCA8418KeyboardBase +{ + public: + CardputerKeyboard(); + void reset(void); + void trigger(void) override; + virtual ~CardputerKeyboard() {} + + protected: + void pressed(uint8_t key) override; + void released(void) override; + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + + private: + uint8_t modifierFlag; + uint32_t last_modifier_time; + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; +}; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index acf79f149..b7c9b27a9 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -34,7 +34,7 @@ #if defined(BUTTON_PIN_TOUCH) ButtonThread *TouchButtonThread = nullptr; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) +#if defined(PIN_EINK_EN) static bool touchBacklightWasOn = false; static bool touchBacklightActive = false; #endif @@ -220,8 +220,8 @@ void InputBroker::Init() }; touchConfig.singlePress = INPUT_BROKER_NONE; touchConfig.longPress = INPUT_BROKER_BACK; -#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN) - // On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds +#if defined(PIN_EINK_EN) + // Touch pad drives the backlight on devices with e-ink backlight pin touchConfig.longPress = INPUT_BROKER_NONE; touchConfig.suppressLeadUpSound = true; touchConfig.onPress = []() { @@ -299,6 +299,7 @@ void InputBroker::Init() // Buttons. Moved here cause we need NodeDB to be initialized // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP UserButtonThread = new ButtonThread("UserButton"); +#if !MESHTASTIC_EXCLUDE_SCREEN if (screen) { ButtonConfig userConfig; userConfig.pinNumber = (uint8_t)_pinNum; @@ -317,7 +318,9 @@ void InputBroker::Init() userConfig.longPressTime = 500; userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; UserButtonThread->initButton(userConfig); - } else { + } else +#endif + { ButtonConfig userConfigNoScreen; userConfigNoScreen.pinNumber = (uint8_t)_pinNum; userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; @@ -379,11 +382,13 @@ void InputBroker::Init() } #endif // HAS_BUTTON #if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { - seesawRotary = new SeesawRotary("SeesawRotary"); - if (!seesawRotary->init()) { - delete seesawRotary; - seesawRotary = nullptr; + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (portduino_config.i2cdev != "") { + seesawRotary = new SeesawRotary("SeesawRotary"); + if (!seesawRotary->init()) { + delete seesawRotary; + seesawRotary = nullptr; + } } aLinuxInputImpl = new LinuxInputImpl(); aLinuxInputImpl->init(); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 9fcdd845f..847604011 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -70,6 +70,7 @@ class InputBroker : public Observable public: InputBroker(); + bool menuMode = true; void registerSource(Observable *source); void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) diff --git a/src/input/MPR121Keyboard.cpp b/src/input/MPR121Keyboard.cpp index ec37cfbaa..80a272d3b 100644 --- a/src/input/MPR121Keyboard.cpp +++ b/src/input/MPR121Keyboard.cpp @@ -177,7 +177,7 @@ void MPR121Keyboard::reset() delay(20); writeRegister(_MPR121_REG_CONFIG2, 0x21); delay(20); - // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels + // Enter run mode by setting partial filter calibration tracking, disable proximity detection, enable 12 channels writeRegister(_MPR121_REG_ELECTRODE_CONFIG, ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); delay(100); @@ -430,4 +430,4 @@ void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value) if (writeCallback) { writeCallback(m_addr, data[0], &(data[1]), 1); } -} \ No newline at end of file +} diff --git a/src/input/SeesawRotary.cpp b/src/input/SeesawRotary.cpp index 0a6e6e974..dc57b296b 100644 --- a/src/input/SeesawRotary.cpp +++ b/src/input/SeesawRotary.cpp @@ -59,7 +59,7 @@ int32_t SeesawRotary::runOnce() wasPressed = currentlyPressed; int32_t new_position = ss.getEncoderPosition(); - // did we move arounde? + // did we move around? if (encoder_position != new_position) { if (encoder_position == 0 && new_position != 1) { e.inputEvent = INPUT_BROKER_ALT_PRESS; @@ -80,4 +80,4 @@ int32_t SeesawRotary::runOnce() return 50; } -#endif \ No newline at end of file +#endif diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index 822885a9f..238b9bb51 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -88,7 +88,7 @@ void TCA8418Keyboard::pressed(uint8_t key) // Check if the key is the same as the last one or if the time interval has passed if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { char_idx = 0; // Reset char index if new key or long press - should_backspace = false; // dont backspace on new key + should_backspace = false; // don't backspace on new key } else { char_idx += 1; // Cycle through characters if same key pressed should_backspace = true; // allow backspace on same key diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index c2755980e..fceac74ba 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -43,7 +43,7 @@ int32_t TouchScreenBase::runOnce() // process touch events int16_t x, y; bool touched = getTouch(x, y); - if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen touched = false; if (touched) { this->setInterval(20); @@ -123,7 +123,7 @@ int32_t TouchScreenBase::runOnce() } } #else - // fire TAP event when no 2nd tap occured within time + // fire TAP event when no 2nd tap occurred within time if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 906dcd2a8..4f62fd5fa 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -8,6 +8,14 @@ UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {} bool UpDownInterruptImpl1::init() { +#if defined(INPUTDRIVER_TWO_WAY_ROCKER) && defined(INPUTDRIVER_TWO_WAY_ROCKER_LEFT) && defined(INPUTDRIVER_TWO_WAY_ROCKER_RIGHT) + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = INPUTDRIVER_TWO_WAY_ROCKER_LEFT; + moduleConfig.canned_message.inputbroker_pin_b = INPUTDRIVER_TWO_WAY_ROCKER_RIGHT; +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) + moduleConfig.canned_message.inputbroker_pin_press = INPUTDRIVER_TWO_WAY_ROCKER_BTN; +#endif +#endif if (!moduleConfig.canned_message.updown1_enabled) { // Input device is disabled. @@ -46,4 +54,4 @@ void UpDownInterruptImpl1::handleIntUp() void UpDownInterruptImpl1::handleIntPressed() { upDownInterruptImpl1->intPressHandler(); -} \ No newline at end of file +} diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index d744ee2ca..510fb1e31 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -7,6 +7,8 @@ #include "TDeckProKeyboard.h" #elif defined(T_LORA_PAGER) #include "TLoraPagerKeyboard.h" +#elif defined(M5STACK_CARDPUTER_ADV) +#include "CardputerKeyboard.h" #elif defined(HACKADAY_COMMUNICATOR) #include "HackadayCommunicatorKeyboard.h" #else @@ -22,6 +24,8 @@ KbI2cBase::KbI2cBase(const char *name) TCAKeyboard(*(new TDeckProKeyboard())) #elif defined(T_LORA_PAGER) TCAKeyboard(*(new TLoraPagerKeyboard())) +#elif defined(M5STACK_CARDPUTER_ADV) + TCAKeyboard(*(new CardputerKeyboard())) #elif defined(HACKADAY_COMMUNICATOR) TCAKeyboard(*(new HackadayCommunicatorKeyboard())) #else @@ -487,7 +491,7 @@ int32_t KbI2cBase::runOnce() e.kbchar = 0; break; case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) - // toggle moddifiers button. + // toggle modifiers button. is_sym = !is_sym; e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the diff --git a/src/main.cpp b/src/main.cpp index 4c686937b..6f78c0b96 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,7 +123,7 @@ void printPartitionTable() #include "AmbientLightingThread.h" #include "PowerFSMThread.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" AccelerometerThread *accelerometerThread = nullptr; #endif @@ -389,8 +389,8 @@ void setup() #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR - // use PSRAM for malloc calls > 256 bytes - heap_caps_malloc_extmem_enable(256); + // use PSRAM for malloc calls > 2048 bytes + heap_caps_malloc_extmem_enable(2048); #endif #endif @@ -657,7 +657,7 @@ void setup() } #endif -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER auto acc_info = i2cScanner->firstAccelerometer(); accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found; LOG_DEBUG("acc_info = %i", acc_info.type); @@ -737,7 +737,7 @@ void setup() #endif #if !MESHTASTIC_EXCLUDE_I2C -#if !defined(ARCH_STM32WL) +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER if (acc_info.type != ScanI2C::DeviceType::NONE) { accelerometerThread = new AccelerometerThread(acc_info.type); } diff --git a/src/main.h b/src/main.h index 91e27951f..56f048134 100644 --- a/src/main.h +++ b/src/main.h @@ -65,7 +65,7 @@ extern UdpMulticastHandler *udpHandler; // Global Screen singleton. extern graphics::Screen *screen; -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" extern AccelerometerThread *accelerometerThread; #endif diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 4dcd94e3b..1583567fe 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -22,6 +22,8 @@ const char *Channels::serialChannel = "serial"; const char *Channels::mqttChannel = "mqtt"; #endif +meshtastic_Channel dummyChannel = {.index = -1}; + uint8_t xorHash(const uint8_t *p, size_t len) { uint8_t code = 0; @@ -309,13 +311,7 @@ meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex) return *ch; } else { LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count); - - static meshtastic_Channel *ch = (meshtastic_Channel *)malloc(sizeof(meshtastic_Channel)); - memset(ch, 0, sizeof(meshtastic_Channel)); - // ch.index -1 means we don't know the channel locally and need to look it up by settings.name - // not sure this is handled right everywhere - ch->index = -1; - return *ch; + return dummyChannel; } } diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 72216a63c..daa7a3a75 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -4,6 +4,7 @@ #include #if !(MESHTASTIC_EXCLUDE_PKI) +#include "HardwareRNG.h" #include "NodeDB.h" #include "aes-ccm.h" #include "meshUtils.h" @@ -26,6 +27,15 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) { // Mix in any randomness we can, to make key generation stronger. CryptRNG.begin(optstr(APP_VERSION)); + + uint8_t hardwareEntropy[64] = {0}; + if (HardwareRNG::fill(hardwareEntropy, sizeof(hardwareEntropy), true)) { + CryptRNG.stir(hardwareEntropy, sizeof(hardwareEntropy)); + } else { + LOG_WARN("Hardware entropy unavailable, falling back to software RNG"); + } + memset(hardwareEntropy, 0, sizeof(hardwareEntropy)); + if (myNodeInfo.device_id.size == 16) { CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); } @@ -61,6 +71,33 @@ bool CryptoEngine::regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey) } return true; } + +bool CryptoEngine::ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user) +{ + if (user.is_licensed) { + return false; + } + + bool keygenSuccess = false; + if (security.private_key.size == 32) { + if (regeneratePublicKey(security.public_key.bytes, security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + generateKeyPair(security.public_key.bytes, security.private_key.bytes); + keygenSuccess = true; + } + + if (keygenSuccess) { + security.public_key.size = 32; + security.private_key.size = 32; + user.public_key.size = 32; + memcpy(user.public_key.bytes, security.public_key.bytes, 32); + } + + return keygenSuccess; +} #endif /** diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 19d572355..f40400331 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -36,6 +36,7 @@ class CryptoEngine #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) virtual void generateKeyPair(uint8_t *pubKey, uint8_t *privKey); virtual bool regeneratePublicKey(uint8_t *pubKey, uint8_t *privKey); + virtual bool ensurePkiKeys(meshtastic_Config_SecurityConfig &security, meshtastic_User &user); #endif void setDHPrivateKey(uint8_t *_private_key); diff --git a/src/mesh/Default.cpp b/src/mesh/Default.cpp index 1bd0340f8..3ecd766f1 100644 --- a/src/mesh/Default.cpp +++ b/src/mesh/Default.cpp @@ -38,11 +38,13 @@ uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultVa uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) { // If we are a router, we don't scale the value. It's already significantly higher. - if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) return getConfiguredOrDefaultMs(configured, defaultValue); // Additionally if we're a tracker or sensor, we want priority to send position and telemetry - if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER)) + if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER, + meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) return getConfiguredOrDefaultMs(configured, defaultValue); return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); diff --git a/src/mesh/Default.h b/src/mesh/Default.h index e206d8277..59425042e 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -1,5 +1,8 @@ #pragma once +#include #include +#include +#include #include #include #define ONE_DAY 24 * 60 * 60 @@ -10,12 +13,12 @@ #define TEN_SECONDS_MS 10 * 1000 #define MAX_INTERVAL INT32_MAX // FIXME: INT32_MAX to avoid overflow issues with Apple clients but should be UINT32_MAX -#define min_default_telemetry_interval_secs 30 * 60 +#define min_default_telemetry_interval_secs IF_ROUTER(ONE_DAY / 2, 30 * 60) #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) #define default_telemetry_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define default_broadcast_smart_minimum_interval_secs 5 * 60 -#define min_default_broadcast_interval_secs 60 * 60 +#define min_default_broadcast_interval_secs IF_ROUTER(ONE_DAY / 2, 60 * 60) #define min_default_broadcast_smart_minimum_interval_secs 5 * 60 #define default_wait_bluetooth_secs IF_ROUTER(1, 60) #define default_sds_secs IF_ROUTER(ONE_DAY, UINT32_MAX) // Default to forever super deep sleep @@ -27,6 +30,11 @@ #define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour #define min_neighbor_info_broadcast_secs 4 * 60 * 60 #define default_map_publish_interval_secs 60 * 60 + +// Traffic management defaults +#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells +#define default_traffic_mgmt_position_min_interval_secs (ONE_DAY / 2) // 12 hours between identical positions + #ifdef USERPREFS_RINGTONE_NAG_SECS #define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS #else @@ -42,7 +50,10 @@ #define default_mqtt_tls_enabled false #define IF_ROUTER(routerVal, normalVal) \ - ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal)) + ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || \ + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) \ + ? (routerVal) \ + : (normalVal)) class Default { @@ -63,25 +74,39 @@ class Default if (numOnlineNodes <= 40) { return 1.0; } else { - float throttlingFactor = 0.075; - if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW) - throttlingFactor = 0.04; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) - throttlingFactor = 0.02; - else if (config.lora.use_preset && - IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) - throttlingFactor = 0.01; + // Resolve SF and BW from preset or manual config + // When use_preset is true, config.lora.spread_factor and bandwidth may be 0 + // because applyModemConfig() sets them on RadioInterface, not on config.lora + float bwKHz; + uint8_t sf; + uint8_t cr; + if (config.lora.use_preset) { + modemPresetToParams(config.lora.modem_preset, false, bwKHz, sf, cr); + } else { + sf = config.lora.spread_factor; + bwKHz = bwCodeToKHz(config.lora.bandwidth); + } + + // Guard against invalid values + sf = clampSpreadFactor(sf); + bwKHz = clampBandwidthKHz(bwKHz); + + // throttlingFactor = 2^SF / (BW_in_kHz * scaling_divisor) + // With scaling_divisor=100: + // In SF11 and BW=250khz (longfast), this gives 0.08192 rather than the original 0.075 + // In SF10 and BW=250khz (mediumslow), this gives 0.04096 rather than the original 0.04 + // In SF9 and BW=250khz (mediumfast), this gives 0.02048 rather than the original 0.02 + // In SF7 and BW=250khz (shortfast), this gives 0.00512 rather than the original 0.01 + float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); #if USERPREFS_EVENT_MODE - // If we are in event mode, scale down the throttling factor - throttlingFactor = 0.04; + // If we are in event mode, scale down the throttling factor by 4 + throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); #endif // Scaling up traffic based on number of nodes over 40 int nodesOverForty = (numOnlineNodes - 40); - return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) + return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor } } -}; +}; \ No newline at end of file diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 78602a9ec..13f98299f 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -91,10 +91,27 @@ void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p) { if (nodeDB) nodeDB->updateFrom(*p); + #if !MESHTASTIC_EXCLUDE_TRACEROUTE + if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + // If we got a packet that is not decoded, try to decode it so we can check for traceroute. + auto decodedState = perhapsDecode(const_cast(p)); + if (decodedState == DecodeState::DECODE_SUCCESS) { + // parsing was successful, print for debugging + printPacket("reprocessPacket(DUP)", p); + } else { + // Fatal decoding error, we can't do anything with this packet + LOG_WARN( + "FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute", + static_cast(decodedState), p->id, getFrom(p)); + return; + } + } + if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && - p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) + p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) { traceRouteModule->processUpgradedPacket(*p); + } #endif } diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp new file mode 100644 index 000000000..b455128ac --- /dev/null +++ b/src/mesh/HardwareRNG.cpp @@ -0,0 +1,159 @@ +#include "HardwareRNG.h" + +#include +#include +#include + +#include "configuration.h" + +#if HAS_RADIO +#include "RadioLibInterface.h" +#endif + +#if defined(ARCH_NRF52) +#include +extern Adafruit_nRFCrypto nRFCrypto; +#elif defined(ARCH_ESP32) +#include +#elif defined(ARCH_RP2040) +#include +#elif defined(ARCH_PORTDUINO) +#include +#include +#include +#endif + +namespace HardwareRNG +{ + +namespace +{ +void fillWithRandomDevice(uint8_t *buffer, size_t length) +{ + std::random_device rd; + size_t offset = 0; + while (offset < length) { + uint32_t value = rd(); + size_t toCopy = std::min(length - offset, sizeof(value)); + memcpy(buffer + offset, &value, toCopy); + offset += toCopy; + } +} + +#if HAS_RADIO +bool mixWithLoRaEntropy(uint8_t *buffer, size_t length) +{ + // Only attempt to pull entropy from the modem if it is initialized and exposes the helper. + // When the radio stack is disabled or has not yet been configured, we simply skip this step + // and return false so callers know no extra mixing occurred. + RadioLibInterface *radio = RadioLibInterface::instance; + if (!radio) { + LOG_ERROR("No radio instance available to provide entropy"); + return false; + } + + constexpr size_t chunkSize = 16; + uint8_t scratch[chunkSize]; + size_t offset = 0; + bool mixed = false; + + while (offset < length) { + size_t toCopy = std::min(length - offset, chunkSize); + + // randomBytes() returns false if the modem does not support it or is not ready + // (for instance, when the radio is powered down). We break immediately to avoid + // blocking or returning partially-filled entropy and simply report failure. + if (!radio->randomBytes(scratch, toCopy)) { + break; + } + + for (size_t i = 0; i < toCopy; ++i) { + buffer[offset + i] ^= scratch[i]; + } + + mixed = true; + offset += toCopy; + } + + // Avoid leaving the modem-sourced bytes sitting on the stack longer than needed. + if (mixed) { + memset(scratch, 0, sizeof(scratch)); + } + + return mixed; +} +#endif +} // namespace + +bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy) +{ + if (!buffer || length == 0) { + return false; + } + + bool filled = false; + +#if defined(ARCH_NRF52) + // The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware. + nRFCrypto.begin(); + auto result = nRFCrypto.Random.generate(buffer, length); + nRFCrypto.end(); + filled = result; +#elif defined(ARCH_ESP32) + // ESP32 exposes a true RNG via esp_fill_random(). + esp_fill_random(buffer, length); + filled = true; +#elif defined(ARCH_RP2040) + // RP2040 has a hardware random number generator accessible through the Arduino core. + size_t offset = 0; + while (offset < length) { + uint32_t value = rp2040.hwrand32(); + size_t toCopy = std::min(length - offset, sizeof(value)); + memcpy(buffer + offset, &value, toCopy); + offset += toCopy; + } + filled = true; +#elif defined(ARCH_PORTDUINO) + // Prefer the host OS RNG first when running under Portduino. + ssize_t generated = ::getrandom(buffer, length, 0); + if (generated == static_cast(length)) { + filled = true; + } + + if (!filled) { + fillWithRandomDevice(buffer, length); + filled = true; + } +#endif + + if (!filled) { + // As a last resort, fall back to std::random_device. This should only be reached + // if a platform-specific source was unavailable. + fillWithRandomDevice(buffer, length); + filled = true; + } + +#if HAS_RADIO + if (useRadioEntropy) { + // Best-effort: if the radio is active and can provide modem entropy, XOR it over the + // buffer to improve overall quality. We consider the filling a success if either a + // good platform RNG or the modem RNG provided data, so we return true as long as at + // least one of those steps succeeded. + filled = mixWithLoRaEntropy(buffer, length) || filled; + } +#endif + + return filled; +} + +bool seed(uint32_t &seedOut) +{ + uint32_t candidate = 0; + if (!fill(reinterpret_cast(&candidate), sizeof(candidate), true)) { + return false; + } + seedOut = candidate; + return true; +} + +} // namespace HardwareRNG diff --git a/src/mesh/HardwareRNG.h b/src/mesh/HardwareRNG.h new file mode 100644 index 000000000..2dacb6c23 --- /dev/null +++ b/src/mesh/HardwareRNG.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace HardwareRNG +{ + +/** + * Fill the provided buffer with random bytes sourced from the most + * appropriate hardware-backed RNG available on the current platform. + * + * @param buffer Destination buffer for random bytes + * @param length Number of bytes to write + * @param useRadioEntropy If true, attempt to mix radio entropy into the output as well. + * @return true if the buffer was fully populated with entropy, false on failure + */ +bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy = false); + +/** + * Populate a 32-bit seed value with hardware-backed randomness where possible. + * + * @param seedOut Destination for the generated seed value + * @return true if a seed was produced from a reliable entropy source + */ +bool seed(uint32_t &seedOut); + +} // namespace HardwareRNG diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 4fec06da4..7a193f7f3 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -71,12 +71,10 @@ template bool LR11x0Interface::init() RadioLibInterface::init(); - limitPower(LR1110_MAX_POWER); - - if ((power > LR1120_MAX_POWER) && - (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // clamp again if wide freq range - power = LR1120_MAX_POWER; - preambleLength = 12; // 12 is the default for operation above 2GHz + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR1120_MAX_POWER); + } else { + limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range } #ifdef LR11X0_RF_SWITCH_SUBGHZ @@ -177,6 +175,12 @@ template bool LR11x0Interface::reconfigure() err = lora.setSyncWord(syncWord); assert(err == RADIOLIB_ERR_NONE); + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR1120_MAX_POWER); + } else { + limitPower(LR1110_MAX_POWER); // default clamp for non-wide freq range + } + err = lora.setPreambleLength(preambleLength); assert(err == RADIOLIB_ERR_NONE); @@ -184,14 +188,14 @@ template bool LR11x0Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > LR1110_MAX_POWER) // This chip has lower power limits than some - power = LR1110_MAX_POWER; - if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit - power = LR1120_MAX_POWER; - err = lora.setOutputPower(power); assert(err == RADIOLIB_ERR_NONE); + // Apply RX gain mode โ€” valid in STDBY, matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err); + startReceive(); // restart receiving return RADIOLIB_ERR_NONE; diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp new file mode 100644 index 000000000..b44c7539b --- /dev/null +++ b/src/mesh/LoRaFEMInterface.cpp @@ -0,0 +1,230 @@ +#if HAS_LORA_FEM +#include "LoRaFEMInterface.h" + +#if defined(ARCH_ESP32) +#include +#include +#endif + +LoRaFEMInterface loraFEMInterface; +void LoRaFEMInterface::init(void) +{ + setLnaCanControl(false); // Default is uncontrollable +#ifdef HELTEC_V4 + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + delay(1); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); + pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used + delay(1); + if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) { + // FEM is KCT8103L + fem_type = KCT8103L_PA; + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); + pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default + setLnaCanControl(true); + } else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) { + // FEM is GC1109 + fem_type = GC1109_PA; + // LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held. + // rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else { + fem_type = OTHER_FEM_TYPES; + } +#elif defined(USE_GC1109_PA) + fem_type = GC1109_PA; + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif + delay(1); + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + fem_type = KCT8103L_PA; + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); +#endif + delay(1); + pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default + setLnaCanControl(true); +#endif +} + +void LoRaFEMInterface::setSleepModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + /* + * Do not switch the power on and off frequently. + * After turning off LORA_GC1109_PA_EN, the power consumption has dropped to the uA level. + */ + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { + // shutdown the PA + digitalWrite(LORA_KCT8103L_PA_CSD, LOW); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + // shutdown the PA + digitalWrite(LORA_KCT8103L_PA_CSD, LOW); +#endif +} + +void LoRaFEMInterface::setTxModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); +#endif +} + +void LoRaFEMInterface::setRxModeEnable(void) +{ +#ifdef HELTEC_V4 + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#endif +} + +void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) +{ + +#ifdef HELTEC_V4 + // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. + // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), + // then latch with RTC hold so the state survives deep sleep. + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); + } else if (fem_type == KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); + } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_GC1109_PA_EN, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif +#elif defined(USE_KCT8103L_PA) + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if (lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); +#endif +#endif +} + +void LoRaFEMInterface::setLNAEnable(bool enabled) +{ + lna_enabled = enabled; +} + +int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) +{ +#ifdef HELTEC_V4 + const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; + const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7}; + const uint16_t *tx_gain; + uint16_t tx_gain_num; + if (fem_type == GC1109_PA) { + tx_gain = gc1109_tx_gain; + tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]); + } else if (fem_type == KCT8103L_PA) { + tx_gain = kct8103l_tx_gain; + tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]); + } else { + return loraOutputPower; + } +#else +#ifdef ARCH_PORTDUINO + const uint16_t *tx_gain = portduino_config.tx_gain_lora; + uint16_t tx_gain_num = portduino_config.num_pa_points; +#else + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; + uint16_t tx_gain_num = NUM_PA_POINTS; +#endif +#endif + for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > loraOutputPower) || + ((radio_dbm == (tx_gain_num - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= loraOutputPower))) { + // we've exceeded the power limit, or hit the max we can do + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", loraOutputPower, tx_gain[radio_dbm]); + loraOutputPower -= tx_gain[radio_dbm]; + break; + } + } + return loraOutputPower; +} + +#endif \ No newline at end of file diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h new file mode 100644 index 000000000..14220c6e3 --- /dev/null +++ b/src/mesh/LoRaFEMInterface.h @@ -0,0 +1,30 @@ +#pragma once +#if HAS_LORA_FEM +#include "configuration.h" +#include + +typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType; + +class LoRaFEMInterface +{ + public: + LoRaFEMInterface() : fem_type(OTHER_FEM_TYPES) {} + virtual ~LoRaFEMInterface() {} + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + int8_t powerConversion(int8_t loraOutputPower); + bool isLnaCanControl(void) { return lna_can_control; } + void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + + private: + LoRaFEMType fem_type; + bool lna_enabled = true; + bool lna_can_control = false; +}; +extern LoRaFEMInterface loraFEMInterface; + +#endif \ No newline at end of file diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index cbea85c62..4aad40c69 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -184,6 +184,29 @@ bool MeshPacketQueue::replaceLowerPriorityPacket(meshtastic_MeshPacket *p) } } + if (backPacket->tx_after) { + // Check if there's a late packet at the queue end + auto now = millis(); + if (backPacket->tx_after < now && (!p->tx_after || backPacket->tx_after > p->tx_after)) { + int32_t dt = (int32_t)(backPacket->tx_after - now); + if (p->tx_after) { + LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x with " + "TX delay %ums", + backPacket->id, dt, p->id, p->tx_after - now); + + } else { + LOG_WARN("Dropping late packet 0x%08x with TX delay %dms to make room in the TX queue for packet 0x%08x " + "with no TX delay", + backPacket->id, dt, p->id); + } + queue.pop_back(); + packetPool.release(backPacket); + // Insert the new packet in the correct order + enqueue(p); + return true; + } + } + // If the back packet's priority is not lower, no replacement occurs return false; } \ No newline at end of file diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index bbb0ee00f..3c3a4cf65 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -5,18 +5,52 @@ #include "PointerQueue.h" #include "configuration.h" +// Sentinel marking the end of a modem preset array +static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END = + static_cast(0xFF); + +// Region profile: bundles the preset list with regulatory parameters shared across regions +struct RegionProfile { + const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default + float spacing; // gaps between radio channels + float padding; // padding at each side of the "operating channel" + bool audioPermitted; + bool licensedOnly; // a region profile for licensed operators only + int8_t textThrottle; // throttle for text - future expansion + int8_t positionThrottle; // throttle for location data - future expansion + int8_t telemetryThrottle; // throttle for telemetry - future expansion + uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place +}; + +extern const RegionProfile PROFILE_STD; +extern const RegionProfile PROFILE_EU868; +extern const RegionProfile PROFILE_UNDEF; +// extern const RegionProfile PROFILE_LITE; +// extern const RegionProfile PROFILE_NARROW; +// extern const RegionProfile PROFILE_HAM; + // Map from old region names to new region enums struct RegionInfo { meshtastic_Config_LoRaConfig_RegionCode code; float freqStart; float freqEnd; - float dutyCycle; - float spacing; + float dutyCycle; // modified by getEffectiveDutyCycle uint8_t powerLimit; // Or zero for not set - bool audioPermitted; bool freqSwitching; bool wideLora; + const RegionProfile *profile; const char *name; // EU433 etc + + // Preset accessors (delegate through profile) + meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return profile->presets[0]; } + const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; } + size_t getNumPresets() const + { + size_t n = 0; + while (profile->presets[n] != MODEM_PRESET_END) + n++; + return n; + } }; extern const RegionInfo regions[]; @@ -24,6 +58,45 @@ extern const RegionInfo *myRegion; extern void initRegion(); +// Valid LoRa spread factor range and defaults +constexpr uint8_t LORA_SF_MIN = 7; +constexpr uint8_t LORA_SF_MAX = 12; +constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default + +// Valid LoRa coding rate range and default +constexpr uint8_t LORA_CR_MIN = 4; +constexpr uint8_t LORA_CR_MAX = 8; +constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default + +// Default bandwidth in kHz (LONG_FAST) +constexpr float LORA_BW_DEFAULT_KHZ = 250.0f; + +/// Clamp spread factor to the valid LoRa range [7, 12]. +/// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT. +static inline uint8_t clampSpreadFactor(uint8_t sf) +{ + if (sf < LORA_SF_MIN || sf > LORA_SF_MAX) + return LORA_SF_DEFAULT; + return sf; +} + +/// Clamp coding rate to the valid LoRa range [4, 8]. +/// Out-of-range values return LORA_CR_DEFAULT. +static inline uint8_t clampCodingRate(uint8_t cr) +{ + if (cr < LORA_CR_MIN || cr > LORA_CR_MAX) + return LORA_CR_DEFAULT; + return cr; +} + +/// Ensure bandwidth is positive. Non-positive values return LORA_BW_DEFAULT_KHZ. +static inline float clampBandwidthKHz(float bwKHz) +{ + if (bwKHz <= 0.0f) + return LORA_BW_DEFAULT_KHZ; + return bwKHz; +} + static inline float bwCodeToKHz(uint16_t bwCode) { if (bwCode == 31) diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index c1b3839bb..952a6d2be 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -87,7 +87,9 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio - bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + bool isPreferredRebroadcaster = + IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index 93ab94a7d..51678325b 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -4,6 +4,9 @@ #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif +#if HAS_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #include "NodeDB.h" NextHopRouter::NextHopRouter() {} @@ -23,7 +26,7 @@ ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) p->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum()); // First set the relayer to us wasSeenRecently(p); // FIXME, move this to a sniffSent method - p->next_hop = getNextHop(p->to, p->relay_node); // set the next hop + p->next_hop = getNextHop(p->to, p->relay_node).value_or(NO_NEXT_HOP_PREFERENCE); // set the next hop LOG_DEBUG("Setting next hop for packet with dest %x to %x", p->to, p->next_hop); // If it's from us, ReliableRouter already handles retransmissions if want_ack is set. If a next hop is set and hop limit is @@ -126,15 +129,28 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast /* Check if we should be rebroadcasting this packet if so, do so. */ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) { - if (!isToUs(p) && !isFromUs(p) && p->hop_limit > 0) { + // Check if traffic management wants to exhaust this packet's hops + bool exhaustHops = false; +#if HAS_TRAFFIC_MANAGEMENT + if (trafficManagementModule && trafficManagementModule->shouldExhaustHops(*p)) { + exhaustHops = true; + } +#endif + + // Allow rebroadcast if hop_limit > 0 OR if we're exhausting hops (which sets hop_limit = 0 but still needs one relay) + if (!isToUs(p) && !isFromUs(p) && (p->hop_limit > 0 || exhaustHops)) { if (p->id != 0) { if (isRebroadcaster()) { if (p->next_hop == NO_NEXT_HOP_PREFERENCE || p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum())) { meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it LOG_INFO("Rebroadcast received message coming from %x", p->relay_node); - // Use shared logic to determine if hop_limit should be decremented - if (shouldDecrementHopLimit(p)) { + // If exhausting hops, force hop_limit = 0 regardless of other logic + if (exhaustHops) { + tosend->hop_limit = 0; + LOG_INFO("Traffic management: exhausting hops for 0x%08x, setting hop_limit=0", getFrom(p)); + } else if (shouldDecrementHopLimit(p)) { + // Use shared logic to determine if hop_limit should be decremented tosend->hop_limit--; // bump down the hop count } else { LOG_INFO("favorite-ROUTER/CLIENT_BASE-to-ROUTER/CLIENT_BASE rebroadcast: preserving hop_limit"); @@ -182,10 +198,10 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p) * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ -uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) +std::optional NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) { if (isBroadcast(to)) - return NO_NEXT_HOP_PREFERENCE; + return std::nullopt; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(to); if (node && node->next_hop) { @@ -196,7 +212,7 @@ uint8_t NextHopRouter::getNextHop(NodeNum to, uint8_t relay_node) } else LOG_WARN("Next hop for 0x%x is 0x%x, same as relayer; set no pref", to, node->next_hop); } - return NO_NEXT_HOP_PREFERENCE; + return std::nullopt; } PendingPacket *NextHopRouter::findPendingPacket(GlobalPacketId key) diff --git a/src/mesh/NextHopRouter.h b/src/mesh/NextHopRouter.h index c1df3596b..42ef13cd9 100644 --- a/src/mesh/NextHopRouter.h +++ b/src/mesh/NextHopRouter.h @@ -1,6 +1,7 @@ #pragma once #include "FloodingRouter.h" +#include #include /** @@ -146,7 +147,7 @@ class NextHopRouter : public FloodingRouter * Get the next hop for a destination, given the relay node * @return the node number of the next hop, 0 if no preference (fallback to FloodingRouter) */ - uint8_t getNextHop(NodeNum to, uint8_t relay_node); + std::optional getNextHop(NodeNum to, uint8_t relay_node); /** Check if we should be rebroadcasting this packet if so, do so. * @return true if we did rebroadcast */ diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 360253041..4b0871566 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -322,9 +322,9 @@ NodeDB::NodeDB() // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; // If we are setup to broadcast on any default channel slot (with default frequency slot semantics), - // ensure that the telemetry intervals are coerced to the minimum value of 30 minutes or more. + // ensure that the telemetry intervals are coerced to the role-aware minimum value. if (channels.hasDefaultChannel()) { - LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); + LOG_DEBUG("Coerce telemetry to role-aware minimum on defaults"); moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue( @@ -347,7 +347,7 @@ NodeDB::NodeDB() } } if (positionUsesDefaultChannel) { - LOG_DEBUG("Coerce position broadcasts to min of 1 hour and smart broadcast min of 5 minutes on defaults"); + LOG_DEBUG("Coerce position broadcasts to role-aware minimum and smart broadcast min of 5 minutes on defaults"); config.position.position_broadcast_secs = Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs); config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue( @@ -568,6 +568,11 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; +#if HAS_LORA_FEM + config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED; +#else + config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; +#endif #if HAS_TFT // For the devices that support MUI, default to that config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; @@ -667,7 +672,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) #endif config.position.broadcast_smart_minimum_distance = 100; config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs; - if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER) + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) config.device.node_info_broadcast_secs = default_node_info_broadcast_secs; config.security.serial_enabled = true; config.security.admin_channel_enabled = false; @@ -813,7 +819,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_external_notification = true; -#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) +#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) moduleConfig.external_notification.enabled = true; #endif #if defined(PIN_BUZZER) @@ -1293,9 +1299,13 @@ void NodeDB::loadFromDisk() // Coerce LoRa config fields derived from presets while bootstrapping. // Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode. if (config.has_lora && config.lora.use_preset) { - RadioInterface::bootstrapLoRaConfigFromPreset(config.lora); + RadioInterface::clampConfigLora(config.lora); } +#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED + config.lora.tx_enabled = false; +#endif + if (backupSecurity.private_key.size > 0) { LOG_DEBUG("Restoring backup of security config"); config.security = backupSecurity; @@ -1640,6 +1650,25 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p) return delta; } +HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p) +{ + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return HopStartStatus::INVALID; + + if (p.hop_start == 0) { + // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a + // bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware + // version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as + // the bitfield is encrypted under the channel encryption key. + if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield) + return HopStartStatus::VALID; + return HopStartStatus::MISSING_OR_UNKNOWN; + } + + return HopStartStatus::VALID; +} + int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown) { // Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a @@ -1677,6 +1706,22 @@ size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) #include "MeshModule.h" #include "Throttle.h" +static constexpr uint32_t HOPSTART_DROP_LOG_INTERVAL_MS = 15000; + +void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context) +{ + static uint32_t lastLogMs = 0; + if (Throttle::isWithinTimespanMs(lastLogMs, HOPSTART_DROP_LOG_INTERVAL_MS)) { + return; + } + lastLogMs = millis(); + const bool decoded = (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag); + const bool hasBitfield = decoded && p.decoded.has_bitfield; + LOG_DEBUG( + "Drop packet (%s): hop_start invalid/missing (from=0x%x id=%u hop_start=%u hop_limit=%u decoded=%d has_bitfield=%d)", + context ? context : "unknown", p.from, p.id, p.hop_start, p.hop_limit, decoded, hasBitfield); +} + /** Update position info for this node based on received position data */ void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index adf2b42ea..f6be963c1 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -114,6 +114,27 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p); /// Returns defaultIfUnknown if the number of hops couldn't be determined. int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); +enum class HopStartStatus : uint8_t { VALID = 0, MISSING_OR_UNKNOWN, INVALID }; + +/// Classify hop_start validity for forwarding decisions. +HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p); + +inline bool shouldDropPacketForPreHop(const meshtastic_MeshPacket &p) +{ +#if !MESHTASTIC_PREHOP_DROP + (void)p; + return false; +#else + if (isFromUs(&p)) { + return false; // local-originated packets should never be dropped by pre-hop drop policy + } + return classifyHopStart(p) != HopStartStatus::VALID; +#endif +} + +/// Rate-limited debug log when hop_start is invalid/missing and packet is dropped. +void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context); + enum LoadFileResult { // Successfully opened the file LOAD_SUCCESS = 1, diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9050ee89d..7df6720a2 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -122,6 +122,8 @@ void PhoneAPI::close() } packetForPhone = NULL; filesManifest.clear(); + filesManifest.shrink_to_fit(); + lastPortNumToRadio.clear(); fromRadioNum = 0; config_nonce = 0; config_state = 0; @@ -447,6 +449,11 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter; break; + case meshtastic_ModuleConfig_traffic_management_tag: + LOG_DEBUG("Send module config: traffic management"); + fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; + fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management; + break; default: LOG_ERROR("Unknown module config type %d", config_state); } diff --git a/src/mesh/PointerQueue.h b/src/mesh/PointerQueue.h index b45245eb8..972eb65fd 100644 --- a/src/mesh/PointerQueue.h +++ b/src/mesh/PointerQueue.h @@ -17,14 +17,4 @@ template class PointerQueue : public TypedQueue return this->dequeue(&p, maxWait) ? p : nullptr; } - -#ifdef HAS_FREE_RTOS - // returns a ptr or null if the queue was empty - T *dequeuePtrFromISR(BaseType_t *higherPriWoken) - { - T *p; - - return this->dequeueFromISR(&p, higherPriWoken) ? p : nullptr; - } -#endif }; diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index b3aa72f7a..32c92de93 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -240,8 +240,7 @@ bool RF95Interface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > RF95_MAX_POWER) // This chip has lower power limits than some - power = RF95_MAX_POWER; + limitPower(RF95_MAX_POWER); #ifdef USE_RF95_RFO err = lora->setOutputPower(power, true); diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index ffb3ebf98..16e1c7f6e 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -16,10 +16,12 @@ #include "configuration.h" #include "detect/LoRaRadioType.h" #include "main.h" +#include "meshUtils.h" // for pow_of_2 #include "sleep.h" #include #include #include +#include #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" @@ -31,16 +33,32 @@ #include "STM32WLE5JCInterface.h" #endif -// Calculate 2^n without calling pow() -uint32_t pow_of_2(uint32_t n) -{ - return 1 << n; -} +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_STD[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, MODEM_PRESET_END}; -#define RDEF(name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, frequency_switching, wide_lora) \ +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_EU_868[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, MODEM_PRESET_END}; + +static const meshtastic_Config_LoRaConfig_ModemPreset PRESETS_UNDEF[] = {meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, + MODEM_PRESET_END}; + +// Region profiles: bundle preset list + regulatory parameters shared across regions +// presets, spacing, padding, audio, licensed, text throttle, position throttle, telemetry throttle, override slot +const RegionProfile PROFILE_STD = {PRESETS_STD, 0, 0, true, false, 0, 0, 0, 0}; +const RegionProfile PROFILE_EU868 = {PRESETS_EU_868, 0, 0, false, false, 0, 0, 0, 0}; +const RegionProfile PROFILE_UNDEF = {PRESETS_UNDEF, 0, 0, true, false, 0, 0, 0, 0}; + +#define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr) \ { \ - meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, spacing, power_limit, audio_permitted, \ - frequency_switching, wide_lora, #name \ + meshtastic_Config_LoRaConfig_RegionCode_##name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, \ + wide_lora, &profile_ptr, #name \ } const RegionInfo regions[] = { @@ -48,7 +66,7 @@ const RegionInfo regions[] = { https://link.springer.com/content/pdf/bbm%3A978-1-4842-4357-2%2F1.pdf https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ */ - RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false), + RDEF(US, 902.0f, 928.0f, 100, 30, false, false, PROFILE_STD), /* EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21] @@ -56,8 +74,7 @@ const RegionInfo regions[] = { https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf FIXME: https://github.com/meshtastic/firmware/issues/3371 */ - RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false), - + RDEF(EU_433, 433.0f, 434.0f, 10, 10, false, false, PROFILE_STD), /* https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/ https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/ @@ -72,33 +89,33 @@ const RegionInfo regions[] = { AFA) to avoid a duty cycle. (Please refer to line P page 22 of this document.) https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.01.01_60/en_30022002v030101p.pdf */ - RDEF(EU_868, 869.4f, 869.65f, 10, 0, 27, false, false, false), + RDEF(EU_868, 869.4f, 869.65f, 10, 27, false, false, PROFILE_EU868), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ - RDEF(CN, 470.0f, 510.0f, 100, 0, 19, true, false, false), + RDEF(CN, 470.0f, 510.0f, 100, 19, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf https://www.arib.or.jp/english/html/overview/doc/5-STD-T108v1_5-E1.pdf https://qiita.com/ammo0613/items/d952154f1195b64dc29f */ - RDEF(JP, 920.5f, 923.5f, 100, 0, 13, true, false, false), + RDEF(JP, 920.5f, 923.5f, 100, 13, false, false, PROFILE_STD), /* https://www.iot.org.au/wp/wp-content/uploads/2016/12/IoTSpectrumFactSheet.pdf https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf Also used in Brazil. */ - RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), + RDEF(ANZ, 915.0f, 928.0f, 100, 30, false, false, PROFILE_STD), /* 433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100 */ - RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false), + RDEF(ANZ_433, 433.05f, 434.79f, 100, 14, false, false, PROFILE_STD), /* https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf @@ -106,13 +123,13 @@ const RegionInfo regions[] = { Note: - We do LBT, so 100% is allowed. */ - RDEF(RU, 868.7f, 869.2f, 100, 0, 20, true, false, false), + RDEF(RU, 868.7f, 869.2f, 100, 20, false, false, PROFILE_STD), /* https://www.law.go.kr/LSW/admRulLsInfoP.do?admRulId=53943&efYd=0 https://resources.lora-alliance.org/technical-specifications/rp002-1-0-4-regional-parameters */ - RDEF(KR, 920.0f, 923.0f, 100, 0, 23, true, false, false), + RDEF(KR, 920.0f, 923.0f, 100, 23, false, false, PROFILE_STD), /* Taiwan, 920-925Mhz, limited to 0.5W indoor or coastal, 1.0W outdoor. @@ -120,42 +137,40 @@ const RegionInfo regions[] = { https://www.ncc.gov.tw/english/files/23070/102_5190_230703_1_doc_C.PDF https://gazette.nat.gov.tw/egFront/e_detail.do?metaid=147283 */ - RDEF(TW, 920.0f, 925.0f, 100, 0, 27, true, false, false), + RDEF(TW, 920.0f, 925.0f, 100, 27, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf */ - RDEF(IN, 865.0f, 867.0f, 100, 0, 30, true, false, false), + RDEF(IN, 865.0f, 867.0f, 100, 30, false, false, PROFILE_STD), /* https://rrf.rsm.govt.nz/smart-web/smart/page/-smart/domain/licence/LicenceSummary.wdk?id=219752 https://iotalliance.org.nz/wp-content/uploads/sites/4/2019/05/IoT-Spectrum-in-NZ-Briefing-Paper.pdf */ - RDEF(NZ_865, 864.0f, 868.0f, 100, 0, 36, true, false, false), + RDEF(NZ_865, 864.0f, 868.0f, 100, 36, false, false, PROFILE_STD), /* https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH + Thailand 920โ€“925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations. */ - RDEF(TH, 920.0f, 925.0f, 100, 0, 16, true, false, false), + RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD), /* 433,05-434,7 Mhz 10 mW - https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf - */ - RDEF(UA_433, 433.0f, 434.7f, 10, 0, 10, true, false, false), - - /* 868,0-868,6 Mhz 25 mW https://nkrzi.gov.ua/images/upload/256/5810/PDF_UUZ_19_01_2016.pdf */ - RDEF(UA_868, 868.0f, 868.6f, 1, 0, 14, true, false, false), + RDEF(UA_433, 433.0f, 434.7f, 10, 10, false, false, PROFILE_STD), + RDEF(UA_868, 868.0f, 868.6f, 1, 14, false, false, PROFILE_STD), /* Malaysia 433 - 435 MHz at 100mW, no restrictions. https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf */ - RDEF(MY_433, 433.0f, 435.0f, 100, 0, 20, true, false, false), + RDEF(MY_433, 433.0f, 435.0f, 100, 20, false, false, PROFILE_STD), /* Malaysia @@ -164,14 +179,14 @@ const RegionInfo regions[] = { Frequency hopping is used for 919 - 923 MHz. https://www.mcmc.gov.my/skmmgovmy/media/General/pdf/Short-Range-Devices-Specification.pdf */ - RDEF(MY_919, 919.0f, 924.0f, 100, 0, 27, true, true, false), + RDEF(MY_919, 919.0f, 924.0f, 100, 27, true, false, PROFILE_STD), /* Singapore SG_923 Band 30d: 917 - 925 MHz at 100mW, no restrictions. https://www.imda.gov.sg/-/media/imda/files/regulation-licensing-and-consultations/ict-standards/telecommunication-standards/radio-comms/imdatssrd.pdf */ - RDEF(SG_923, 917.0f, 925.0f, 100, 0, 20, true, false, false), + RDEF(SG_923, 917.0f, 925.0f, 100, 20, false, false, PROFILE_STD), /* Philippines @@ -181,8 +196,9 @@ const RegionInfo regions[] = { https://github.com/meshtastic/firmware/issues/4948#issuecomment-2394926135 */ - RDEF(PH_433, 433.0f, 434.7f, 100, 0, 10, true, false, false), RDEF(PH_868, 868.0f, 869.4f, 100, 0, 14, true, false, false), - RDEF(PH_915, 915.0f, 918.0f, 100, 0, 24, true, false, false), + RDEF(PH_433, 433.0f, 434.7f, 100, 10, false, false, PROFILE_STD), + RDEF(PH_868, 868.0f, 869.4f, 100, 14, false, false, PROFILE_STD), + RDEF(PH_915, 915.0f, 918.0f, 100, 24, false, false, PROFILE_STD), /* Kazakhstan @@ -190,37 +206,38 @@ const RegionInfo regions[] = { 863 - 868 MHz <25 mW EIRP, 500kHz channels allowed, must not be used at airfields https://github.com/meshtastic/firmware/issues/7204 */ - RDEF(KZ_433, 433.075f, 434.775f, 100, 0, 10, true, false, false), - RDEF(KZ_863, 863.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(KZ_433, 433.075f, 434.775f, 100, 10, false, false, PROFILE_STD), + RDEF(KZ_863, 863.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Nepal 865โ€ฏMHz to 868โ€ฏMHz frequency band for IoT (Internet of Things), M2M (Machine-to-Machine), and smart metering use, specifically in non-cellular mode. https://www.nta.gov.np/uploads/contents/Radio-Frequency-Policy-2080-English.pdf */ - RDEF(NP_865, 865.0f, 868.0f, 100, 0, 30, true, false, false), + RDEF(NP_865, 865.0f, 868.0f, 100, 30, false, false, PROFILE_STD), /* Brazil 902 - 907.5 MHz , 1W power limit, no duty cycle restrictions https://github.com/meshtastic/firmware/issues/3741 */ - RDEF(BR_902, 902.0f, 907.5f, 100, 0, 30, true, false, false), + RDEF(BR_902, 902.0f, 907.5f, 100, 30, false, false, PROFILE_STD), /* 2.4 GHZ WLAN Band equivalent. Only for SX128x chips. */ - RDEF(LORA_24, 2400.0f, 2483.5f, 100, 0, 10, true, false, true), + RDEF(LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, PROFILE_STD), /* This needs to be last. Same as US. */ - RDEF(UNSET, 902.0f, 928.0f, 100, 0, 30, true, false, false) + RDEF(UNSET, 902.0f, 928.0f, 100, 30, false, false, PROFILE_UNDEF) }; const RegionInfo *myRegion; bool RadioInterface::uses_default_frequency_slot = true; +bool RadioInterface::uses_custom_channel_name = false; static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; @@ -506,45 +523,14 @@ void initRegion() myRegion = r; } -void RadioInterface::bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig) +const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code) { - if (!loraConfig.use_preset) { - return; - } - - // Find region info to determine whether "wide" LoRa is permitted (2.4 GHz uses wider bandwidth codes). const RegionInfo *r = regions; - for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != loraConfig.region; r++) + for (; r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && r->code != code; r++) ; - - const bool regionWideLora = r->wideLora; - - float bwKHz = 0; - uint8_t sf = 0; - uint8_t cr = 0; - modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); - - // If selected preset requests a bandwidth larger than the region span, fall back to LONG_FAST. - if (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET && (r->freqEnd - r->freqStart) < (bwKHz / 1000.0f)) { - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - modemPresetToParams(loraConfig.modem_preset, regionWideLora, bwKHz, sf, cr); - } - - loraConfig.bandwidth = bwKHzToCode(bwKHz); - loraConfig.spread_factor = sf; + return r; } -/** - * ## LoRaWAN for North America - -LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments. - -The maximum output power for North America is +30 dBM. - -The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are -separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. -*/ - uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool received) { uint32_t pl = 0; @@ -754,7 +740,7 @@ void RadioInterface::saveFreq(float freq) } /** - * Save our channel for later reuse. + * Save our frequency slot (aka channel) for later reuse. */ void RadioInterface::saveChannelNum(uint32_t channel_num) { @@ -777,113 +763,304 @@ uint32_t RadioInterface::getChannelNum() return savedChannelNum; } +/** + * Send an error-level client notification. Safe to call when service is null (e.g. in tests). + */ +static void sendErrorNotification(const char *msg) +{ + if (!service) + return; + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (!cn) + return; + cn->level = meshtastic_LogRecord_Level_ERROR; + snprintf(cn->message, sizeof(cn->message), "%s", msg); + service->sendClientNotification(cn); +} + +/** + * Checks if a region is valid for the current settings. + * Returns false if not compatible. + */ +bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig) +{ + const RegionInfo *newRegion = getRegion(loraConfig.region); + + // If you are not licensed, you can't use ham regions. + if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed) { + char err_string[160]; + snprintf(err_string, sizeof(err_string), "Region %s requires licensed mode", newRegion->name); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + return false; + } + + return true; +} + +/** + * Internal helper: validate or clamp a LoRa config against its region. + * When clamp==false, returns false on first error (pure validation). + * When clamp==true, fixes invalid settings in-place and returns true. + */ +bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp) +{ + char err_string[160]; + float check_bw; + + const RegionInfo *newRegion = getRegion(loraConfig.region); + + const char *presetName = DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + + // Check preset validity (only when use_preset is true) + if (loraConfig.use_preset) { + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + + bool preset_valid = false; + for (size_t i = 0; i < newRegion->getNumPresets(); i++) { + if (loraConfig.modem_preset == newRegion->getAvailablePresets()[i]) { + preset_valid = true; + break; + } + } + if (!preset_valid) { + const char *defaultName = DisplayFormatters::getModemPresetDisplayName(newRegion->getDefaultPreset(), false, true); + if (clamp) { + snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s, using %s", presetName, newRegion->name, + defaultName); + } else { + snprintf(err_string, sizeof(err_string), "Preset %s invalid for %s", presetName, newRegion->name); + } + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + loraConfig.modem_preset = newRegion->getDefaultPreset(); + check_bw = modemPresetToBwKHz(loraConfig.modem_preset, newRegion->wideLora); + } else { + return false; + } + } + } else { + check_bw = bwCodeToKHz(loraConfig.bandwidth); + } + + // Calculate width of slots (aka channels) based on bandwidth and any spacing or padding required by the region: + // spacing = gap between slots (0 for continuous spectrum) and at the beginning of the band + // padding = gap at the beginning and end of the slots (0 for no padding) + float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz + uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); + + // Check if the region supports the requested bandwidth + if ((newRegion->freqEnd - newRegion->freqStart) < freqSlotWidth) { + const float regionSpanKHz = (newRegion->freqEnd - newRegion->freqStart) * 1000.0f; + snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora)); + check_bw = bwCodeToKHz(loraConfig.bandwidth); + + // Recompute slot width and number of slots based on the new bandwidth + freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (check_bw / 1000); // in MHz + numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); + } else { + return false; + } + } + + const char *channelName = channels.getName(channels.getPrimaryIndex()); + const char *presetNameDisplay = + DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); + uint32_t channelNameHashSlot = hash(channelName) % numFreqSlots; + uint32_t presetNameHashSlot = hash(presetNameDisplay) % numFreqSlots; + + if (loraConfig.override_frequency == 0) { + + // Check if we use the default frequency slot + uses_default_frequency_slot = + (loraConfig.channel_num == 0) || // user choice unset, no frequency override, so use default + (newRegion->profile->overrideSlot != 0 && + loraConfig.channel_num == newRegion->profile->overrideSlot) || // user setting matches override + ((newRegion->profile->overrideSlot == 0) && + ((uint32_t)(loraConfig.channel_num - 1) == presetNameHashSlot)); // user setting matches preset hash, no override + + // check if user setting different to preset name + uses_custom_channel_name = (strcmp(channelName, presetNameDisplay) != 0); + + if (loraConfig.channel_num > numFreqSlots) { + snprintf(err_string, sizeof(err_string), "Channel number %u invalid for %s, max is %u", loraConfig.channel_num, + newRegion->name, numFreqSlots); + LOG_ERROR("%s", err_string); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + sendErrorNotification(err_string); + + if (clamp) { + if (uses_custom_channel_name) { // clamp to channel name hash + loraConfig.channel_num = + channelNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 + } else if ((loraConfig.use_preset) && (newRegion->profile->overrideSlot != 0)) { // clamp to preset override slot + loraConfig.channel_num = + newRegion->profile->overrideSlot; // use the override slot specified by the region profile + uses_default_frequency_slot = true; + } else if (loraConfig.use_preset) { // clamp to preset slot + loraConfig.channel_num = presetNameHashSlot + 1; // channel_num is 1-based, but hash slot is 0-based, so add 1 + uses_default_frequency_slot = true; + } else { // if not using preset, and no custom channel name, just clamp to default anyway + uses_default_frequency_slot = true; + }; + } else { + return false; + } + } // end of channel number check + } else { + // if we have a frequency override, we ignore the channel number and just use the override frequency + snprintf(err_string, sizeof(err_string), "Frequency override in place, using %.3f", loraConfig.override_frequency); + } + return true; +} + +bool RadioInterface::validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig) +{ + auto copy = loraConfig; + return checkOrClampConfigLora(copy, false); +} + +void RadioInterface::clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig) +{ + checkOrClampConfigLora(loraConfig, true); +} + /** * Pull our channel settings etc... from protobufs to the dumb interface settings + * Note: this must be given only settings which have been validated or clamped! */ void RadioInterface::applyModemConfig() { // Set up default configuration // No Sync Words in LORA mode meshtastic_Config_LoRaConfig &loraConfig = config.lora; - bool validConfig = false; // We need to check for a valid configuration - while (!validConfig) { - if (loraConfig.use_preset) { - modemPresetToParams(loraConfig.modem_preset, myRegion->wideLora, bw, sf, cr); - if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate != cr) { - cr = loraConfig.coding_rate; - LOG_INFO("Using custom Coding Rate %u", cr); - } - } else { - sf = loraConfig.spread_factor; + const RegionInfo *newRegion = getRegion(loraConfig.region); + myRegion = newRegion; + + if (loraConfig.use_preset) { + if (!validateConfigLora(loraConfig)) { + loraConfig.modem_preset = newRegion->getDefaultPreset(); + } + uint8_t newcr; + modemPresetToParams(loraConfig.modem_preset, newRegion->wideLora, bw, sf, newcr); + // If custom CR is being used already, check if the new preset is higher + if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate < newcr) { + cr = newcr; + LOG_INFO("Default Coding Rate is higher than custom setting, using %u", cr); + } + // If the custom CR is higher than the preset, use it + else if (loraConfig.coding_rate >= 5 && loraConfig.coding_rate <= 8 && loraConfig.coding_rate > newcr) { cr = loraConfig.coding_rate; - bw = bwCodeToKHz(loraConfig.bandwidth); - } - - if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f; - const float requestedBwKHz = bw; - const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset - const char *presetName = - DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset); - - char err_string[160]; - if (isWideRequest) { - snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.", - myRegion->name, presetName); - } else { - snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.", - myRegion->name, regionSpanKHz, requestedBwKHz); - } - LOG_ERROR("%s", err_string); - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); - cn->level = meshtastic_LogRecord_Level_ERROR; - snprintf(cn->message, sizeof(cn->message), "%s", err_string); - service->sendClientNotification(cn); - - // Set to default modem preset - loraConfig.use_preset = true; - loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + LOG_INFO("Using custom Coding Rate %u", cr); } else { - validConfig = true; + cr = newcr; } + + } else { // if not using preset, then just use the custom settings + if (validateConfigLora(loraConfig)) { + } else { + LOG_WARN("Invalid LoRa config settings, cannot apply requested modem config - falling back to %s defaults", + newRegion->name); + clampConfigLora(loraConfig); + } + bw = bwCodeToKHz(loraConfig.bandwidth); + sf = loraConfig.spread_factor; + cr = loraConfig.coding_rate; } power = loraConfig.tx_power; - if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) - power = myRegion->powerLimit; + if ((power == 0) || ((power > newRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = newRegion->powerLimit; if (power == 0) - power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults + power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of newRegion defaults // to 0, currently no region has an actual power limit of 0 [dBm] so we can assume regions which have this // variable set to 0 don't have a valid power limit) // Set final tx_power back onto config loraConfig.tx_power = (int8_t)power; // cppcheck-suppress assignmentAddressToInteger - // Calculate the number of channels - uint32_t numChannels = floor((myRegion->freqEnd - myRegion->freqStart) / (myRegion->spacing + (bw / 1000))); + uint32_t channel_num; + float freq; - // If user has manually specified a channel num, then use that, otherwise generate one by hashing the name - const char *channelName = channels.getName(channels.getPrimaryIndex()); - // channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1) - uint32_t channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels; + // Calculate number of frequency slots (aka Channels): + // spacing = gap between channels (0 for continuous spectrum) and at the beginning of the band + // padding = gap at the beginning and end of the channel (0 for no padding) + float freqSlotWidth = newRegion->profile->spacing + (newRegion->profile->padding * 2) + (bw / 1000); // in MHz + uint32_t numFreqSlots = round((newRegion->freqEnd - newRegion->freqStart + newRegion->profile->spacing) / freqSlotWidth); - // Check if we use the default frequency slot - RadioInterface::uses_default_frequency_slot = - channel_num == - hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; - - // Old frequency selection formula - // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); - - // New frequency selection formula - float freq = myRegion->freqStart + (bw / 2000) + (channel_num * (bw / 1000)); + // Calculate hash of channel name and preset name to pick a default frequency slot if user has not specified one. + // Note that channel_num is actually (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to + // (numFreqSlots - 1). + uint32_t presetNameHashSlot = + hash(DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset)) % numFreqSlots; // override if we have a verbatim frequency if (loraConfig.override_frequency) { freq = loraConfig.override_frequency; channel_num = -1; + uses_default_frequency_slot = false; + } else { + + // If user has not manually specified a frequency slot, or has not specified one that is different than the default or the + // override for the new region, then use the default or override. If the user has not specified one, but has specified a + // custom channel name, then use the hash of that channel name to pick a frequency slot. Note that channel_num is actually + // (channel_num - 1), i.e. zero-based, since modulus (%) returns values from 0 to (numFreqSlots - 1). + // NB: channel_num is also know as frequency slot but it's too late to fix now. + if (uses_default_frequency_slot) { + // if there's an override slot, use that + if (newRegion->profile->overrideSlot != 0) { + channel_num = newRegion->profile->overrideSlot - 1; + } else { + channel_num = presetNameHashSlot; + } + } else { // use the manually defined one + channel_num = loraConfig.channel_num - 1; + } + + // Calculate frequency: freqStart is band edge, add half bandwidth (plus optional padding) to get middle of first channel + // subsequent channels are spaced by freqSlotWidth + freq = newRegion->freqStart + (bw / 2000) + newRegion->profile->padding + (channel_num * freqSlotWidth); // in MHz } saveChannelNum(channel_num); saveFreq(freq + loraConfig.frequency_offset); + const char *channelName = channels.getName(channels.getPrimaryIndex()); + + if (newRegion->wideLora) { // clamp if wide freq range + preambleLength = wideLoraPreambleLengthDefault; // 12 is the default for operation above 2GHz + } else { + preambleLength = + preambleLengthDefault; // 8 is default, but we use longer to increase the amount of sleep time when receiving + } slotTimeMsec = computeSlotTimeMsec(); preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw); LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset); - LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset, + LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", newRegion->name, channelName, loraConfig.modem_preset, channel_num, power); - LOG_INFO("myRegion->freqStart -> myRegion->freqEnd: %f -> %f (%f MHz)", myRegion->freqStart, myRegion->freqEnd, - myRegion->freqEnd - myRegion->freqStart); - LOG_INFO("numChannels: %d x %.3fkHz", numChannels, bw); + LOG_INFO("newRegion->freqStart -> newRegion->freqEnd: %f -> %f (%f MHz)", newRegion->freqStart, newRegion->freqEnd, + newRegion->freqEnd - newRegion->freqStart); + LOG_INFO("numFreqSlots: %d x %.3fkHz", numFreqSlots, bw); + if (newRegion->profile->overrideSlot != 0) { + LOG_INFO("Using region override slot: %d", newRegion->profile->overrideSlot); + } LOG_INFO("channel_num: %d", channel_num + 1); LOG_INFO("frequency: %f", getFreq()); LOG_INFO("Slot time: %u msec, preamble time: %u msec", slotTimeMsec, preambleTimeMsec); -} +} // end of applyModemConfig /** Slottime is the time to detect a transmission has started, consisting of: - CAD duration; @@ -920,6 +1097,12 @@ void RadioInterface::limitPower(int8_t loraMaxPower) power = maxPower; } +#if HAS_LORA_FEM + if (!devicestate.owner.is_licensed) { + power = loraFEMInterface.powerConversion(power); + } +#else +// todo:All entries containing "lora fem" are grouped together above. #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; @@ -945,7 +1128,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } } - +#endif if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; @@ -991,4 +1174,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); -} +} \ No newline at end of file diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 1fe3dd7b0..a1c692e24 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -8,6 +8,10 @@ #include "error.h" #include +#if HAS_LORA_FEM +#include "LoRaFEMInterface.h" +#endif + // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; @@ -88,15 +92,20 @@ class RadioInterface uint8_t sf = 9; uint8_t cr = 5; - const uint8_t NUM_SYM_CAD = 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 - const uint8_t NUM_SYM_CAD_24GHZ = 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 + static constexpr uint8_t NUM_SYM_CAD = + 2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48 + static constexpr uint8_t NUM_SYM_CAD_24GHZ = + 4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280 uint32_t slotTimeMsec = computeSlotTimeMsec(); - uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving - uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast - const uint32_t PROCESSING_TIME_MSEC = - 4500; // time to construct, process and construct a packet again (empirically determined) - const uint8_t CWmin = 3; // minimum CWsize - const uint8_t CWmax = 8; // maximum CWsize + uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t preambleLengthDefault = + 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving + static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora + uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast + static constexpr uint32_t PROCESSING_TIME_MSEC = + 4500; // time to construct, process and construct a packet again (empirically determined) + static constexpr uint8_t CWmin = 3; // minimum CWsize + static constexpr uint8_t CWmax = 8; // maximum CWsize meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending uint32_t lastTxStart = 0L; @@ -123,7 +132,7 @@ class RadioInterface * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. * This is used during early bootstrapping so UIs that display these fields directly remain consistent. */ - static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); + // static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded? /** * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving) @@ -152,7 +161,7 @@ class RadioInterface virtual ErrorCode send(meshtastic_MeshPacket *p) = 0; /** Return TX queue status */ - virtual meshtastic_QueueStatus getQueueStatus() + [[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus() { meshtastic_QueueStatus qs; qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0; @@ -178,22 +187,22 @@ class RadioInterface virtual bool reconfigure(); /** The delay to use for retransmitting dropped packets */ - uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); + [[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p); /** The delay to use when we want to send something */ - uint32_t getTxDelayMsec(); + [[nodiscard]] uint32_t getTxDelayMsec(); /** The CW to use when calculating SNR_based delays */ - uint8_t getCWsize(float snr); + [[nodiscard]] uint8_t getCWsize(float snr); /** The worst-case SNR_based packet delay */ - uint32_t getTxDelayMsecWeightedWorst(float snr); + [[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr); /** Returns true if we should rebroadcast early like a ROUTER */ - bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p); + [[nodiscard]] 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(meshtastic_MeshPacket *p); + [[nodiscard]] 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; } @@ -211,18 +220,18 @@ class RadioInterface * * @return num msecs for the packet */ - uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); - virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; + [[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false); + [[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0; /** * Get the channel we saved. */ - uint32_t getChannelNum(); + [[nodiscard]] uint32_t getChannelNum(); /** * Get the frequency we saved. */ - virtual float getFreq(); + [[nodiscard]] virtual float getFreq(); /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers virtual bool isIRQPending() { return false; } @@ -230,6 +239,20 @@ class RadioInterface // Whether we use the default frequency slot given our LoRa config (region and modem preset) static bool uses_default_frequency_slot; + // Whether we have a custom channel name + static bool uses_custom_channel_name; + + static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp); + + // Check if a candidate region is compatible and valid. + static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig); + + // Check if a candidate radio configuration is valid. + static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig); + + // Make a candidate radio configuration valid, even if it isn't. + static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig); + protected: int8_t power = 17; // Set by applyModemConfig() @@ -242,7 +265,7 @@ class RadioInterface * * Used as the first step of */ - size_t beginSending(meshtastic_MeshPacket *p); + [[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p); /** * Some regulatory regions limit xmit power. diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 30cd587da..7ef707e0d 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -246,6 +246,24 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) return txQueue.find(from, id); } +bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length) +{ + if (!buffer || length == 0 || !iface) { + return false; + } + + // Older RadioLib versions only expose random(min, max), so fill the buffer byte-by-byte. + for (size_t i = 0; i < length; ++i) { + int32_t value = iface->random(0, 255); + if (value < 0) { + return false; + } + buffer[i] = static_cast(value & 0xFF); + } + + return true; +} + /** radio helper thread callback. We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of 'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision. @@ -587,4 +605,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) return res == RADIOLIB_ERR_NONE; } -} \ No newline at end of file +} diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index ca3d78503..310ca76bb 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -172,6 +172,12 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) override; + /** + * Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface. + * @return true if len bytes were produced, false otherwise. + */ + bool randomBytes(uint8_t *buffer, size_t length); + private: /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually * doing the transmit */ diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 10eb02197..785d4928d 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -11,12 +11,16 @@ #include "mesh-pb-constants.h" #include "meshUtils.h" #include "modules/RoutingModule.h" +#if HAS_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" #endif #include "Default.h" #if ARCH_PORTDUINO #include "modules/Native/StoreForwardPlusPlus.h" +#include "Throttle.h" #include "platform/portduino/PortduinoGlue.h" #endif #if ENABLE_JSON_LOGGING || ARCH_PORTDUINO @@ -95,6 +99,20 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) return true; } +#if HAS_TRAFFIC_MANAGEMENT + // When router_preserve_hops is enabled, preserve hops for decoded packets that are not + // position or telemetry (those have their own exhaust_hop controls). + if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled && + moduleConfig.traffic_management.router_preserve_hops && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + p->decoded.portnum != meshtastic_PortNum_POSITION_APP && p->decoded.portnum != meshtastic_PortNum_TELEMETRY_APP) { + LOG_DEBUG("Router hop preserved: port=%d from=0x%08x (traffic_management)", p->decoded.portnum, getFrom(p)); + if (trafficManagementModule) { + trafficManagementModule->recordRouterHopPreserved(); + } + return false; + } +#endif + // For subsequent hops, check if previous relay is a favorite router // Optimized search for favorite routers with matching last byte // Check ordering optimized for IoT devices (cheapest checks first) @@ -539,6 +557,25 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) { LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str()); } else if (portduino_config.JSONFilename != "") { + if (portduino_config.JSONFileRotate != 0) { + static uint32_t fileage = 0; + + if (portduino_config.JSONFileRotate != 0 && + (fileage == 0 || !Throttle::isWithinTimespanMs(fileage, portduino_config.JSONFileRotate * 60 * 1000))) { + time_t timestamp = time(NULL); + struct tm *timeinfo; + char buffer[80]; + timeinfo = localtime(×tamp); + strftime(buffer, 80, "%Y%m%d-%H%M%S", timeinfo); + + std::string datetime(buffer); + if (JSONFile.is_open()) { + JSONFile.close(); + } + JSONFile.open(portduino_config.JSONFilename + "_" + datetime, std::ios::out | std::ios::app); + fileage = millis(); + } + } if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; } @@ -788,8 +825,32 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) p_encrypted->pki_encrypted = true; // After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet if ((decodedState == DecodeState::DECODE_SUCCESS || p_encrypted->pki_encrypted) && moduleConfig.mqtt.enabled && - !isFromUs(p) && mqtt) + !isFromUs(p) && mqtt) { + if (decodedState == DecodeState::DECODE_SUCCESS && p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && + moduleConfig.mqtt.encryption_enabled) { + // For TRACEROUTE_APP packets release the original encrypted packet and encrypt a new from the changed packet + // Only release the original after successful allocation to avoid losing an incomplete but valid packet + auto *p_encrypted_new = packetPool.allocCopy(*p); + if (p_encrypted_new) { + auto encodeResult = perhapsEncode(p_encrypted_new); + if (encodeResult != meshtastic_Routing_Error_NONE) { + // Encryption failed, release the new packet and fall back to sending the original encrypted packet to + // MQTT + LOG_WARN("Encryption of new TR packet failed, sending original TR to MQTT"); + packetPool.release(p_encrypted_new); + p_encrypted_new = nullptr; + } else { + // Successfully re-encrypted, release the original encrypted packet and use the new one for MQTT + packetPool.release(p_encrypted); + p_encrypted = p_encrypted_new; + } + } else { + // Allocation failed, log a warning and fall back to sending the original encrypted packet to MQTT + LOG_WARN("Failed to allocate new encrypted packet for TR, sending original TR to MQTT"); + } + } mqtt->onSend(*p_encrypted, *p, p->channel); + } } #endif } @@ -837,6 +898,12 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) return; } + if (shouldDropPacketForPreHop(*p)) { + logHopStartDrop(*p, "pre-hop drop"); + packetPool.release(p); + return; + } + if (shouldFilterReceived(p)) { LOG_DEBUG("Incoming msg was filtered from 0x%x", p->from); packetPool.release(p); diff --git a/src/mesh/Router.h b/src/mesh/Router.h index dbb5e5802..0f342d57b 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -58,14 +58,14 @@ class Router : protected concurrency::OSThread, protected PacketHistory /** Allocate and return a meshpacket which defaults as send to broadcast from the current node. * The returned packet is guaranteed to have a unique packet ID already assigned */ - meshtastic_MeshPacket *allocForSending(); + [[nodiscard]] meshtastic_MeshPacket *allocForSending(); /** Return Underlying interface's TX queue status */ - meshtastic_QueueStatus getQueueStatus(); + [[nodiscard]] meshtastic_QueueStatus getQueueStatus(); /** * @return our local nodenum */ - NodeNum getNodeNum(); + [[nodiscard]] NodeNum getNodeNum(); /** Wake up the router thread ASAP, because we just queued a message for it. * FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this queue' diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index a9ee16545..bcb08f2c5 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -6,7 +6,7 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #endif -#if defined(USE_GC1109_PA) && defined(ARCH_ESP32) +#if defined(ARCH_ESP32) #include #include #endif @@ -56,41 +56,12 @@ template bool SX126xInterface::init() pinMode(SX126X_POWER_EN, OUTPUT); #endif -#if defined(USE_GC1109_PA) - // GC1109 FEM chip initialization - // See variant.h for full pin mapping and control logic documentation - // - // On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in - // enableLoraInterrupt). We configure GPIO registers before releasing the hold - // so the pad transitions atomically from held-HIGH to register-HIGH with no - // power glitch. On cold boot the hold_dis is a harmless no-op. - - // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) - pinMode(LORA_PA_POWER, OUTPUT); - digitalWrite(LORA_PA_POWER, HIGH); - rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); - - // TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait - // for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement: - // "VBAT must be prior to CSD/CPS/CTX for the power on sequence" - // On deep sleep wake the LDO was held on via RTC latch, so no delay needed. -#if defined(ARCH_ESP32) - if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) { - delayMicroseconds(1000); +#if HAS_LORA_FEM + loraFEMInterface.init(); + // Apply saved FEM LNA mode from config + if (loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); } -#else - delayMicroseconds(1000); -#endif - - // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX - pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, HIGH); - rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN); - - // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) - // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH - pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); // Start in RX-ready state #endif #ifdef RF95_FAN_EN @@ -194,16 +165,13 @@ template bool SX126xInterface::init() LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } -#ifdef USE_GC1109_PA - // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity - // on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5. - // Reference: https://github.com/meshcore-dev/MeshCore/pull/1398 + // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity. + // Sets bit 0 of register 0x8B5. if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) { - LOG_INFO("Applied SX1262 register 0x8B5 patch for GC1109 RX improvement"); + LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement"); } else { - LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for GC1109"); + LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement"); } -#endif #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms @@ -277,14 +245,21 @@ template bool SX126xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX126X_MAX_POWER) // This chip has lower power limits than some - power = SX126X_MAX_POWER; + limitPower(SX126X_MAX_POWER); + // Make sure we reach the minimum power supported to turn the chip on (-9dBm) + if (power < -9) + power = -9; err = lora.setOutputPower(power); if (err != RADIOLIB_ERR_NONE) LOG_ERROR("SX126X setOutputPower %s%d", radioLibErr, err); assert(err == RADIOLIB_ERR_NONE); + // Apply RX gain mode โ€” valid in STDBY (datasheet ยง9.6), matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("SX126X setRxBoostedGainMode %s%d", radioLibErr, err); + startReceive(); // restart receiving return RADIOLIB_ERR_NONE; @@ -422,15 +397,10 @@ template bool SX126xInterface::sleep() digitalWrite(SX126X_POWER_EN, LOW); #endif -#if defined(USE_GC1109_PA) - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - * // digitalWrite(LORA_PA_POWER, LOW); - */ - digitalWrite(LORA_PA_EN, LOW); - digitalWrite(LORA_PA_TX_EN, LOW); +#if HAS_LORA_FEM + loraFEMInterface.setSleepModeEnable(); #endif + return true; } @@ -492,10 +462,12 @@ template void SX126xInterface::resetAGC() /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { -#if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on - digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) +#if HAS_LORA_FEM + if (txon) { + loraFEMInterface.setTxModeEnable(); + } else { + loraFEMInterface.setRxModeEnable(); + } #endif } diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 0e882ef05..cb21c0770 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -144,8 +144,7 @@ template bool SX128xInterface::reconfigure() if (err != RADIOLIB_ERR_NONE) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - if (power > SX128X_MAX_POWER) // This chip has lower power limits than some - power = SX128X_MAX_POWER; + limitPower(SX128X_MAX_POWER); err = lora.setOutputPower(power); if (err != RADIOLIB_ERR_NONE) diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 97e231f23..c724871cb 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -52,6 +52,9 @@ class StreamAPI : public PhoneAPI virtual int32_t runOncePart(); virtual int32_t runOncePart(char *buf, uint16_t bufLen); + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override = 0; + private: /** * Read any rx chars from the link and call handleToRadio @@ -73,9 +76,6 @@ class StreamAPI : public PhoneAPI virtual void onConnectionChanged(bool connected) override; - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override = 0; - /** * Send the current txBuffer over our stream */ diff --git a/src/mesh/Throttle.cpp b/src/mesh/Throttle.cpp index f278cc843..a4f8347b2 100644 --- a/src/mesh/Throttle.cpp +++ b/src/mesh/Throttle.cpp @@ -31,5 +31,6 @@ bool Throttle::execute(uint32_t *lastExecutionMs, uint32_t minumumIntervalMs, vo /// @param timeSpanMs The interval in milliseconds of the timespan bool Throttle::isWithinTimespanMs(uint32_t lastExecutionMs, uint32_t timeSpanMs) { - return (millis() - lastExecutionMs) < timeSpanMs; + uint32_t now = millis(); + return (now - lastExecutionMs) < timeSpanMs; } \ No newline at end of file diff --git a/src/mesh/TransmitHistory.cpp b/src/mesh/TransmitHistory.cpp index 3dbabc635..33da7d35c 100644 --- a/src/mesh/TransmitHistory.cpp +++ b/src/mesh/TransmitHistory.cpp @@ -16,6 +16,20 @@ TransmitHistory *TransmitHistory::getInstance() return transmitHistory; } +TransmitHistory::StoredTimestamp TransmitHistory::makeStoredTimestamp(uint32_t seconds, uint8_t flags) +{ + StoredTimestamp stored; + stored.seconds = seconds; + stored.flags = flags; + return stored; +} + +TransmitHistory::StoredTimestamp TransmitHistory::decodeLegacyTimestamp(uint32_t seconds) +{ + const bool isProbablyBootRelative = seconds > 0 && seconds <= LEGACY_BOOT_RELATIVE_MAX_SEC; + return makeStoredTimestamp(seconds, isProbablyBootRelative ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE); +} + void TransmitHistory::loadFromDisk() { spiLock->lock(); @@ -23,16 +37,33 @@ void TransmitHistory::loadFromDisk() if (file) { FileHeader header{}; if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && - header.version == VERSION && header.count <= MAX_ENTRIES) { + (header.version == 1 || header.version == VERSION) && header.count <= MAX_ENTRIES) { for (uint8_t i = 0; i < header.count; i++) { - Entry entry{}; - if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) { - if (entry.epochSeconds > 0) { - history[entry.key] = entry.epochSeconds; - // Seed in-memory millis so throttle works even without RTC/GPS. - // Treating stored entries as "just sent" is safe โ€” worst case the - // node waits one full interval before its first broadcast. - lastMillis[entry.key] = millis(); + if (header.version == 1) { + LegacyEntry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = decodeLegacyTimestamp(entry.epochSeconds); + } + } else { + Entry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry) && entry.epochSeconds > 0) { + history[entry.key] = makeStoredTimestamp(entry.epochSeconds, entry.flags); + // Do NOT seed lastMillis here. + // + // getLastSentToMeshMillis() reconstructs a millis()-relative value + // from the stored epoch, and Throttle::isWithinTimespanMs() uses + // the same unsigned subtraction pattern. Once getTime() has a valid + // wall-clock epoch comparable to stored values, recent reboots still + // throttle correctly while long power-off periods no longer look like + // "just sent" and incorrectly suppress the first send. + // + // Before RTC/NTP/GPS time is valid, persisted absolute epochs do not + // contribute, but boot-relative entries still suppress near-term reboot + // chatter via a narrow recovery window. + // + // If we seeded lastMillis to millis() here, every loaded entry would + // appear to have been sent at boot time, regardless of the true age + // of the last transmission. That was the regression behind #9901. } } } @@ -53,7 +84,8 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) lastMillis[key] = millis(); uint32_t now = getTime(); if (now >= 2) { - history[key] = now; + const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE; + history[key] = makeStoredTimestamp(now, flags); dirty = true; // Don't flush to disk on every transmit โ€” flash has limited write endurance. // The in-memory lastMillis map handles throttle during normal operation. @@ -68,15 +100,84 @@ void TransmitHistory::setLastSentToMesh(uint16_t key) } } +#ifdef PIO_UNIT_TESTING +void TransmitHistory::setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds) +{ + if (epochSeconds > 0) { + history[key] = makeStoredTimestamp(epochSeconds, ENTRY_FLAG_NONE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} + +void TransmitHistory::setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot) +{ + if (secondsSinceBoot > 0) { + history[key] = makeStoredTimestamp(secondsSinceBoot, ENTRY_FLAG_BOOT_RELATIVE); + dirty = true; + } else { + history.erase(key); + lastMillis.erase(key); + } +} +#endif + uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const { auto it = history.find(key); if (it != history.end()) { - return it->second; + return it->second.seconds; } return 0; } +uint32_t TransmitHistory::getLastSentAbsoluteMillis(uint32_t storedEpoch) const +{ + uint32_t now = getTime(); + if (now < 2) { + return 0; + } + + if (storedEpoch > now) { + return 0; + } + + uint32_t secondsAgo = now - storedEpoch; + uint32_t msAgo = secondsAgo * 1000; + + if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { + return 0; + } + + return millis() - msAgo; +} + +uint32_t TransmitHistory::getLastSentBootRelativeMillis(uint32_t storedSeconds) const +{ + if (getRTCQuality() != RTCQualityNone) { + return 0; + } + + uint32_t now = getTime(); + + if (storedSeconds <= now) { + uint32_t secondsAgo = now - storedSeconds; + if (secondsAgo > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + return millis() - (secondsAgo * 1000); + } + + uint32_t secondsAhead = storedSeconds - now; + if (secondsAhead > BOOT_RELATIVE_RECOVERY_WINDOW_SEC) { + return 0; + } + + return millis(); +} + uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const { // Prefer runtime millis value (accurate within this boot) @@ -86,34 +187,23 @@ uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const } // Fall back to epoch conversion (loaded from disk after reboot) - uint32_t storedEpoch = getLastSentToMeshEpoch(key); - if (storedEpoch == 0) { + auto it = history.find(key); + if (it == history.end() || it->second.seconds == 0) { return 0; // No stored time โ€” module has never sent } - uint32_t now = getTime(); - if (now < 2) { - // No valid RTC time yet โ€” can't convert to millis. Return 0 so throttle doesn't block. - return 0; + // Convert to a millis()-relative timestamp: millis() - msAgo. + // + // The result may wrap if msAgo is larger than the current uptime, and that is + // intentional. Throttle::isWithinTimespanMs() also uses unsigned subtraction, + // so the reconstructed age is preserved across wraparound: + // - recent reboot, 5 min ago -> (millis() - lastMs) == 300000, still throttled + // - long reboot, 30 min ago -> (millis() - lastMs) == 1800000, allowed + if ((it->second.flags & ENTRY_FLAG_BOOT_RELATIVE) != 0) { + return getLastSentBootRelativeMillis(it->second.seconds); } - if (storedEpoch > now) { - // Stored time is in the future (clock went backwards?) โ€” treat as stale - return 0; - } - - uint32_t secondsAgo = now - storedEpoch; - uint32_t msAgo = secondsAgo * 1000; - - // Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle) - if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { - return 0; - } - - // Convert to a millis()-relative timestamp: millis() - msAgo - // This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval), - // correctly reports whether the transmit was within interval ms. - return millis() - msAgo; + return getLastSentAbsoluteMillis(it->second.seconds); } bool TransmitHistory::saveToDisk() @@ -141,12 +231,13 @@ bool TransmitHistory::saveToDisk() file.write((uint8_t *)&header, sizeof(header)); uint8_t written = 0; - for (auto &pair : history) { + for (const auto &[key, stored] : history) { if (written >= MAX_ENTRIES) break; Entry entry{}; - entry.key = pair.first; - entry.epochSeconds = pair.second; + entry.key = key; + entry.epochSeconds = stored.seconds; + entry.flags = stored.flags; file.write((uint8_t *)&entry, sizeof(entry)); written++; } diff --git a/src/mesh/TransmitHistory.h b/src/mesh/TransmitHistory.h index 01201eaac..1a79048ea 100644 --- a/src/mesh/TransmitHistory.h +++ b/src/mesh/TransmitHistory.h @@ -35,8 +35,25 @@ class TransmitHistory */ void setLastSentToMesh(uint16_t key); +#ifdef PIO_UNIT_TESTING /** - * Get the last transmit epoch seconds for a given key, or 0 if unknown. + * Directly set the stored epoch for a key without touching the runtime lastMillis map. + * Intended for testing purposes: lets tests simulate "the last broadcast happened N + * seconds ago" without needing to fake the system clock. + */ + void setLastSentAtEpoch(uint16_t key, uint32_t epochSeconds); + + /** + * Directly set a boot-relative timestamp (seconds since boot) for testing. + */ + void setLastSentAtBootRelative(uint16_t key, uint32_t secondsSinceBoot); +#endif + + /** + * Get the raw persisted timestamp seconds for a given key, or 0 if unknown. + * + * The returned value is an absolute epoch when persisted with valid RTC/NTP/GPS time, + * or boot-relative seconds when ENTRY_FLAG_BOOT_RELATIVE is set. */ uint32_t getLastSentToMeshEpoch(uint16_t key) const; @@ -64,13 +81,31 @@ class TransmitHistory static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; static constexpr uint32_t MAGIC = 0x54485354; // "THST" - static constexpr uint8_t VERSION = 1; + static constexpr uint8_t VERSION = 2; static constexpr uint8_t MAX_ENTRIES = 16; static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + static constexpr uint32_t BOOT_RELATIVE_RECOVERY_WINDOW_SEC = 2 * 60; + static constexpr uint32_t LEGACY_BOOT_RELATIVE_MAX_SEC = 365UL * 24 * 60 * 60; + + enum EntryFlags : uint8_t { + ENTRY_FLAG_NONE = 0, + ENTRY_FLAG_BOOT_RELATIVE = 0x01, + }; + + struct StoredTimestamp { + uint32_t seconds = 0; + uint8_t flags = ENTRY_FLAG_NONE; + }; struct __attribute__((packed)) Entry { uint16_t key; uint32_t epochSeconds; + uint8_t flags; + }; + + struct __attribute__((packed)) LegacyEntry { + uint16_t key; + uint32_t epochSeconds; }; struct __attribute__((packed)) FileHeader { @@ -79,8 +114,13 @@ class TransmitHistory uint8_t count; }; - std::map history; // key -> epoch seconds (for disk persistence) - std::map lastMillis; // key -> millis() value (for runtime throttle) + uint32_t getLastSentAbsoluteMillis(uint32_t storedEpoch) const; + uint32_t getLastSentBootRelativeMillis(uint32_t storedSeconds) const; + static StoredTimestamp makeStoredTimestamp(uint32_t seconds, uint8_t flags = ENTRY_FLAG_NONE); + static StoredTimestamp decodeLegacyTimestamp(uint32_t seconds); + + std::map history; // key -> persisted transmit time + std::map lastMillis; // key -> millis() value (for runtime throttle) bool dirty = false; uint32_t lastDiskSave = 0; // millis() of last disk flush }; diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h index 357eb05c2..aececf85e 100644 --- a/src/mesh/api/PacketAPI.h +++ b/src/mesh/api/PacketAPI.h @@ -15,11 +15,11 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread static PacketAPI *create(PacketServer *_server); virtual ~PacketAPI(){}; virtual int32_t runOnce(); + // Check the current underlying physical queue to see if the client is fetching packets + bool checkIsConnected() override; protected: explicit PacketAPI(PacketServer *_server); - // Check the current underlying physical queue to see if the client is fetching packets - bool checkIsConnected() override; void onNowHasData(uint32_t fromRadioNum) override {} void onConnectionChanged(bool connected) override {} diff --git a/src/mesh/api/ServerAPI.cpp b/src/mesh/api/ServerAPI.cpp index 1a506421c..f3e7854ca 100644 --- a/src/mesh/api/ServerAPI.cpp +++ b/src/mesh/api/ServerAPI.cpp @@ -1,7 +1,10 @@ #include "ServerAPI.h" +#include "Throttle.h" #include "configuration.h" #include +static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL; + template ServerAPI::ServerAPI(T &_client) : StreamAPI(&client), concurrency::OSThread("ServerAPI"), client(_client) { @@ -28,9 +31,16 @@ template bool ServerAPI::checkIsConnected() template int32_t ServerAPI::runOnce() { if (client.connected()) { + if (lastContactMsec > 0 && !Throttle::isWithinTimespanMs(lastContactMsec, TCP_IDLE_TIMEOUT_MS)) { + LOG_WARN("TCP connection timeout, no data for %lu ms", (unsigned long)(millis() - lastContactMsec)); + close(); + enabled = false; + return 0; + } return StreamAPI::runOncePart(); } else { LOG_INFO("Client dropped connection, suspend API service"); + close(); enabled = false; // we no longer need to run return 0; } @@ -45,13 +55,18 @@ template void APIServerPort::init() template int32_t APIServerPort::runOnce() { + // Clean up previous connection if its client already disconnected + if (openAPI && !openAPI->checkIsConnected()) { + openAPI.reset(); + } + #ifdef ARCH_ESP32 #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) auto client = U::accept(); #else auto client = U::available(); #endif -#elif defined(ARCH_RP2040) +#elif defined(ARCH_RP2040) || defined(ARCH_NRF52) auto client = U::accept(); #else auto client = U::available(); @@ -70,10 +85,10 @@ template int32_t APIServerPort::runOnce() } #endif LOG_INFO("Force close previous TCP connection"); - delete openAPI; + openAPI.reset(); } - openAPI = new T(client); + openAPI.reset(new T(client)); } #if RAK_4631 diff --git a/src/mesh/api/ServerAPI.h b/src/mesh/api/ServerAPI.h index 111314476..2da77c8e9 100644 --- a/src/mesh/api/ServerAPI.h +++ b/src/mesh/api/ServerAPI.h @@ -1,6 +1,7 @@ #pragma once #include "StreamAPI.h" +#include #define SERVER_API_DEFAULT_PORT 4403 @@ -21,15 +22,15 @@ template class ServerAPI : public StreamAPI, private concurrency::OSTh /// override close to also shutdown the TCP link virtual void close(); + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override; + protected: /// We override this method to prevent publishing EVENT_SERIAL_CONNECTED/DISCONNECTED for wifi links (we want the board to /// stay in the POWERED state to prevent disabling wifi) virtual void onConnectionChanged(bool connected) override {} virtual int32_t runOnce() override; // Check for dropped client connections - - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override; }; /** @@ -42,7 +43,7 @@ template class APIServerPort : public U, private concurrency: * FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to * delegate to the worker. Once coroutines are implemented we can relax this restriction. */ - T *openAPI = NULL; + std::unique_ptr openAPI; #if defined(RAK_4631) || defined(RAK11310) // Track wait time for RAK13800 Ethernet requests int32_t waitTime = 100; diff --git a/src/mesh/api/ethServerAPI.cpp b/src/mesh/api/ethServerAPI.cpp index 10ff06df2..43ed74cf8 100644 --- a/src/mesh/api/ethServerAPI.cpp +++ b/src/mesh/api/ethServerAPI.cpp @@ -17,6 +17,15 @@ void initApiServer(int port) } } +void deInitApiServer() +{ + if (apiPort) { + LOG_INFO("Deinit API server"); + delete apiPort; + apiPort = nullptr; + } +} + ethServerAPI::ethServerAPI(EthernetClient &_client) : ServerAPI(_client) { LOG_INFO("Incoming ethernet connection"); diff --git a/src/mesh/api/ethServerAPI.h b/src/mesh/api/ethServerAPI.h index c616c87be..8f81ee6ff 100644 --- a/src/mesh/api/ethServerAPI.h +++ b/src/mesh/api/ethServerAPI.h @@ -24,4 +24,5 @@ class ethServerPort : public APIServerPort }; void initApiServer(int port = SERVER_API_DEFAULT_PORT); +void deInitApiServer(); #endif diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index a811ec16c..440f7b76a 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -32,6 +32,69 @@ static Periodic *ethEvent; static int32_t reconnectETH() { if (config.network.eth_enabled) { + + // Detect W5100S chip reset by verifying the MAC address register. + // PoE power instability can brownout the W5100S while the MCU keeps running, + // causing all chip registers (MAC, IP, sockets) to revert to defaults. + uint8_t currentMac[6]; + Ethernet.MACAddress(currentMac); + + uint8_t expectedMac[6]; + getMacAddr(expectedMac); + expectedMac[0] &= 0xfe; + + if (memcmp(currentMac, expectedMac, 6) != 0) { + LOG_WARN("W5100S MAC mismatch (chip reset detected), reinitializing Ethernet"); + + syslog.disable(); +#if !MESHTASTIC_EXCLUDE_SOCKETAPI + deInitApiServer(); +#endif +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } +#endif + + ethStartupComplete = false; +#ifndef DISABLE_NTP + ntp_renew = 0; +#endif + +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, LOW); + delay(100); + digitalWrite(PIN_ETHERNET_RESET, HIGH); + delay(100); +#endif + +#ifdef RAK11310 + ETH_SPI_PORT.setSCK(PIN_SPI0_SCK); + ETH_SPI_PORT.setTX(PIN_SPI0_MOSI); + ETH_SPI_PORT.setRX(PIN_SPI0_MISO); + ETH_SPI_PORT.begin(); +#endif + Ethernet.init(ETH_SPI_PORT, PIN_ETHERNET_SS); + + int status = 0; + if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_DHCP) { + status = Ethernet.begin(expectedMac); + } else if (config.network.address_mode == meshtastic_Config_NetworkConfig_AddressMode_STATIC) { + Ethernet.begin(expectedMac, config.network.ipv4_config.ip, config.network.ipv4_config.dns, + config.network.ipv4_config.gateway, config.network.ipv4_config.subnet); + status = 1; + } + + if (status == 0) { + LOG_ERROR("Ethernet re-initialization failed, will retry"); + return 5000; + } + + LOG_INFO("Ethernet reinitialized - IP %u.%u.%u.%u", Ethernet.localIP()[0], Ethernet.localIP()[1], + Ethernet.localIP()[2], Ethernet.localIP()[3]); + } + Ethernet.maintain(); if (!ethStartupComplete) { // Start web server @@ -39,7 +102,6 @@ static int32_t reconnectETH() #ifndef DISABLE_NTP LOG_INFO("Start NTP time client"); - timeClient.begin(); timeClient.setUpdateInterval(60 * 60); // Update once an hour #endif @@ -96,6 +158,7 @@ static int32_t reconnectETH() LOG_ERROR("NTP Update failed"); ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes } + timeClient.end(); // W5100S: release UDP socket for other services } #endif diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index d0bd09800..3dcc241d9 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -39,6 +39,9 @@ PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO) +PB_BIND(meshtastic_SHTXX_config, meshtastic_SHTXX_config, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index d529336c8..58e0356ca 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -229,6 +229,12 @@ typedef struct _meshtastic_SCD30_config { bool soft_reset; } meshtastic_SCD30_config; +typedef struct _meshtastic_SHTXX_config { + /* Accuracy mode (0 = low, 1 = medium, 2 = high) */ + bool has_set_accuracy; + uint32_t set_accuracy; +} meshtastic_SHTXX_config; + typedef struct _meshtastic_SensorConfig { /* SCD4X CO2 Sensor configuration */ bool has_scd4x_config; @@ -239,6 +245,9 @@ typedef struct _meshtastic_SensorConfig { /* SCD30 CO2 Sensor configuration */ bool has_scd30_config; meshtastic_SCD30_config scd30_config; + /* SHTXX temperature and relative humidity sensor configuration */ + bool has_shtxx_config; + meshtastic_SHTXX_config shtxx_config; } meshtastic_SensorConfig; typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; @@ -427,6 +436,7 @@ extern "C" { + /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} @@ -435,10 +445,11 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default} +#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default, false, meshtastic_SHTXX_config_init_default} #define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_default {false, 0, false, 0} #define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SHTXX_config_init_default {false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}} @@ -446,10 +457,11 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero} +#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero, false, meshtastic_SHTXX_config_init_zero} #define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_zero {false, 0, false, 0} #define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_SHTXX_config_init_zero {false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_AdminMessage_InputEvent_event_code_tag 1 @@ -486,9 +498,11 @@ extern "C" { #define meshtastic_SCD30_config_set_altitude_tag 4 #define meshtastic_SCD30_config_set_measurement_interval_tag 5 #define meshtastic_SCD30_config_soft_reset_tag 6 +#define meshtastic_SHTXX_config_set_accuracy_tag 1 #define meshtastic_SensorConfig_scd4x_config_tag 1 #define meshtastic_SensorConfig_sen5x_config_tag 2 #define meshtastic_SensorConfig_scd30_config_tag 3 +#define meshtastic_SensorConfig_shtxx_config_tag 4 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -679,12 +693,14 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_SensorConfig_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \ -X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) +X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) \ +X(a, STATIC, OPTIONAL, MESSAGE, shtxx_config, 4) #define meshtastic_SensorConfig_CALLBACK NULL #define meshtastic_SensorConfig_DEFAULT NULL #define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config #define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config #define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config +#define meshtastic_SensorConfig_shtxx_config_MSGTYPE meshtastic_SHTXX_config #define meshtastic_SCD4X_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ @@ -713,6 +729,11 @@ X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6) #define meshtastic_SCD30_config_CALLBACK NULL #define meshtastic_SCD30_config_DEFAULT NULL +#define meshtastic_SHTXX_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, UINT32, set_accuracy, 1) +#define meshtastic_SHTXX_config_CALLBACK NULL +#define meshtastic_SHTXX_config_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; @@ -724,6 +745,7 @@ extern const pb_msgdesc_t meshtastic_SensorConfig_msg; extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; extern const pb_msgdesc_t meshtastic_SCD30_config_msg; +extern const pb_msgdesc_t meshtastic_SHTXX_config_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg @@ -737,6 +759,7 @@ extern const pb_msgdesc_t meshtastic_SCD30_config_msg; #define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg #define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg #define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg +#define meshtastic_SHTXX_config_fields &meshtastic_SHTXX_config_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size @@ -749,7 +772,8 @@ extern const pb_msgdesc_t meshtastic_SCD30_config_msg; #define meshtastic_SCD30_config_size 27 #define meshtastic_SCD4X_config_size 29 #define meshtastic_SEN5X_config_size 7 -#define meshtastic_SensorConfig_size 69 +#define meshtastic_SHTXX_config_size 6 +#define meshtastic_SensorConfig_size 77 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/apponly.pb.h b/src/mesh/generated/meshtastic/apponly.pb.h index f4c33bd79..ce766878b 100644 --- a/src/mesh/generated/meshtastic/apponly.pb.h +++ b/src/mesh/generated/meshtastic/apponly.pb.h @@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size -#define meshtastic_ChannelSet_size 679 +#define meshtastic_ChannelSet_size 682 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/atak.pb.cpp b/src/mesh/generated/meshtastic/atak.pb.cpp index a0368cf6b..bbafa33e2 100644 --- a/src/mesh/generated/meshtastic/atak.pb.cpp +++ b/src/mesh/generated/meshtastic/atak.pb.cpp @@ -24,6 +24,18 @@ PB_BIND(meshtastic_Contact, meshtastic_Contact, AUTO) PB_BIND(meshtastic_PLI, meshtastic_PLI, AUTO) +PB_BIND(meshtastic_AircraftTrack, meshtastic_AircraftTrack, AUTO) + + +PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2) + + + + + + + + diff --git a/src/mesh/generated/meshtastic/atak.pb.h b/src/mesh/generated/meshtastic/atak.pb.h index 8533bcbf9..c12b042f4 100644 --- a/src/mesh/generated/meshtastic/atak.pb.h +++ b/src/mesh/generated/meshtastic/atak.pb.h @@ -65,6 +65,197 @@ typedef enum _meshtastic_MemberRole { meshtastic_MemberRole_K9 = 8 } meshtastic_MemberRole; +/* CoT how field values. + Represents how the coordinates were generated. */ +typedef enum _meshtastic_CotHow { + /* Unspecified */ + meshtastic_CotHow_CotHow_Unspecified = 0, + /* Human entered */ + meshtastic_CotHow_CotHow_h_e = 1, + /* Machine generated */ + meshtastic_CotHow_CotHow_m_g = 2, + /* Human GPS/INS derived */ + meshtastic_CotHow_CotHow_h_g_i_g_o = 3, + /* Machine relayed (imported from another system/gateway) */ + meshtastic_CotHow_CotHow_m_r = 4, + /* Machine fused (corroborated from multiple sources) */ + meshtastic_CotHow_CotHow_m_f = 5, + /* Machine predicted */ + meshtastic_CotHow_CotHow_m_p = 6, + /* Machine simulated */ + meshtastic_CotHow_CotHow_m_s = 7 +} meshtastic_CotHow; + +/* Well-known CoT event types. + When the type is known, use the enum value for efficient encoding. + For unknown types, set cot_type_id to CotType_Other and populate cot_type_str. */ +typedef enum _meshtastic_CotType { + /* Unknown or unmapped type, use cot_type_str */ + meshtastic_CotType_CotType_Other = 0, + /* a-f-G-U-C: Friendly ground unit combat */ + meshtastic_CotType_CotType_a_f_G_U_C = 1, + /* a-f-G-U-C-I: Friendly ground unit combat infantry */ + meshtastic_CotType_CotType_a_f_G_U_C_I = 2, + /* a-n-A-C-F: Neutral aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_n_A_C_F = 3, + /* a-n-A-C-H: Neutral aircraft civilian helicopter */ + meshtastic_CotType_CotType_a_n_A_C_H = 4, + /* a-n-A-C: Neutral aircraft civilian */ + meshtastic_CotType_CotType_a_n_A_C = 5, + /* a-f-A-M-H: Friendly aircraft military helicopter */ + meshtastic_CotType_CotType_a_f_A_M_H = 6, + /* a-f-A-M: Friendly aircraft military */ + meshtastic_CotType_CotType_a_f_A_M = 7, + /* a-f-A-M-F-F: Friendly aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_f_A_M_F_F = 8, + /* a-f-A-M-H-A: Friendly aircraft military helicopter attack */ + meshtastic_CotType_CotType_a_f_A_M_H_A = 9, + /* a-f-A-M-H-U-M: Friendly aircraft military helicopter utility medium */ + meshtastic_CotType_CotType_a_f_A_M_H_U_M = 10, + /* a-h-A-M-F-F: Hostile aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_h_A_M_F_F = 11, + /* a-h-A-M-H-A: Hostile aircraft military helicopter attack */ + meshtastic_CotType_CotType_a_h_A_M_H_A = 12, + /* a-u-A-C: Unknown aircraft civilian */ + meshtastic_CotType_CotType_a_u_A_C = 13, + /* t-x-d-d: Tasking delete/disconnect */ + meshtastic_CotType_CotType_t_x_d_d = 14, + /* a-f-G-E-S-E: Friendly ground equipment sensor */ + meshtastic_CotType_CotType_a_f_G_E_S_E = 15, + /* a-f-G-E-V-C: Friendly ground equipment vehicle */ + meshtastic_CotType_CotType_a_f_G_E_V_C = 16, + /* a-f-S: Friendly sea */ + meshtastic_CotType_CotType_a_f_S = 17, + /* a-f-A-M-F: Friendly aircraft military fixed-wing */ + meshtastic_CotType_CotType_a_f_A_M_F = 18, + /* a-f-A-M-F-C-H: Friendly aircraft military fixed-wing cargo heavy */ + meshtastic_CotType_CotType_a_f_A_M_F_C_H = 19, + /* a-f-A-M-F-U-L: Friendly aircraft military fixed-wing utility light */ + meshtastic_CotType_CotType_a_f_A_M_F_U_L = 20, + /* a-f-A-M-F-L: Friendly aircraft military fixed-wing liaison */ + meshtastic_CotType_CotType_a_f_A_M_F_L = 21, + /* a-f-A-M-F-P: Friendly aircraft military fixed-wing patrol */ + meshtastic_CotType_CotType_a_f_A_M_F_P = 22, + /* a-f-A-C-H: Friendly aircraft civilian helicopter */ + meshtastic_CotType_CotType_a_f_A_C_H = 23, + /* a-n-A-M-F-Q: Neutral aircraft military fixed-wing drone */ + meshtastic_CotType_CotType_a_n_A_M_F_Q = 24, + /* b-t-f: GeoChat message */ + meshtastic_CotType_CotType_b_t_f = 25, + /* b-r-f-h-c: CASEVAC/MEDEVAC report */ + meshtastic_CotType_CotType_b_r_f_h_c = 26, + /* b-a-o-pan: Ring the bell / alert all */ + meshtastic_CotType_CotType_b_a_o_pan = 27, + /* b-a-o-opn: Troops in contact */ + meshtastic_CotType_CotType_b_a_o_opn = 28, + /* b-a-o-can: Cancel alert */ + meshtastic_CotType_CotType_b_a_o_can = 29, + /* b-a-o-tbl: 911 alert */ + meshtastic_CotType_CotType_b_a_o_tbl = 30, + /* b-a-g: Geofence breach alert */ + meshtastic_CotType_CotType_b_a_g = 31, + /* a-f-G: Friendly ground (generic) */ + meshtastic_CotType_CotType_a_f_G = 32, + /* a-f-G-U: Friendly ground unit (generic) */ + meshtastic_CotType_CotType_a_f_G_U = 33, + /* a-h-G: Hostile ground (generic) */ + meshtastic_CotType_CotType_a_h_G = 34, + /* a-u-G: Unknown ground (generic) */ + meshtastic_CotType_CotType_a_u_G = 35, + /* a-n-G: Neutral ground (generic) */ + meshtastic_CotType_CotType_a_n_G = 36, + /* b-m-r: Route */ + meshtastic_CotType_CotType_b_m_r = 37, + /* b-m-p-w: Route waypoint */ + meshtastic_CotType_CotType_b_m_p_w = 38, + /* b-m-p-s-p-i: Self-position marker */ + meshtastic_CotType_CotType_b_m_p_s_p_i = 39, + /* u-d-f: Freeform shape (line/polygon) */ + meshtastic_CotType_CotType_u_d_f = 40, + /* u-d-r: Rectangle */ + meshtastic_CotType_CotType_u_d_r = 41, + /* u-d-c-c: Circle */ + meshtastic_CotType_CotType_u_d_c_c = 42, + /* u-rb-a: Range/bearing line */ + meshtastic_CotType_CotType_u_rb_a = 43, + /* a-h-A: Hostile aircraft (generic) */ + meshtastic_CotType_CotType_a_h_A = 44, + /* a-u-A: Unknown aircraft (generic) */ + meshtastic_CotType_CotType_a_u_A = 45, + /* a-f-A-M-H-Q: Friendly aircraft military helicopter observation */ + meshtastic_CotType_CotType_a_f_A_M_H_Q = 46, + /* a-f-A-C-F: Friendly aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_f_A_C_F = 47, + /* a-f-A-C: Friendly aircraft civilian (generic) */ + meshtastic_CotType_CotType_a_f_A_C = 48, + /* a-f-A-C-L: Friendly aircraft civilian lighter-than-air */ + meshtastic_CotType_CotType_a_f_A_C_L = 49, + /* a-f-A: Friendly aircraft (generic) */ + meshtastic_CotType_CotType_a_f_A = 50, + /* a-f-A-M-H-C: Friendly aircraft military helicopter cargo */ + meshtastic_CotType_CotType_a_f_A_M_H_C = 51, + /* a-n-A-M-F-F: Neutral aircraft military fixed-wing fighter */ + meshtastic_CotType_CotType_a_n_A_M_F_F = 52, + /* a-u-A-C-F: Unknown aircraft civilian fixed-wing */ + meshtastic_CotType_CotType_a_u_A_C_F = 53, + /* a-f-G-U-C-F-T-A: Friendly ground unit combat forces theater aviation */ + meshtastic_CotType_CotType_a_f_G_U_C_F_T_A = 54, + /* a-f-G-U-C-V-S: Friendly ground unit combat vehicle support */ + meshtastic_CotType_CotType_a_f_G_U_C_V_S = 55, + /* a-f-G-U-C-R-X: Friendly ground unit combat reconnaissance exploitation */ + meshtastic_CotType_CotType_a_f_G_U_C_R_X = 56, + /* a-f-G-U-C-I-Z: Friendly ground unit combat infantry mechanized */ + meshtastic_CotType_CotType_a_f_G_U_C_I_Z = 57, + /* a-f-G-U-C-E-C-W: Friendly ground unit combat engineer construction wheeled */ + meshtastic_CotType_CotType_a_f_G_U_C_E_C_W = 58, + /* a-f-G-U-C-I-L: Friendly ground unit combat infantry light */ + meshtastic_CotType_CotType_a_f_G_U_C_I_L = 59, + /* a-f-G-U-C-R-O: Friendly ground unit combat reconnaissance other */ + meshtastic_CotType_CotType_a_f_G_U_C_R_O = 60, + /* a-f-G-U-C-R-V: Friendly ground unit combat reconnaissance cavalry */ + meshtastic_CotType_CotType_a_f_G_U_C_R_V = 61, + /* a-f-G-U-H: Friendly ground unit headquarters */ + meshtastic_CotType_CotType_a_f_G_U_H = 62, + /* a-f-G-U-U-M-S-E: Friendly ground unit support medical surgical evacuation */ + meshtastic_CotType_CotType_a_f_G_U_U_M_S_E = 63, + /* a-f-G-U-S-M-C: Friendly ground unit support maintenance collection */ + meshtastic_CotType_CotType_a_f_G_U_S_M_C = 64, + /* a-f-G-E-S: Friendly ground equipment sensor (generic) */ + meshtastic_CotType_CotType_a_f_G_E_S = 65, + /* a-f-G-E: Friendly ground equipment (generic) */ + meshtastic_CotType_CotType_a_f_G_E = 66, + /* a-f-G-E-V-C-U: Friendly ground equipment vehicle utility */ + meshtastic_CotType_CotType_a_f_G_E_V_C_U = 67, + /* a-f-G-E-V-C-ps: Friendly ground equipment vehicle public safety */ + meshtastic_CotType_CotType_a_f_G_E_V_C_ps = 68, + /* a-u-G-E-V: Unknown ground equipment vehicle */ + meshtastic_CotType_CotType_a_u_G_E_V = 69, + /* a-f-S-N-N-R: Friendly sea surface non-naval rescue */ + meshtastic_CotType_CotType_a_f_S_N_N_R = 70, + /* a-f-F-B: Friendly force boundary */ + meshtastic_CotType_CotType_a_f_F_B = 71, + /* b-m-p-s-p-loc: Self-position location marker */ + meshtastic_CotType_CotType_b_m_p_s_p_loc = 72, + /* b-i-v: Imagery/video */ + meshtastic_CotType_CotType_b_i_v = 73, + /* b-f-t-r: File transfer request */ + meshtastic_CotType_CotType_b_f_t_r = 74, + /* b-f-t-a: File transfer acknowledgment */ + meshtastic_CotType_CotType_b_f_t_a = 75 +} meshtastic_CotType; + +/* Geopoint and altitude source */ +typedef enum _meshtastic_GeoPointSource { + /* Unspecified */ + meshtastic_GeoPointSource_GeoPointSource_Unspecified = 0, + /* GPS derived */ + meshtastic_GeoPointSource_GeoPointSource_GPS = 1, + /* User entered */ + meshtastic_GeoPointSource_GeoPointSource_USER = 2, + /* Network/external */ + meshtastic_GeoPointSource_GeoPointSource_NETWORK = 3 +} meshtastic_GeoPointSource; + /* Struct definitions */ /* ATAK GeoChat message */ typedef struct _meshtastic_GeoChat { @@ -146,6 +337,95 @@ typedef struct _meshtastic_TAKPacket { } payload_variant; } meshtastic_TAKPacket; +/* Aircraft track information from ADS-B or military air tracking. + Covers the majority of observed real-world CoT traffic. */ +typedef struct _meshtastic_AircraftTrack { + /* ICAO hex identifier (e.g. "AD237C") */ + char icao[8]; + /* Aircraft registration (e.g. "N946AK") */ + char registration[16]; + /* Flight number/callsign (e.g. "ASA864") */ + char flight[16]; + /* ICAO aircraft type designator (e.g. "B39M") */ + char aircraft_type[8]; + /* Transponder squawk code (0-7777 octal) */ + uint16_t squawk; + /* ADS-B emitter category (e.g. "A3") */ + char category[4]; + /* Received signal strength * 10 (e.g. -194 for -19.4 dBm) */ + int32_t rssi_x10; + /* Whether receiver has GPS fix */ + bool gps; + /* CoT host ID for source attribution */ + char cot_host_id[64]; +} meshtastic_AircraftTrack; + +typedef PB_BYTES_ARRAY_T(220) meshtastic_TAKPacketV2_raw_detail_t; +/* ATAK v2 packet with expanded CoT field support and zstd dictionary compression. + Sent on ATAK_PLUGIN_V2 port. The wire payload is: + [1 byte flags][zstd-compressed TAKPacketV2 protobuf] + Flags byte: bits 0-5 = dictionary ID, bits 6-7 = reserved. */ +typedef struct _meshtastic_TAKPacketV2 { + /* Well-known CoT event type enum. + Use CotType_Other with cot_type_str for unknown types. */ + meshtastic_CotType cot_type_id; + /* How the coordinates were generated */ + meshtastic_CotHow how; + /* Callsign */ + char callsign[120]; + /* Team color assignment */ + meshtastic_Team team; + /* Role of the group member */ + meshtastic_MemberRole role; + /* Latitude, multiply by 1e-7 to get degrees in floating point */ + int32_t latitude_i; + /* Longitude, multiply by 1e-7 to get degrees in floating point */ + int32_t longitude_i; + /* Altitude in meters (HAE) */ + int32_t altitude; + /* Speed in cm/s */ + uint32_t speed; + /* Course in degrees * 100 (0-36000) */ + uint16_t course; + /* Battery level 0-100 */ + uint8_t battery; + /* Geopoint source */ + meshtastic_GeoPointSource geo_src; + /* Altitude source */ + meshtastic_GeoPointSource alt_src; + /* Device UID (UUID string or device ID like "ANDROID-xxxx") */ + char uid[48]; + /* Device callsign */ + char device_callsign[120]; + /* Stale time as seconds offset from event time */ + uint16_t stale_seconds; + /* TAK client version string */ + char tak_version[64]; + /* TAK device model */ + char tak_device[32]; + /* TAK platform (ATAK-CIV, WebTAK, etc.) */ + char tak_platform[32]; + /* TAK OS version */ + char tak_os[16]; + /* Connection endpoint */ + char endpoint[32]; + /* Phone number */ + char phone[20]; + /* CoT event type string, only populated when cot_type_id is CotType_Other */ + char cot_type_str[32]; + pb_size_t which_payload_variant; + union { + /* Position report (true = PLI, no extra fields beyond the common ones above) */ + bool pli; + /* ATAK GeoChat message */ + meshtastic_GeoChat chat; + /* Aircraft track data (ADS-B, military air) */ + meshtastic_AircraftTrack aircraft; + /* Generic CoT detail XML for unmapped types */ + meshtastic_TAKPacketV2_raw_detail_t raw_detail; + } payload_variant; +} meshtastic_TAKPacketV2; + #ifdef __cplusplus extern "C" { @@ -160,6 +440,18 @@ extern "C" { #define _meshtastic_MemberRole_MAX meshtastic_MemberRole_K9 #define _meshtastic_MemberRole_ARRAYSIZE ((meshtastic_MemberRole)(meshtastic_MemberRole_K9+1)) +#define _meshtastic_CotHow_MIN meshtastic_CotHow_CotHow_Unspecified +#define _meshtastic_CotHow_MAX meshtastic_CotHow_CotHow_m_s +#define _meshtastic_CotHow_ARRAYSIZE ((meshtastic_CotHow)(meshtastic_CotHow_CotHow_m_s+1)) + +#define _meshtastic_CotType_MIN meshtastic_CotType_CotType_Other +#define _meshtastic_CotType_MAX meshtastic_CotType_CotType_b_f_t_a +#define _meshtastic_CotType_ARRAYSIZE ((meshtastic_CotType)(meshtastic_CotType_CotType_b_f_t_a+1)) + +#define _meshtastic_GeoPointSource_MIN meshtastic_GeoPointSource_GeoPointSource_Unspecified +#define _meshtastic_GeoPointSource_MAX meshtastic_GeoPointSource_GeoPointSource_NETWORK +#define _meshtastic_GeoPointSource_ARRAYSIZE ((meshtastic_GeoPointSource)(meshtastic_GeoPointSource_GeoPointSource_NETWORK+1)) + #define meshtastic_Group_role_ENUMTYPE meshtastic_MemberRole @@ -169,6 +461,14 @@ extern "C" { +#define meshtastic_TAKPacketV2_cot_type_id_ENUMTYPE meshtastic_CotType +#define meshtastic_TAKPacketV2_how_ENUMTYPE meshtastic_CotHow +#define meshtastic_TAKPacketV2_team_ENUMTYPE meshtastic_Team +#define meshtastic_TAKPacketV2_role_ENUMTYPE meshtastic_MemberRole +#define meshtastic_TAKPacketV2_geo_src_ENUMTYPE meshtastic_GeoPointSource +#define meshtastic_TAKPacketV2_alt_src_ENUMTYPE meshtastic_GeoPointSource + + /* Initializer values for message structs */ #define meshtastic_TAKPacket_init_default {0, false, meshtastic_Contact_init_default, false, meshtastic_Group_init_default, false, meshtastic_Status_init_default, 0, {meshtastic_PLI_init_default}} #define meshtastic_GeoChat_init_default {"", false, "", false, ""} @@ -176,12 +476,16 @@ extern "C" { #define meshtastic_Status_init_default {0} #define meshtastic_Contact_init_default {"", ""} #define meshtastic_PLI_init_default {0, 0, 0, 0, 0} +#define meshtastic_AircraftTrack_init_default {"", "", "", "", 0, "", 0, 0, ""} +#define meshtastic_TAKPacketV2_init_default {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", 0, {0}} #define meshtastic_TAKPacket_init_zero {0, false, meshtastic_Contact_init_zero, false, meshtastic_Group_init_zero, false, meshtastic_Status_init_zero, 0, {meshtastic_PLI_init_zero}} #define meshtastic_GeoChat_init_zero {"", false, "", false, ""} #define meshtastic_Group_init_zero {_meshtastic_MemberRole_MIN, _meshtastic_Team_MIN} #define meshtastic_Status_init_zero {0} #define meshtastic_Contact_init_zero {"", ""} #define meshtastic_PLI_init_zero {0, 0, 0, 0, 0} +#define meshtastic_AircraftTrack_init_zero {"", "", "", "", 0, "", 0, 0, ""} +#define meshtastic_TAKPacketV2_init_zero {_meshtastic_CotType_MIN, _meshtastic_CotHow_MIN, "", _meshtastic_Team_MIN, _meshtastic_MemberRole_MIN, 0, 0, 0, 0, 0, 0, _meshtastic_GeoPointSource_MIN, _meshtastic_GeoPointSource_MIN, "", "", 0, "", "", "", "", "", "", "", 0, {0}} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_GeoChat_message_tag 1 @@ -204,6 +508,42 @@ extern "C" { #define meshtastic_TAKPacket_pli_tag 5 #define meshtastic_TAKPacket_chat_tag 6 #define meshtastic_TAKPacket_detail_tag 7 +#define meshtastic_AircraftTrack_icao_tag 1 +#define meshtastic_AircraftTrack_registration_tag 2 +#define meshtastic_AircraftTrack_flight_tag 3 +#define meshtastic_AircraftTrack_aircraft_type_tag 4 +#define meshtastic_AircraftTrack_squawk_tag 5 +#define meshtastic_AircraftTrack_category_tag 6 +#define meshtastic_AircraftTrack_rssi_x10_tag 7 +#define meshtastic_AircraftTrack_gps_tag 8 +#define meshtastic_AircraftTrack_cot_host_id_tag 9 +#define meshtastic_TAKPacketV2_cot_type_id_tag 1 +#define meshtastic_TAKPacketV2_how_tag 2 +#define meshtastic_TAKPacketV2_callsign_tag 3 +#define meshtastic_TAKPacketV2_team_tag 4 +#define meshtastic_TAKPacketV2_role_tag 5 +#define meshtastic_TAKPacketV2_latitude_i_tag 6 +#define meshtastic_TAKPacketV2_longitude_i_tag 7 +#define meshtastic_TAKPacketV2_altitude_tag 8 +#define meshtastic_TAKPacketV2_speed_tag 9 +#define meshtastic_TAKPacketV2_course_tag 10 +#define meshtastic_TAKPacketV2_battery_tag 11 +#define meshtastic_TAKPacketV2_geo_src_tag 12 +#define meshtastic_TAKPacketV2_alt_src_tag 13 +#define meshtastic_TAKPacketV2_uid_tag 14 +#define meshtastic_TAKPacketV2_device_callsign_tag 15 +#define meshtastic_TAKPacketV2_stale_seconds_tag 16 +#define meshtastic_TAKPacketV2_tak_version_tag 17 +#define meshtastic_TAKPacketV2_tak_device_tag 18 +#define meshtastic_TAKPacketV2_tak_platform_tag 19 +#define meshtastic_TAKPacketV2_tak_os_tag 20 +#define meshtastic_TAKPacketV2_endpoint_tag 21 +#define meshtastic_TAKPacketV2_phone_tag 22 +#define meshtastic_TAKPacketV2_cot_type_str_tag 23 +#define meshtastic_TAKPacketV2_pli_tag 30 +#define meshtastic_TAKPacketV2_chat_tag 31 +#define meshtastic_TAKPacketV2_aircraft_tag 32 +#define meshtastic_TAKPacketV2_raw_detail_tag 33 /* Struct field encoding specification for nanopb */ #define meshtastic_TAKPacket_FIELDLIST(X, a) \ @@ -255,12 +595,60 @@ X(a, STATIC, SINGULAR, UINT32, course, 5) #define meshtastic_PLI_CALLBACK NULL #define meshtastic_PLI_DEFAULT NULL +#define meshtastic_AircraftTrack_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, icao, 1) \ +X(a, STATIC, SINGULAR, STRING, registration, 2) \ +X(a, STATIC, SINGULAR, STRING, flight, 3) \ +X(a, STATIC, SINGULAR, STRING, aircraft_type, 4) \ +X(a, STATIC, SINGULAR, UINT32, squawk, 5) \ +X(a, STATIC, SINGULAR, STRING, category, 6) \ +X(a, STATIC, SINGULAR, SINT32, rssi_x10, 7) \ +X(a, STATIC, SINGULAR, BOOL, gps, 8) \ +X(a, STATIC, SINGULAR, STRING, cot_host_id, 9) +#define meshtastic_AircraftTrack_CALLBACK NULL +#define meshtastic_AircraftTrack_DEFAULT NULL + +#define meshtastic_TAKPacketV2_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, cot_type_id, 1) \ +X(a, STATIC, SINGULAR, UENUM, how, 2) \ +X(a, STATIC, SINGULAR, STRING, callsign, 3) \ +X(a, STATIC, SINGULAR, UENUM, team, 4) \ +X(a, STATIC, SINGULAR, UENUM, role, 5) \ +X(a, STATIC, SINGULAR, SFIXED32, latitude_i, 6) \ +X(a, STATIC, SINGULAR, SFIXED32, longitude_i, 7) \ +X(a, STATIC, SINGULAR, SINT32, altitude, 8) \ +X(a, STATIC, SINGULAR, UINT32, speed, 9) \ +X(a, STATIC, SINGULAR, UINT32, course, 10) \ +X(a, STATIC, SINGULAR, UINT32, battery, 11) \ +X(a, STATIC, SINGULAR, UENUM, geo_src, 12) \ +X(a, STATIC, SINGULAR, UENUM, alt_src, 13) \ +X(a, STATIC, SINGULAR, STRING, uid, 14) \ +X(a, STATIC, SINGULAR, STRING, device_callsign, 15) \ +X(a, STATIC, SINGULAR, UINT32, stale_seconds, 16) \ +X(a, STATIC, SINGULAR, STRING, tak_version, 17) \ +X(a, STATIC, SINGULAR, STRING, tak_device, 18) \ +X(a, STATIC, SINGULAR, STRING, tak_platform, 19) \ +X(a, STATIC, SINGULAR, STRING, tak_os, 20) \ +X(a, STATIC, SINGULAR, STRING, endpoint, 21) \ +X(a, STATIC, SINGULAR, STRING, phone, 22) \ +X(a, STATIC, SINGULAR, STRING, cot_type_str, 23) \ +X(a, STATIC, ONEOF, BOOL, (payload_variant,pli,payload_variant.pli), 30) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,chat,payload_variant.chat), 31) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,aircraft,payload_variant.aircraft), 32) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,raw_detail,payload_variant.raw_detail), 33) +#define meshtastic_TAKPacketV2_CALLBACK NULL +#define meshtastic_TAKPacketV2_DEFAULT NULL +#define meshtastic_TAKPacketV2_payload_variant_chat_MSGTYPE meshtastic_GeoChat +#define meshtastic_TAKPacketV2_payload_variant_aircraft_MSGTYPE meshtastic_AircraftTrack + extern const pb_msgdesc_t meshtastic_TAKPacket_msg; extern const pb_msgdesc_t meshtastic_GeoChat_msg; extern const pb_msgdesc_t meshtastic_Group_msg; extern const pb_msgdesc_t meshtastic_Status_msg; extern const pb_msgdesc_t meshtastic_Contact_msg; extern const pb_msgdesc_t meshtastic_PLI_msg; +extern const pb_msgdesc_t meshtastic_AircraftTrack_msg; +extern const pb_msgdesc_t meshtastic_TAKPacketV2_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_TAKPacket_fields &meshtastic_TAKPacket_msg @@ -269,14 +657,18 @@ extern const pb_msgdesc_t meshtastic_PLI_msg; #define meshtastic_Status_fields &meshtastic_Status_msg #define meshtastic_Contact_fields &meshtastic_Contact_msg #define meshtastic_PLI_fields &meshtastic_PLI_msg +#define meshtastic_AircraftTrack_fields &meshtastic_AircraftTrack_msg +#define meshtastic_TAKPacketV2_fields &meshtastic_TAKPacketV2_msg /* Maximum encoded size of messages (where known) */ -#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_TAKPacket_size +#define MESHTASTIC_MESHTASTIC_ATAK_PB_H_MAX_SIZE meshtastic_TAKPacketV2_size +#define meshtastic_AircraftTrack_size 134 #define meshtastic_Contact_size 242 #define meshtastic_GeoChat_size 444 #define meshtastic_Group_size 4 #define meshtastic_PLI_size 31 #define meshtastic_Status_size 3 +#define meshtastic_TAKPacketV2_size 1027 #define meshtastic_TAKPacket_size 705 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp index 52a591f33..c554ca43c 100644 --- a/src/mesh/generated/meshtastic/config.pb.cpp +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -67,6 +67,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index c669de6a8..c82dd5ff5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -318,6 +318,15 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; +typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { + /* FEM_LNA is present but disabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED = 0, + /* FEM_LNA is present and enabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED = 1, + /* FEM_LNA is not present on the device */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT = 2 +} meshtastic_Config_LoRaConfig_FEM_LNA_Mode; + typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { /* Device generates a random PIN that will be shown on the screen of the device for pairing */ meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN = 0, @@ -579,6 +588,8 @@ typedef struct _meshtastic_Config_LoRaConfig { bool ignore_mqtt; /* Sets the ok_to_mqtt bit on outgoing packets */ bool config_ok_to_mqtt; + /* Set where LORA FEM is enabled, disabled, or not present */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode; } meshtastic_Config_LoRaConfig; typedef struct _meshtastic_Config_BluetoothConfig { @@ -698,6 +709,10 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO #define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_FEM_LNA_Mode)(meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT+1)) + #define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1)) @@ -721,6 +736,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_modem_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset #define meshtastic_Config_LoRaConfig_region_ENUMTYPE meshtastic_Config_LoRaConfig_RegionCode +#define meshtastic_Config_LoRaConfig_fem_lna_mode_ENUMTYPE meshtastic_Config_LoRaConfig_FEM_LNA_Mode #define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode @@ -735,7 +751,7 @@ extern "C" { #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_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, 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_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, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #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} #define meshtastic_Config_SessionkeyConfig_init_default {0} @@ -746,7 +762,7 @@ extern "C" { #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_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, 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_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, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #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} #define meshtastic_Config_SessionkeyConfig_init_zero {0} @@ -832,6 +848,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_ignore_incoming_tag 103 #define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104 #define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105 +#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106 #define meshtastic_Config_BluetoothConfig_enabled_tag 1 #define meshtastic_Config_BluetoothConfig_mode_tag 2 #define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3 @@ -983,7 +1000,8 @@ X(a, STATIC, SINGULAR, FLOAT, override_frequency, 14) \ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \ X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \ X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \ -X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) +X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \ +X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) #define meshtastic_Config_LoRaConfig_CALLBACK NULL #define meshtastic_Config_LoRaConfig_DEFAULT NULL @@ -1040,7 +1058,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define meshtastic_Config_BluetoothConfig_size 10 #define meshtastic_Config_DeviceConfig_size 100 #define meshtastic_Config_DisplayConfig_size 36 -#define meshtastic_Config_LoRaConfig_size 85 +#define meshtastic_Config_LoRaConfig_size 88 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 204 #define meshtastic_Config_PositionConfig_size 62 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 1d9e9baf8..1d6cd32f9 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -361,7 +361,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 2426 +#define meshtastic_BackupPreferences_size 2429 #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 e0bd95d40..8425c122a 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -205,7 +205,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_LocalModuleConfig_size -#define meshtastic_LocalConfig_size 751 +#define meshtastic_LocalConfig_size 754 #define meshtastic_LocalModuleConfig_size 820 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 773cc8ed6..fc6931d73 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -304,6 +304,12 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_TBEAM_BPF = 124, /* LilyGo T-Mini E-paper S3 Kit */ meshtastic_HardwareModel_MINI_EPAPER_S3 = 125, + /* LilyGo T-Display S3 Pro LR1121 */ + meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126, + /* Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen. */ + meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 = 127, + /* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio, GPS, button, buzzer, and sensors. */ + meshtastic_HardwareModel_TRACKER_T1000_E_PRO = 128, /* ------------------------------------------------------------------------------------------------------------------------------------------ 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. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index d31daa4b2..a474e5b92 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -140,6 +140,9 @@ typedef enum _meshtastic_PortNum { meshtastic_PortNum_MAP_REPORT_APP = 73, /* PowerStress based monitoring support (for automated power consumption testing) */ meshtastic_PortNum_POWERSTRESS_APP = 74, + /* LoraWAN Payload Transport + ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule */ + meshtastic_PortNum_LORAWAN_BRIDGE = 75, /* Reticulum Network Stack Tunnel App ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface */ meshtastic_PortNum_RETICULUM_TUNNEL_APP = 76, @@ -147,6 +150,14 @@ typedef enum _meshtastic_PortNum { arbitrary telemetry over meshtastic that is not covered by telemetry.proto ENCODING: CayenneLLP */ meshtastic_PortNum_CAYENNE_APP = 77, + /* ATAK Plugin V2 + Portnum for payloads from the official Meshtastic ATAK plugin using + TAKPacketV2 with zstd dictionary compression. */ + meshtastic_PortNum_ATAK_PLUGIN_V2 = 78, + /* GroupAlarm integration + Used for transporting GroupAlarm-related messages between Meshtastic nodes + and companion applications/services. */ + meshtastic_PortNum_GROUPALARM_APP = 112, /* Private applications should use portnums >= 256. To simplify initial development and testing you can use "PRIVATE_APP" in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 8c34cd84c..f48d946a4 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -26,7 +26,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_INA219 = 5, /* High accuracy temperature and pressure */ meshtastic_TelemetrySensorType_BMP280 = 6, - /* High accuracy temperature and humidity */ + /* TODO - REMOVE High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHTC3 = 7, /* High accuracy pressure */ meshtastic_TelemetrySensorType_LPS22 = 8, @@ -36,7 +36,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_QMI8658 = 10, /* 3-Axis magnetic sensor */ meshtastic_TelemetrySensorType_QMC5883L = 11, - /* High accuracy temperature and humidity */ + /* TODO - REMOVE High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHT31 = 12, /* PM2.5 air quality sensor */ meshtastic_TelemetrySensorType_PMSA003I = 13, @@ -46,7 +46,7 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_BMP085 = 15, /* RCWL-9620 Doppler Radar Distance Sensor, used for water level detection */ meshtastic_TelemetrySensorType_RCWL9620 = 16, - /* Sensirion High accuracy temperature and humidity */ + /* TODO - REMOVE Sensirion High accuracy temperature and humidity */ meshtastic_TelemetrySensorType_SHT4X = 17, /* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. */ meshtastic_TelemetrySensorType_VEML7700 = 18, @@ -106,12 +106,14 @@ typedef enum _meshtastic_TelemetrySensorType { meshtastic_TelemetrySensorType_BH1750 = 45, /* HDC1080 Temperature and Humidity Sensor */ meshtastic_TelemetrySensorType_HDC1080 = 46, - /* STH21 Temperature and R. Humidity sensor */ + /* TODO - REMOVE STH21 Temperature and R. Humidity sensor */ meshtastic_TelemetrySensorType_SHT21 = 47, /* Sensirion STC31 CO2 sensor */ meshtastic_TelemetrySensorType_STC31 = 48, /* SCD30 CO2, humidity, temperature sensor */ - meshtastic_TelemetrySensorType_SCD30 = 49 + meshtastic_TelemetrySensorType_SCD30 = 49, + /* SHT family of sensors for temperature and humidity */ + meshtastic_TelemetrySensorType_SHTXX = 50 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -489,8 +491,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD30 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD30+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SHTXX +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SHTXX+1)) diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index 6efdb59b7..ed182ad76 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -27,10 +27,10 @@ class HttpAPI : public PhoneAPI public: HttpAPI() { api_type = TYPE_HTTP; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this private: // Nothing here yet protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; \ No newline at end of file diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h index e7a29a5a7..762afc618 100644 --- a/src/mesh/http/WebServer.h +++ b/src/mesh/http/WebServer.h @@ -5,6 +5,8 @@ #include #include +#if !MESHTASTIC_EXCLUDE_WEBSERVER + void initWebServer(); void createSSLCert(); @@ -24,3 +26,20 @@ class WebServerThread : private concurrency::OSThread }; extern WebServerThread *webServerThread; + +#else +// Stub implementations when web server is excluded +inline void initWebServer() {} +inline void createSSLCert() {} + +class WebServerThread +{ + public: + WebServerThread() {} + uint32_t requestRestart = 0; + void markActivity() {} +}; + +inline WebServerThread *webServerThread = nullptr; + +#endif diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index e4f65aa28..eea7d4efc 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -69,6 +69,22 @@ static inline int get_max_num_nodes() /// Max number of channels allowed #define MAX_NUM_CHANNELS (member_size(meshtastic_ChannelFile, channels) / member_size(meshtastic_ChannelFile, channels[0])) +// Traffic Management module configuration +// Enable per-variant by defining HAS_TRAFFIC_MANAGEMENT=1 in variant.h +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 0 +#endif + +// Cache size for traffic management (number of nodes to track) +// Can be overridden per-variant based on available memory +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#if HAS_TRAFFIC_MANAGEMENT +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 +#else +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 0 +#endif +#endif + /// helper function for encoding a record as a protobuf, any failures to encode are fatal and we will panic /// returns the encoded packet size size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc_t *fields, const void *src_struct); @@ -90,4 +106,4 @@ bool writecb(pb_ostream_t *stream, const uint8_t *buf, size_t count); */ bool is_in_helper(uint32_t n, const uint32_t *array, pb_size_t count); -#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) \ No newline at end of file +#define is_in_repeated(name, n) is_in_helper(n, name, name##_count) diff --git a/src/mesh/raspihttp/PiWebServer.h b/src/mesh/raspihttp/PiWebServer.h index 5a4adedaa..74b094f8c 100644 --- a/src/mesh/raspihttp/PiWebServer.h +++ b/src/mesh/raspihttp/PiWebServer.h @@ -29,12 +29,13 @@ class HttpAPI : public PhoneAPI public: HttpAPI() { api_type = TYPE_HTTP; } + /// Check the current underlying physical link to see if the client is currently connected + virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this + private: // Nothing here yet protected: - /// Check the current underlying physical link to see if the client is currently connected - virtual bool checkIsConnected() override { return true; } // FIXME, be smarter about this }; class PiWebServerThread diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 71bdf488f..493cc5353 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -69,14 +69,16 @@ class UdpMulticastHandler final // FIXME(PORTDUINO): arduino lacks IPAddress::toString() LOG_DEBUG("UDP broadcast from: %s, len=%u", packet.remoteIP().toString().c_str(), packetLength); #endif - meshtastic_MeshPacket mp; + meshtastic_MeshPacket mp = meshtastic_MeshPacket_init_zero; 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) { + // Drop packets with spoofed local origin โ€” no legitimate LAN node should send from=0 or our own nodeNum + if (isFromUs(&mp)) { + LOG_WARN("UDP packet with spoofed local from=0x%x, dropping", mp.from); + return; + } 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)); UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; @@ -105,8 +107,7 @@ class UdpMulticastHandler final LOG_DEBUG("Broadcasting packet over UDP (id=%u)", mp->id); uint8_t buffer[meshtastic_MeshPacket_size]; size_t encodedLength = pb_encode_to_bytes(buffer, sizeof(buffer), &meshtastic_MeshPacket_msg, mp); - udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); - return true; + return udp.writeTo(buffer, encodedLength, udpIpAddress, UDP_MULTICAST_DEFAUL_PORT); } private: @@ -114,4 +115,4 @@ class UdpMulticastHandler final AsyncUDP udp; bool isRunning; }; -#endif // HAS_UDP_MULTICAST \ No newline at end of file +#endif // HAS_UDP_MULTICAST diff --git a/src/meshUtils.h b/src/meshUtils.h index 67446f91f..da3a4593b 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -38,4 +38,10 @@ const std::string vformat(const char *const zcFormat, ...); // Get actual string length for nanopb char array fields. size_t pb_string_length(const char *str, size_t max_len); +/// Calculate 2^n without calling pow() - used for spreading factor and other calculations +inline uint32_t pow_of_2(uint32_t n) +{ + return 1 << n; +} + #define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 419d2b773..9a52a9aff 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -23,7 +23,10 @@ #endif #include "Default.h" +#include "MeshRadio.h" +#include "RadioInterface.h" #include "TypeConversions.h" +#include "mesh/RadioLibInterface.h" #if !MESHTASTIC_EXCLUDE_MQTT #include "mqtt/MQTT.h" @@ -37,7 +40,7 @@ #include "modules/PositionModule.h" #endif -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "motion/AccelerometerThread.h" #endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ @@ -196,10 +199,35 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta handleSetOwner(r->set_owner); break; - case meshtastic_AdminMessage_set_config_tag: + case meshtastic_AdminMessage_set_config_tag: { LOG_DEBUG("Client set config"); - handleSetConfig(r->set_config); + + // Non-LoRa configs need no further validation. + if (r->set_config.which_payload_variant != meshtastic_Config_lora_tag) { + LOG_DEBUG("Non-LoRa config, applying directly"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + // Only LORA_24 requires hardware capability validation. + if (r->set_config.payload_variant.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { + LOG_DEBUG("LoRa config, region is not LORA_24, applying directly"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + // Hardware supports 2.4 GHz โ€” apply the config. + // Fail closed: null instance is treated as incapable. + if (RadioLibInterface::instance && RadioLibInterface::instance->wideLora()) { + LOG_DEBUG("LORA_24 requested, radio hardware supports 2.4 GHz, applying"); + handleSetConfig(r->set_config, fromOthers); + break; + } + + LOG_WARN("Radio hardware does not support 2.4 GHz; rejecting LORA_24 region"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); break; + } case meshtastic_AdminMessage_set_module_config_tag: LOG_DEBUG("Client set module config"); @@ -625,7 +653,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) } } -void AdminModule::handleSetConfig(const meshtastic_Config &c) +void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) { auto changes = SEGMENT_CONFIG; auto existingRole = config.device.role; @@ -636,7 +664,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) case meshtastic_Config_device_tag: LOG_INFO("Set config: Device"); config.has_device = true; -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true && accelerometerThread->enabled == false) { config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press; @@ -652,9 +681,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) } config.device = c.payload_variant.device; if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE && - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) { + (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - const char *warning = "Rebroadcast mode can't be set to NONE for a router"; + const char *warning = "Rebroadcast mode can't be set to NONE for a router role"; LOG_WARN(warning); sendWarning(warning); } @@ -737,7 +767,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { config.bluetooth.enabled = false; } -#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && \ + !MESHTASTIC_EXCLUDE_ACCELEROMETER if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && accelerometerThread->enabled == false) { config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion; @@ -756,33 +787,66 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) LOG_INFO("Set config: LoRa"); config.has_lora = true; - if (validatedLora.coding_rate < 4 || validatedLora.coding_rate > 8) { - LOG_WARN("Invalid coding_rate %d, setting to 5", validatedLora.coding_rate); - validatedLora.coding_rate = 5; + if (validatedLora.coding_rate != clampCodingRate(validatedLora.coding_rate)) { + LOG_WARN("Invalid coding_rate %d, setting to %d", validatedLora.coding_rate, LORA_CR_DEFAULT); + validatedLora.coding_rate = LORA_CR_DEFAULT; } - if (validatedLora.spread_factor < 7 || validatedLora.spread_factor > 12) { - LOG_WARN("Invalid spread_factor %d, setting to 11", validatedLora.spread_factor); - validatedLora.spread_factor = 11; + if (validatedLora.spread_factor != clampSpreadFactor(validatedLora.spread_factor)) { + LOG_WARN("Invalid spread_factor %d, setting to %d", validatedLora.spread_factor, LORA_SF_DEFAULT); + validatedLora.spread_factor = LORA_SF_DEFAULT; } - if (validatedLora.bandwidth == 0) { - int originalBandwidth = validatedLora.bandwidth; - validatedLora.bandwidth = myRegion->wideLora ? 800 : 250; - LOG_WARN("Invalid bandwidth %d, setting to default", originalBandwidth); + // If we're setting a new region, check the region is valid and then init the region or discard the change + if (validatedLora.region != myRegion->code) { + // Region has changed so check whether it is valid for e.g. licensing conditions and if the lora config is valid + if (RadioInterface::validateConfigRegion(validatedLora) && RadioInterface::validateConfigLora(validatedLora)) { + // If we're setting region for the first time, init the region and regenerate the keys + if (isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { +#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) + if (crypto) { + crypto->ensurePkiKeys(config.security, owner); + } +#endif + // new region is valid and we're coming from an unset region, so enable tx + validatedLora.tx_enabled = true; + } + // If we're unsetting the region for some reason, disable tx + if (!isRegionUnset && validatedLora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + validatedLora.tx_enabled = false; + } + // Ensure initRegion() uses the newly validated region + config.lora.region = validatedLora.region; + initRegion(); + if (myRegion->dutyCycle < 100) { + validatedLora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { + // Default root is in use, so subscribe to the appropriate MQTT topic for this region + sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); + } + changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; + } else { + // Region validation has failed, so just copy all of the old config over the new config + validatedLora = oldLoraConfig; + } + } // end of new region handling + + if (!RadioInterface::validateConfigLora(validatedLora)) { + if (fromOthers) { + LOG_WARN("Invalid LoRa config received from another node, rejecting changes"); + // modem_preset set to use the old setting if the check fails + validatedLora.modem_preset = oldLoraConfig.modem_preset; + } else { + LOG_WARN("Invalid LoRa config received from client, using corrected values"); + RadioInterface::clampConfigLora(validatedLora); + } + // use_preset and bandwidth are coerced into valid values by the check. } - // If no lora radio parameters change, don't need to reboot - if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region && - oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth && - oldLoraConfig.spread_factor == validatedLora.spread_factor && - oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power && - oldLoraConfig.frequency_offset == validatedLora.frequency_offset && - oldLoraConfig.override_frequency == validatedLora.override_frequency && - oldLoraConfig.channel_num == validatedLora.channel_num && - oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) { - requiresReboot = false; - } + // All LoRa radio changes apply live via configChanged observer โ†’ reconfigure(). + // reconfigure() puts the radio in standby, reprograms all modem parameters, and restarts receive. + requiresReboot = false; #if defined(ARCH_PORTDUINO) // If running on portduino and using SimRadio, do not require reboot @@ -799,52 +863,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) digitalWrite(RF95_FAN_EN, HIGH ^ 0); } #endif - config.lora = validatedLora; - // If we're setting region for the first time, init the region and regenerate the keys - if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { -#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } + +#if HAS_LORA_FEM + // Apply FEM LNA mode from config (only meaningful on hardware that supports it) + // Note that a rejected lora config will revert this as well. + if (loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED); + } else if (validatedLora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) { + // Hardware FEM does not support LNA control; normalize stored config to match actual capability + LOG_WARN("FEM LNA mode configured but current FEM does not support LNA control; normalizing to NOT_PRESENT"); + validatedLora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT; + } #endif - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - // Compare the entire string, we are sure of the length as a topic has never been set - if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) { - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } - } - if (config.lora.region != myRegion->code) { - // Region has changed so check whether there is a regulatory one we should be using instead. - // Additionally as a side-effect, assume a new value under myRegion - initRegion(); - if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) { - // Default root is in use, so subscribe to the appropriate MQTT topic for this region - sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name); - } + config.lora = validatedLora; // Finally, return the validated config back to the main config - changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG; - } break; } case meshtastic_Config_bluetooth_tag: @@ -892,10 +925,10 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) } if (requiresReboot && !hasOpenEditTransaction) { disableBluetooth(); - } + } // end of switch case which_payload_variant saveChanges(changes, requiresReboot); -} +} // end of handleSetConfig bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { @@ -1001,6 +1034,11 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) moduleConfig.statusmessage = c.payload_variant.statusmessage; shouldReboot = false; break; + case meshtastic_ModuleConfig_traffic_management_tag: + LOG_INFO("Set module config: Traffic Management"); + moduleConfig.has_traffic_management = true; + moduleConfig.traffic_management = c.payload_variant.traffic_management; + break; } saveChanges(SEGMENT_MODULECONFIG, shouldReboot); return true; @@ -1115,78 +1153,85 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default; if (req.decoded.want_response) { + const char *configName = "?"; switch (configType) { case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: - LOG_INFO("Get module config: MQTT"); + configName = "MQTT"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt; break; case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: - LOG_INFO("Get module config: Serial"); + configName = "Serial"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag; res.get_module_config_response.payload_variant.serial = moduleConfig.serial; break; case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: - LOG_INFO("Get module config: External Notification"); + configName = "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; break; case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: - LOG_INFO("Get module config: Store & Forward"); + configName = "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; break; case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: - LOG_INFO("Get module config: Range Test"); + configName = "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; break; case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: - LOG_INFO("Get module config: Telemetry"); + configName = "Telemetry"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry; break; case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: - LOG_INFO("Get module config: Canned Message"); + configName = "Canned Message"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message; break; case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: - LOG_INFO("Get module config: Audio"); + configName = "Audio"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag; res.get_module_config_response.payload_variant.audio = moduleConfig.audio; break; case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: - LOG_INFO("Get module config: Remote Hardware"); + configName = "Remote Hardware"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware; break; case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: - LOG_INFO("Get module config: Neighbor Info"); + configName = "Neighbor Info"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info; break; case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: - LOG_INFO("Get module config: Detection Sensor"); + configName = "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; break; case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: - LOG_INFO("Get module config: Ambient Lighting"); + configName = "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; break; case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: - LOG_INFO("Get module config: Paxcounter"); + configName = "Paxcounter"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; break; case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG: - LOG_INFO("Get module config: StatusMessage"); + configName = "StatusMessage"; res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag; res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage; break; + case meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG: + configName = "Traffic Management"; + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag; + res.get_module_config_response.payload_variant.traffic_management = moduleConfig.traffic_management; + break; } + LOG_INFO("Get module config: %s", configName); // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. // So even if we internally use 0 to represent 'use default' we still need to send the value we are @@ -1369,23 +1414,17 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { // Validate ham parameters before setting since this would bypass validation in the owner struct - if (*p.call_sign) { - const char *start = p.call_sign; - // Skip all whitespace - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); - return; - } - } - if (*p.short_name) { - const char *start = p.short_name; - while (*start && isspace((unsigned char)*start)) - start++; - if (*start == '\0') { - LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); - return; + const char *fieldsToCheck[] = {p.call_sign, p.short_name}; + const char *fieldNames[] = {"call_sign", "short_name"}; + for (int i = 0; i < 2; i++) { + if (*fieldsToCheck[i]) { + const char *start = fieldsToCheck[i]; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham %s: must contain at least 1 non-whitespace character", fieldNames[i]); + return; + } } } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index c446887b3..5c690abbd 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -60,7 +60,11 @@ class AdminModule : public ProtobufModule, public Obser */ void handleSetOwner(const meshtastic_User &o); void handleSetChannel(const meshtastic_Channel &cc); - void handleSetConfig(const meshtastic_Config &c); + + protected: + void handleSetConfig(const meshtastic_Config &c, bool fromOthers); + + private: bool handleSetModuleConfig(const meshtastic_ModuleConfig &c); void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c7eb1b15b..65e903134 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,10 +13,12 @@ #include "buzz.h" #include "detect/ScanI2C.h" #include "gps/RTC.h" +#include "graphics/EmoteRenderer.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/MessageRenderer.h" #include "graphics/draw/NotificationRenderer.h" +#include "graphics/draw/UIRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "input/SerialKeyboard.h" @@ -45,71 +47,6 @@ extern MessageStore messageStore; // Remove Canned message screen if no action is taken for some milliseconds #define INACTIVATE_AFTER_MS 20000 -// Tokenize a message string into emote/text segments -static std::vector> tokenizeMessageWithEmotes(const char *msg) -{ - std::vector> tokens; - int msgLen = strlen(msg); - int pos = 0; - while (pos < msgLen) { - const graphics::Emote *foundEmote = nullptr; - int foundLen = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - int labelLen = strlen(label); - if (labelLen == 0) - continue; - if (strncmp(msg + pos, label, labelLen) == 0) { - if (!foundEmote || labelLen > foundLen) { - foundEmote = &graphics::emotes[j]; - foundLen = labelLen; - } - } - } - if (foundEmote) { - tokens.emplace_back(true, String(foundEmote->label)); - pos += foundLen; - } else { - // Find next emote - int nextEmote = msgLen; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *found = strstr(msg + pos, label); - if (found && (found - msg) < nextEmote) { - nextEmote = found - msg; - } - } - int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); - if (textLen > 0) { - tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); - pos += textLen; - } else { - break; - } - } - } - return tokens; -} - -// Render a single emote token centered vertically on a row -static void renderEmote(OLEDDisplay *display, int &nextX, int lineY, int rowHeight, const String &label) -{ - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (label == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; - } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; // vertically center the emote - display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; // spacing between tokens - } -} - namespace graphics { extern int bannerSignalBars; @@ -132,7 +69,7 @@ CannedMessageModule::CannedMessageModule() this->loadProtoForModule(); if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE) { LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; + this->updateState(CANNED_MESSAGE_RUN_STATE_DISABLED); disable(); } else { LOG_INFO("CannedMessageModule is enabled"); @@ -164,8 +101,7 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan currentMessageIndex = selectDestination; // This triggers the canned message list - runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -194,8 +130,7 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t lastChannel = channel; lastDestSet = true; - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -266,19 +201,20 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (graphics::currentResolution == graphics::ScreenResolution::High) { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } + (void)buffer; + + char header[96]; + if (this->dest == NODENUM_BROADCAST) { + const char *channelName = channels.getName(this->channel); + snprintf(header, sizeof(header), "To: #%s", channelName ? channelName : "?"); } else { - if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); - } else { - display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); - } + snprintf(header, sizeof(header), "To: @%s", getNodeName(this->dest)); } + + const int maxWidth = std::max(0, display->getWidth() - x); + char truncatedHeader[96]; + graphics::UIRenderer::truncateStringWithEmotes(display, header, truncatedHeader, sizeof(truncatedHeader), maxWidth); + graphics::UIRenderer::drawStringWithEmotes(display, x, y, truncatedHeader, FONT_HEIGHT_SMALL, 1, false); } void CannedMessageModule::resetSearch() @@ -372,6 +308,92 @@ bool CannedMessageModule::isCharInputAllowed() const { return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } + +static int getRowHeightForEmoteText(const char *text, int minimumHeight, int emoteSpacing = 2) +{ + // Grow the row only when an emote is taller than the font. + const auto metrics = + graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", 0, graphics::emotes, graphics::numEmotes, emoteSpacing); + return std::max(minimumHeight, metrics.tallestHeight + 2); +} + +static void drawCenteredEmoteText(OLEDDisplay *display, int x, int y, int rowHeight, const char *text, int emoteSpacing = 2) +{ + // Center mixed text and emotes inside the row height. + const auto metrics = graphics::EmoteRenderer::analyzeLine(nullptr, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, + graphics::numEmotes, emoteSpacing); + const int contentHeight = std::max(FONT_HEIGHT_SMALL, metrics.tallestHeight); + const int drawY = y + ((rowHeight - contentHeight) / 2); + graphics::EmoteRenderer::drawStringWithEmotes(display, x, drawY, text ? text : "", FONT_HEIGHT_SMALL, graphics::emotes, + graphics::numEmotes, emoteSpacing, false); +} + +static size_t firstWrappedTokenLen(const char *text) +{ + // Fall back to one full emote or one UTF-8 glyph when width is tiny. + if (!text || !*text) + return 0; + + const size_t textLen = strlen(text); + size_t matchLen = 0; + if (graphics::EmoteRenderer::findEmoteAt(text, textLen, 0, matchLen, graphics::emotes, graphics::numEmotes)) + return matchLen; + + return graphics::EmoteRenderer::utf8CharLen(static_cast(text[0])); +} + +static void drawWrappedEmoteText(OLEDDisplay *display, int x, int y, const char *text, int maxWidth, int minimumRowHeight, + int emoteSpacing = 2) +{ + // Wrap onto multiple rows without splitting emotes. + if (!display || !text || maxWidth <= 0) + return; + + constexpr size_t kLineBufferSize = 256; + char lineBuffer[kLineBufferSize]; + const size_t textLen = strlen(text); + size_t offset = 0; + int yCursor = y; + + while (offset < textLen) { + size_t copied = graphics::EmoteRenderer::truncateToWidth(display, text + offset, lineBuffer, sizeof(lineBuffer), maxWidth, + "", graphics::emotes, graphics::numEmotes, emoteSpacing); + size_t consumed = copied; + + if (copied == 0) { + consumed = firstWrappedTokenLen(text + offset); + if (consumed == 0) + break; + + const size_t fallbackLen = std::min(consumed, sizeof(lineBuffer) - 1); + memcpy(lineBuffer, text + offset, fallbackLen); + lineBuffer[fallbackLen] = '\0'; + consumed = fallbackLen; + } else if (text[offset + copied] != '\0') { + // Prefer wrapping at the last space when a full line does not fit. + size_t lastSpace = copied; + while (lastSpace > 0 && lineBuffer[lastSpace - 1] != ' ') + --lastSpace; + + if (lastSpace > 0) { + consumed = lastSpace; + while (consumed > 0 && lineBuffer[consumed - 1] == ' ') + --consumed; + lineBuffer[consumed] = '\0'; + } + } + + if (lineBuffer[0]) { + const int rowHeight = getRowHeightForEmoteText(lineBuffer, minimumRowHeight, emoteSpacing); + drawCenteredEmoteText(display, x, yCursor, rowHeight, lineBuffer, emoteSpacing); + yCursor += rowHeight; + } + + offset += std::max(consumed, 1); + while (offset < textLen && text[offset] == ' ') + ++offset; + } +} /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. @@ -390,11 +412,10 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) // Matrix keypad: If matrix key, trigger action select for canned message if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, true); payload = INPUT_BROKER_MATRIXKEY; currentMessageIndex = event->kbchar - 1; lastTouchMillis = millis(); - requestFocus(); return 1; } @@ -432,8 +453,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } // Printable char (ASCII) opens free text compose if (event->kbchar >= 32 && event->kbchar <= 126) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -457,6 +477,20 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) return 0; } +void CannedMessageModule::updateState(cannedMessageModuleRunState newState, bool shouldRequestFocus) +{ + runState = newState; + if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + inputBroker->menuMode = + false; // Allow any key input to be sent to the message composer instead of being interpreted as menu navigation + } else { + inputBroker->menuMode = true; // Re-enable menu navigation for destination selection + } + if (shouldRequestFocus) { + requestFocus(); + } +} + bool CannedMessageModule::isUpEvent(const InputEvent *event) { return event->inputEvent == INPUT_BROKER_UP || @@ -481,15 +515,16 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) if (event->kbchar != 0x09) return false; - runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + const cannedMessageModuleRunState targetState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; - // RESTORE THIS! - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + if (targetState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) updateDestinationSelectionList(); - requestFocus(); + + updateState(targetState, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -595,7 +630,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event } } - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; screen->forceDisplay(true); return 1; @@ -603,7 +638,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event // CANCEL if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT, true); returnToCannedList = false; searchQuery = ""; @@ -634,7 +669,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Handle Cancel key: go inactive, clear UI state if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -652,10 +687,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Handle up/down navigation if (isUp && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_UP); handled = true; } else if (isDown && messagesCount > 0) { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_DOWN); handled = true; } else if (isSelect) { const char *current = messages[currentMessageIndex]; @@ -663,7 +698,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Select Destination] triggers destination selection UI if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; - runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + updateState(CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, true); destIndex = 0; scrollIndex = 0; updateDestinationSelectionList(); // Make sure list is fresh @@ -674,7 +709,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Exit] returns to the main/inactive screen if (strcmp(current, "[Exit]") == 0) { // Set runState to inactive so we return to main UI - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); currentMessageIndex = -1; // Notify UI to regenerate frame set and redraw @@ -688,8 +723,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // [Free Text] triggers the free text input (virtual keyboard) #if defined(USE_VIRTUAL_KEYBOARD) if (strcmp(current, "[-- Free Text --]") == 0) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); @@ -708,7 +742,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (!text.empty()) { this->freetext = text.c_str(); this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); currentMessageIndex = -1; UIFrameEvent e; @@ -725,7 +759,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo graphics::NotificationRenderer::resetBanner(); // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; @@ -755,12 +789,12 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { this->currentMessageIndex = savedIndex; this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); this->setIntervalFromNow(0); }); #else payload = runState; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); #endif // Do not immediately set runState; wait for confirmation handled = true; @@ -786,7 +820,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) #if defined(USE_VIRTUAL_KEYBOARD) // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_LEFT) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -832,7 +866,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) } // Touch enter/submit else if (keyTapped == "โ†ต") { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); // Send the message! payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; shift = false; @@ -858,8 +892,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) // All hardware keys fall through to here (CardKB, physical, etc.) if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { - runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; - requestFocus(); + updateState(CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); screen->forceDisplay(); return true; } @@ -876,7 +909,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + updateState(CANNED_MESSAGE_RUN_STATE_ACTION_SELECT); lastTouchMillis = millis(); runOnce(); return true; @@ -911,7 +944,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) // Cancel (dismiss freetext screen) if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG || (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() == 0)) { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); freetext = ""; cursor = 0; payload = 0; @@ -979,14 +1012,14 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); } cursor += emoteInsert.length(); - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } // Cancel returns to freetext if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + updateState(CANNED_MESSAGE_RUN_STATE_FREETEXT, true); screen->forceDisplay(); return 1; } @@ -1072,12 +1105,14 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha } else { sm.dest = dest; sm.type = MessageType::DM_TO_US; - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { + // Only add as favorite if our role is not router-like (ROUTER, ROUTER_LATE, CLIENT_BASE) + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && + config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { LOG_INFO("Proactively adding %x as favorite node", dest); nodeDB->set_favorite(true, dest); } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", dest); + LOG_DEBUG("Not favoriting node %x because role is router-like", dest); } } sm.ackStatus = AckStatus::NONE; @@ -1093,9 +1128,8 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha playComboTune(); - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE); this->payload = wantReplies ? 1 : 0; - requestFocus(); // Tell Screen to switch to TextMessage frame via UIFrameEvent UIFrameEvent e; @@ -1146,7 +1180,7 @@ int32_t CannedMessageModule::runOnce() } else { // Empty message, just go inactive LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } UIFrameEvent e; @@ -1163,7 +1197,7 @@ int32_t CannedMessageModule::runOnce() this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; @@ -1172,7 +1206,7 @@ int32_t CannedMessageModule::runOnce() } // Handle SENDING_ACTIVE state transition after virtual keyboard message else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; @@ -1184,7 +1218,7 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); // Clean up virtual keyboard if it exists during timeout if (graphics::NotificationRenderer::virtualKeyboard) { @@ -1198,7 +1232,7 @@ int32_t CannedMessageModule::runOnce() if (this->payload == 0) { // [Exit] button pressed - return to inactive state LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); @@ -1213,15 +1247,15 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); return INT32_MAX; // don't fall back into canned list } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } else { if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); return INT32_MAX; } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { @@ -1240,12 +1274,12 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); // Now deactivate this module - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); return INT32_MAX; // don't fall back into canned list } } else { - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_INACTIVE); } } // fallback clean-up if nothing above returned @@ -1279,14 +1313,14 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = getPrevIndex(); this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); this->freetext = ""; this->cursor = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + this->updateState(CANNED_MESSAGE_RUN_STATE_ACTIVE); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { @@ -1674,55 +1708,51 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O int xOffset = 0; int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64] = ""; + std::string entryText; // Draw Channels First if (itemIndex < numActiveChannels) { uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); + const char *channelName = channels.getName(channelIndex); + entryText = std::string("#") + (channelName ? channelName : "?"); } // Then Draw Nodes else { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && node->user.long_name) { - strncpy(entryText, node->user.long_name, sizeof(entryText) - 1); - entryText[sizeof(entryText) - 1] = '\0'; + if (node) { + if (display->getWidth() <= 64) { + entryText = node->user.short_name; + } else if (node->user.long_name[0]) { + entryText = node->user.long_name; + } else { + entryText = node->user.short_name; + } } + int availWidth = display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 40 : 20) - ((node && node->is_favorite) ? 10 : 0); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(entryText); - while (entryText[0] && display->getStringWidth(entryText) > availWidth) { - entryText[strlen(entryText) - 1] = '\0'; - } - if (strlen(entryText) < origLen) { - strcat(entryText, "..."); - } + char truncatedEntry[96]; + graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), + availWidth); + entryText = truncatedEntry; // Prepend "* " if this is a favorite if (node && node->is_favorite) { - size_t len = strlen(entryText); - if (len + 2 < sizeof(entryText)) { - memmove(entryText + 2, entryText, len + 1); - entryText[0] = '*'; - entryText[1] = ' '; - } - } - if (node) { - if (display->getWidth() <= 64) { - snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); - } + entryText = "* " + entryText; } + graphics::UIRenderer::truncateStringWithEmotes(display, entryText.c_str(), truncatedEntry, sizeof(truncatedEntry), + availWidth); + entryText = truncatedEntry; } } - if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) - strcpy(entryText, "?"); + if (entryText.empty() || entryText == "Unknown") + entryText = "?"; // Highlight background (if selected) if (itemIndex == destIndex) { @@ -1732,7 +1762,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O } // Draw entry text - display->drawString(xOffset + 2, yOffset, entryText); + graphics::UIRenderer::drawStringWithEmotes(display, xOffset + 2, yOffset, entryText.c_str(), FONT_HEIGHT_SMALL, 1, false); display->setColor(WHITE); // Draw key icon (after highlight) @@ -1773,15 +1803,10 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla { const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height const int headerMargin = 2; // Extra pixels below header - const int labelGap = 6; + const int labelGap = 4; const int bitmapGapX = 4; - // Find max emote height (assume all same, or precalculated) - int maxEmoteHeight = 0; - for (int i = 0; i < graphics::numEmotes; ++i) - if (graphics::emotes[i].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[i].height; - + const int maxEmoteHeight = graphics::EmoteRenderer::maxEmoteHeight(); const int rowHeight = maxEmoteHeight + 2; // Place header at top, then compute start of emote list @@ -1828,14 +1853,16 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla display->setColor(BLACK); } - // Emote bitmap (left), 1px margin from highlight bar top - int emoteY = rowY + 1; - display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + // Emote bitmap (left), centered inside the row + int labelStartX = x + bitmapGapX; + const int emoteY = rowY + ((rowHeight - emote.height) / 2); + display->drawXbm(labelStartX, emoteY, emote.width, emote.height, emote.bitmap); + labelStartX += emote.width; // Emote label (right of bitmap) display->setFont(FONT_MEDIUM); int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); - display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + display->drawString(labelStartX + labelGap, labelY, emote.label); if (emoteIdx == emotePickerIndex) display->setColor(WHITE); @@ -1995,91 +2022,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st { int inputY = 0 + y + FONT_HEIGHT_SMALL; String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); - - // Tokenize input into (isEmote, token) pairs - const char *msg = msgWithCursor.c_str(); - std::vector> tokens = tokenizeMessageWithEmotes(msg); - - // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) - std::vector>> lines; - std::vector> currentLine; - int lineWidth = 0; - int maxWidth = display->getWidth(); - for (auto &token : tokens) { - if (token.first) { - // Emote - int tokenWidth = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - tokenWidth = graphics::emotes[j].width + 2; - break; - } - } - if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back(token); - lineWidth += tokenWidth; - } else { - // Text: split by words and wrap inside word if needed - String text = token.second; - int pos = 0; - while (pos < static_cast(text.length())) { - // Find next space (or end) - int spacePos = text.indexOf(' ', pos); - int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space - String word = text.substring(pos, endPos); - int wordWidth = display->getStringWidth(word); - - if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - // If word itself too big, split by character - if (wordWidth > maxWidth) { - uint16_t charPos = 0; - while (charPos < word.length()) { - String oneChar = word.substring(charPos, charPos + 1); - int charWidth = display->getStringWidth(oneChar); - if (lineWidth + charWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back({false, oneChar}); - lineWidth += charWidth; - charPos++; - } - } else { - currentLine.push_back({false, word}); - lineWidth += wordWidth; - } - pos = endPos; - } - } - } - if (!currentLine.empty()) - lines.push_back(currentLine); - - // Draw lines with emotes - int rowHeight = FONT_HEIGHT_SMALL; - int yLine = inputY; - for (const auto &line : lines) { - int nextX = x; - for (const auto &token : line) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, yLine, rowHeight, token.second); - } else { - display->drawString(nextX, yLine, token.second); - nextX += display->getStringWidth(token.second); - } - } - yLine += rowHeight; - } + drawWrappedEmoteText(display, x, inputY, msgWithCursor.c_str(), display->getWidth() - x, FONT_HEIGHT_SMALL); } #endif return; @@ -2094,7 +2037,6 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; - std::vector rowHeights; int _visibleRows; // Draw header (To: ...) @@ -2110,36 +2052,15 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st : 0; int countRows = std::min(messagesCount, _visibleRows); - // Build per-row max height based on all emotes in line - for (int i = 0; i < countRows; i++) { - const char *msg = getMessageByIndex(topMsg + i); - int maxEmoteHeight = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *search = msg; - while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[j].height; - search += strlen(label); // Advance past this emote - } - } - rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); - } - // Draw all message rows with multi-emote support int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; int lineY = yCursor; const char *msg = getMessageByIndex(msgIdx); - int rowHeight = rowHeights[vis]; + int rowHeight = getRowHeightForEmoteText(msg, baseRowSpacing); bool _highlight = (msgIdx == currentMessageIndex); - // Multi-emote tokenization - std::vector> tokens = tokenizeMessageWithEmotes(msg); - // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -2156,17 +2077,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int nextX = x + (_highlight ? 2 : 0); #endif - // Draw all tokens left to right - for (const auto &token : tokens) { - if (token.first) { - // Emote rendering centralized in helper - renderEmote(display, nextX, lineY, rowHeight, token.second); - } else { - // Text - display->drawString(nextX, lineY + textYOffset, token.second); - nextX += display->getStringWidth(token.second); - } - } + if (msg && *msg) + drawCenteredEmoteText(display, nextX, lineY, rowHeight, msg); #ifndef USE_EINK if (_highlight) display->setColor(WHITE); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 65715dd22..f6cb4d011 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -183,6 +183,8 @@ class CannedMessageModule : public SinglePortModule, public Observable 0) digitalWrite(output, (moduleConfig.external_notification.active ? on : !on)); +#ifdef PCA_LED_NOTIFICATION + io.digitalWrite(PCA_LED_NOTIFICATION, on); + +#endif break; } @@ -350,12 +354,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP { // Trigger external notification if enabled and not muted; isSilenced is from temporary mute toggles if (moduleConfig.external_notification.enabled && !isSilenced) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 56); - drv.setWaveform(2, 0); - drv.go(); -#endif if (!isFromUs(&mp)) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. auto &p = mp.decoded; @@ -380,60 +378,69 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP const bool buzzerModeIsDirectOnly = (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY); - if (containsBell || !is_muted) { - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message || - moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra || - ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz())) { - nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout - ? (moduleConfig.external_notification.nag_timeout * 1000) - : moduleConfig.external_notification.output_ms); - LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); - isNagging = true; - } + // Each output evaluates its own alert condition independently: + // alert_bell_* fires only when a bell character is present. + // alert_message_* fires on any non-muted message. - if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_message) { - LOG_INFO("externalNotificationModule - Notification Module or Bell"); - setExternalState(0, true); - } + // Alert when receiving a bell = alertBell: true + // Alert when receiving a message = alertMessage: true + const bool genericShouldAlert = (moduleConfig.external_notification.alert_bell && containsBell) || + (moduleConfig.external_notification.alert_message && !is_muted); - if (moduleConfig.external_notification.alert_bell_vibra || - moduleConfig.external_notification.alert_message_vibra) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Vibra)"); - setExternalState(1, true); - } + // Alert GPIO Vibra when receiving a bell = alertBellVibra: true + // Alert GPIO Vibra when receiving a message = alertMessageVibra: true + const bool vibraShouldAlert = (moduleConfig.external_notification.alert_bell_vibra && containsBell) || + (moduleConfig.external_notification.alert_message_vibra && !is_muted); - if ((moduleConfig.external_notification.alert_bell_buzzer || - moduleConfig.external_notification.alert_message_buzzer) && - canBuzz()) { - LOG_INFO("externalNotificationModule - Notification Module or Bell (Buzzer)"); - if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { - LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); - } else { - // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us -#ifdef T_LORA_PAGER - drv.setWaveform(0, 16); // Long buzzer 100% - drv.setWaveform(1, 0); // Pause - drv.setWaveform(2, 16); - drv.setWaveform(3, 0); - drv.setWaveform(4, 16); - drv.setWaveform(5, 0); - drv.setWaveform(6, 16); - drv.setWaveform(7, 0); - drv.go(); + // Alert GPIO Buzzer when receiving a bell = alertBellBuzzer: true + // Alert GPIO Buzzer when receiving a message = alertMessageBuzzer: true + const bool buzzerShouldAlert = canBuzz() && ((moduleConfig.external_notification.alert_bell_buzzer && containsBell) || + (moduleConfig.external_notification.alert_message_buzzer && !is_muted)); + + if (genericShouldAlert || vibraShouldAlert || buzzerShouldAlert) { + nagCycleCutoff = millis() + (moduleConfig.external_notification.nag_timeout + ? (moduleConfig.external_notification.nag_timeout * 1000) + : moduleConfig.external_notification.output_ms); + LOG_INFO("Toggling nagCycleCutoff to %lu", nagCycleCutoff); + isNagging = true; + } + + if (genericShouldAlert) { + LOG_INFO("externalNotificationModule - Generic alert"); + setExternalState(0, true); + } + + if (vibraShouldAlert) { + LOG_INFO("externalNotificationModule - Vibra alert"); + setExternalState(1, true); + } + + if (buzzerShouldAlert) { + LOG_INFO("externalNotificationModule - Buzzer alert"); + if (buzzerModeIsDirectOnly && !isDmToUs && !containsBell) { + LOG_INFO("Message buzzer was suppressed because buzzer mode DIRECT_MSG_ONLY"); + } else { + // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us +#ifdef HAS_DRV2605 + drv.setWaveform(0, 16); // Long buzzer 100% + drv.setWaveform(1, 0); // Pause + drv.setWaveform(2, 16); + drv.setWaveform(3, 0); + drv.setWaveform(4, 16); + drv.setWaveform(5, 0); + drv.setWaveform(6, 16); + drv.setWaveform(7, 0); + drv.go(); #endif + + if (moduleConfig.external_notification.use_i2s_as_buzzer) { #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { - audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); - } else + audioThread->beginRttl(rtttlConfig.ringtone, strlen_P(rtttlConfig.ringtone)); #endif - if (moduleConfig.external_notification.use_pwm) { - rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); - } else { - setExternalState(2, true); - } + } else if (moduleConfig.external_notification.use_pwm) { + rtttl::begin(config.device.buzzer_gpio, rtttlConfig.ringtone); + } else { + setExternalState(2, true); } } } @@ -513,4 +520,4 @@ int ExternalNotificationModule::handleInputEvent(const InputEvent *event) return 1; } return 0; -} \ No newline at end of file +} diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 7a332afae..f735ab776 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -38,6 +38,9 @@ #include "modules/PowerStressModule.h" #endif #include "modules/RoutingModule.h" +#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT +#include "modules/TrafficManagementModule.h" +#endif #include "modules/TextMessageModule.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" @@ -123,6 +126,14 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_REPLYBOT new ReplyBotModule(); #endif + +#if HAS_TRAFFIC_MANAGEMENT && !MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT + // Instantiate only when enabled to avoid extra memory use and background work. + if (moduleConfig.has_traffic_management && moduleConfig.traffic_management.enabled) { + trafficManagementModule = new TrafficManagementModule(); + } +#endif + #if !MESHTASTIC_EXCLUDE_ADMIN adminModule = new AdminModule(); #endif diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 79bda557c..4de479241 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -30,7 +30,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes auto p = *pptr; - if (mp.decoded.want_response) { + // Suppress replies to senders we've replied to recently (12H window) + if (mp.decoded.want_response && !isFromUs(&mp)) { const NodeNum sender = getFrom(&mp); const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); auto it = lastNodeInfoSeen.find(sender); @@ -117,9 +118,21 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha } } +void NodeInfoModule::triggerImmediateNodeInfoCheck() +{ + LOG_DEBUG("NodeInfo: scheduling immediate periodic check"); + setIntervalFromNow(0); +} + meshtastic_MeshPacket *NodeInfoModule::allocReply() { - if (suppressReplyForCurrentRequest) { + // Only apply suppression when actually replying to someone else's request, not for periodic broadcasts. + const bool isReplyingToExternalRequest = currentRequest && + currentRequest->which_payload_variant == meshtastic_MeshPacket_decoded_tag && + currentRequest->decoded.portnum == meshtastic_PortNum_NODEINFO_APP && + currentRequest->decoded.want_response && !isFromUs(currentRequest); + + if (suppressReplyForCurrentRequest && isReplyingToExternalRequest) { LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); ignoreRequest = true; suppressReplyForCurrentRequest = false; diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 0c0dec849..9b3b66cae 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -24,6 +24,12 @@ class NodeInfoModule : public ProtobufModule, private concurren void sendOurNodeInfo(NodeNum dest = NODENUM_BROADCAST, bool wantReplies = false, uint8_t channel = 0, bool _shorterTimeout = false); + /** + * Schedule an immediate NodeInfo periodic check. + * Used when external conditions change (for example time source quality). + */ + void triggerImmediateNodeInfoCheck(); + protected: /** Called to handle a particular incoming message diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index e9a21a22b..0378d01e7 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -467,7 +467,11 @@ int32_t PositionModule::runOnce() if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + []() { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip send smart broadcast due to time throttling"); +#endif + })) { LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " "minTimeInterval=%ims)", @@ -559,7 +563,11 @@ void PositionModule::handleNewPosition() if (smartPosition.hasTraveledOverThreshold && Throttle::execute( &lastGpsSend, minimumTimeThreshold, []() { positionModule->sendOurPosition(); }, - []() { LOG_DEBUG("Skip send smart broadcast due to time throttling"); })) { + []() { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip send smart broadcast due to time throttling"); +#endif + })) { LOG_DEBUG("Sent smart pos@%x:6 to mesh (distanceTraveled=%fm, minDistanceThreshold=%im, timeElapsed=%ims, " "minTimeInterval=%ims)", localPosition.timestamp, smartPosition.distanceTraveled, smartPosition.distanceThreshold, msSinceLastSend, diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index d87cf3a44..85e7f8c06 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -20,10 +20,11 @@ bool RoutingModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mesh if ((nodeDB->getMeshNode(mp.from) == NULL || !nodeDB->getMeshNode(mp.from)->has_user) && (nodeDB->getMeshNode(mp.to) == NULL || !nodeDB->getMeshNode(mp.to)->has_user)) return false; - } else if (owner.is_licensed && nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) { - // Don't let licensed users to rebroadcast packets from unlicensed users + } else if (owner.is_licensed && ((nodeDB->getLicenseStatus(mp.from) == UserLicenseStatus::NotLicensed) || + (nodeDB->getLicenseStatus(mp.to) == UserLicenseStatus::NotLicensed))) { + // Don't let licensed users to rebroadcast packets to or from unlicensed users // If we know they are in-fact unlicensed - LOG_DEBUG("Packet from unlicensed user, ignoring packet"); + LOG_DEBUG("Packet to or from unlicensed user, ignoring packet"); return false; } diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 7a969343e..20d4d7d8c 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -651,7 +651,7 @@ void SerialModule::processWXSerial() LOG_INFO("WS8X : %i %.1fg%.1f %.1fv %.1fv %.1fC rain: %.1f, %i sum", atoi(windDir), strtof(windVel, nullptr), strtof(windGust, nullptr), batVoltageF, capVoltageF, temperatureF, rain, rainSum); } - if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis)) { + if (gotwind && !Throttle::isWithinTimespanMs(lastAveraged, averageIntervalMillis) && velCount > 0 && dirCount > 0) { // calculate averages and send to the mesh float velAvg = 1.0 * velSum / velCount; diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 135d9f6eb..6df0e18f0 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -588,7 +588,8 @@ StoreForwardModule::StoreForwardModule() if (moduleConfig.store_forward.enabled) { // Router - if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || moduleConfig.store_forward.is_server)) { + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE || moduleConfig.store_forward.is_server)) { LOG_INFO("Init Store & Forward Module in Server mode"); if (memGet.getPsramSize() > 0) { if (memGet.getFreePsram() >= 1024 * 1024) { diff --git a/src/modules/Telemetry/BaseTelemetryModule.h b/src/modules/Telemetry/BaseTelemetryModule.h index b032bef3f..d986f41a9 100644 --- a/src/modules/Telemetry/BaseTelemetryModule.h +++ b/src/modules/Telemetry/BaseTelemetryModule.h @@ -9,6 +9,7 @@ class BaseTelemetryModule bool isSensorOrRouterRole() const { return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR || - config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; } }; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index b7b6e04a9..684d408a1 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -66,18 +66,10 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/MCP9808Sensor.h" #endif -#if __has_include() -#include "Sensor/SHT31Sensor.h" -#endif - #if __has_include() #include "Sensor/LPS22HBSensor.h" #endif -#if __has_include() -#include "Sensor/SHTC3Sensor.h" -#endif - #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 #include "Sensor/RAK12035Sensor.h" #endif @@ -94,8 +86,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/OPT3001Sensor.h" #endif -#if __has_include() -#include "Sensor/SHT4XSensor.h" +#if __has_include() +#include "Sensor/SHTXXSensor.h" #endif #if __has_include() @@ -155,6 +147,15 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) } LOG_INFO("Environment Telemetry adding I2C devices..."); + /* + Uncomment the preferences below if you want to use the module + without having to configure it from the PythonAPI or WebUI. + */ + + // moduleConfig.telemetry.environment_measurement_enabled = 1; + // moduleConfig.telemetry.environment_screen_enabled = 1; + // moduleConfig.telemetry.environment_update_interval = 15; + // order by priority of metrics/values (low top, high bottom) #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR @@ -202,15 +203,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MCP9808); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT31); -#endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LPS22HB); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHTC3); -#endif #if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 addSensor(i2cScanner, ScanI2C::DeviceType::RAK12035); #endif @@ -223,13 +218,9 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::OPT3001); #endif -#if __has_include() - addSensor(i2cScanner, ScanI2C::DeviceType::SHT4X); -#endif #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::MLX90632); #endif - #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BMP_3XX); #endif @@ -245,7 +236,10 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::BH1750); #endif - +#if __has_include() + // TODO Can we scan for multiple sensors connected on the same bus? + addSensor(i2cScanner, ScanI2C::DeviceType::SHTXX); +#endif #endif } @@ -260,14 +254,6 @@ int32_t EnvironmentTelemetryModule::runOnce() } uint32_t result = UINT32_MAX; - /* - Uncomment the preferences below if you want to use the module - without having to configure it from the PythonAPI or WebUI. - */ - - // moduleConfig.telemetry.environment_measurement_enabled = 1; - // moduleConfig.telemetry.environment_screen_enabled = 1; - // moduleConfig.telemetry.environment_update_interval = 15; if (!(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled || ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE)) { diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp index b0f0f9071..2feac6d5f 100644 --- a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -23,7 +23,7 @@ bool SEN5XSensor::getVersion() } delay(20); // From Sensirion Datasheet - uint8_t versionBuffer[12]; + uint8_t versionBuffer[12]{}; size_t charNumber = readBuffer(&versionBuffer[0], 3); if (charNumber == 0) { LOG_ERROR("SEN5X: Error getting data ready flag value"); @@ -638,7 +638,7 @@ bool SEN5XSensor::readValues() LOG_DEBUG("SEN5X: Reading PM Values"); delay(20); // From Sensirion Datasheet - uint8_t dataBuffer[16]; + uint8_t dataBuffer[16]{}; size_t receivedNumber = readBuffer(&dataBuffer[0], 24); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting values"); @@ -691,7 +691,7 @@ bool SEN5XSensor::readPNValues(bool cumulative) LOG_DEBUG("SEN5X: Reading PN Values"); delay(20); // From Sensirion Datasheet - uint8_t dataBuffer[20]; + uint8_t dataBuffer[20]{}; size_t receivedNumber = readBuffer(&dataBuffer[0], 30); if (receivedNumber == 0) { LOG_ERROR("SEN5X: Error getting PN values"); diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp b/src/modules/Telemetry/Sensor/SHT31Sensor.cpp deleted file mode 100644 index 67a36933d..000000000 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHT31Sensor.h" -#include "TelemetrySensor.h" -#include - -SHT31Sensor::SHT31Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT31, "SHT31") {} - -bool SHT31Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - sht31 = Adafruit_SHT31(bus); - status = sht31.begin(dev->address.address); - initI2CSensor(); - return status; -} - -bool SHT31Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - measurement->variant.environment_metrics.temperature = sht31.readTemperature(); - measurement->variant.environment_metrics.relative_humidity = sht31.readHumidity(); - - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT31Sensor.h b/src/modules/Telemetry/Sensor/SHT31Sensor.h deleted file mode 100644 index ecb7d63a6..000000000 --- a/src/modules/Telemetry/Sensor/SHT31Sensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHT31Sensor : public TelemetrySensor -{ - private: - Adafruit_SHT31 sht31; - - public: - SHT31Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp b/src/modules/Telemetry/Sensor/SHT4XSensor.cpp deleted file mode 100644 index b11795d97..000000000 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHT4XSensor.h" -#include "TelemetrySensor.h" -#include - -SHT4XSensor::SHT4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHT4X, "SHT4X") {} - -bool SHT4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - - uint32_t serialNumber = 0; - - status = sht4x.begin(bus); - if (!status) { - return status; - } - - serialNumber = sht4x.readSerial(); - if (serialNumber != 0) { - LOG_DEBUG("serialNumber : %x", serialNumber); - status = 1; - } else { - LOG_DEBUG("Error trying to execute readSerial(): "); - status = 0; - } - - initI2CSensor(); - return status; -} - -bool SHT4XSensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - - sensors_event_t humidity, temp; - sht4x.getEvent(&humidity, &temp); - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHT4XSensor.h b/src/modules/Telemetry/Sensor/SHT4XSensor.h deleted file mode 100644 index 7311d2366..000000000 --- a/src/modules/Telemetry/Sensor/SHT4XSensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHT4XSensor : public TelemetrySensor -{ - private: - Adafruit_SHT4x sht4x = Adafruit_SHT4x(); - - public: - SHT4XSensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp b/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp deleted file mode 100644 index fdab0b266..000000000 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "SHTC3Sensor.h" -#include "TelemetrySensor.h" -#include - -SHTC3Sensor::SHTC3Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTC3, "SHTC3") {} - -bool SHTC3Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) -{ - LOG_INFO("Init sensor: %s", sensorName); - status = shtc3.begin(bus); - - initI2CSensor(); - return status; -} - -bool SHTC3Sensor::getMetrics(meshtastic_Telemetry *measurement) -{ - measurement->variant.environment_metrics.has_temperature = true; - measurement->variant.environment_metrics.has_relative_humidity = true; - - sensors_event_t humidity, temp; - shtc3.getEvent(&humidity, &temp); - - measurement->variant.environment_metrics.temperature = temp.temperature; - measurement->variant.environment_metrics.relative_humidity = humidity.relative_humidity; - - return true; -} - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTC3Sensor.h b/src/modules/Telemetry/Sensor/SHTC3Sensor.h deleted file mode 100644 index 51cee18f7..000000000 --- a/src/modules/Telemetry/Sensor/SHTC3Sensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#include "configuration.h" - -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() - -#include "../mesh/generated/meshtastic/telemetry.pb.h" -#include "TelemetrySensor.h" -#include - -class SHTC3Sensor : public TelemetrySensor -{ - private: - Adafruit_SHTC3 shtc3 = Adafruit_SHTC3(); - - public: - SHTC3Sensor(); - virtual bool getMetrics(meshtastic_Telemetry *measurement) override; - virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; -}; - -#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTXXSensor.cpp b/src/modules/Telemetry/Sensor/SHTXXSensor.cpp new file mode 100644 index 000000000..92cac7f77 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTXXSensor.cpp @@ -0,0 +1,145 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SHTXXSensor.h" +#include "TelemetrySensor.h" +#include + +SHTXXSensor::SHTXXSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SHTXX, "SHTXX") {} + +void SHTXXSensor::getSensorVariant(SHTSensor::SHTSensorType sensorType) +{ + switch (sensorType) { + case SHTSensor::SHTSensorType::SHT2X: + sensorVariant = "SHT2x"; + break; + + case SHTSensor::SHTSensorType::SHT3X: + case SHTSensor::SHTSensorType::SHT85: + sensorVariant = "SHT3x/SHT85"; + break; + + case SHTSensor::SHTSensorType::SHT3X_ALT: + sensorVariant = "SHT3x"; + break; + + case SHTSensor::SHTSensorType::SHTW1: + case SHTSensor::SHTSensorType::SHTW2: + case SHTSensor::SHTSensorType::SHTC1: + case SHTSensor::SHTSensorType::SHTC3: + sensorVariant = "SHTC1/SHTC3/SHTW1/SHTW2"; + break; + + case SHTSensor::SHTSensorType::SHT4X: + sensorVariant = "SHT4x"; + break; + + default: + sensorVariant = "Unknown"; + break; + } +} + +bool SHTXXSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + + if (sht.init(*_bus)) { + LOG_INFO("%s: init(): success", sensorName); + getSensorVariant(sht.mSensorType); + LOG_INFO("%s Sensor detected: %s on 0x%x", sensorName, sensorVariant, _address); + status = 1; + } else { + LOG_ERROR("%s: init(): failed", sensorName); + } + + initI2CSensor(); + return status; +} + +/** + * Accuracy setting of measurement. + * Not all sensors support changing the sampling accuracy (only SHT3X and SHT4X) + * SHTAccuracy: + * - SHT_ACCURACY_HIGH: Highest repeatability at the cost of slower measurement + * - SHT_ACCURACY_MEDIUM: Balanced repeatability and speed of measurement + * - SHT_ACCURACY_LOW: Fastest measurement but lowest repeatability + */ +bool SHTXXSensor::setAccuracy(SHTSensor::SHTAccuracy newAccuracy) +{ + // Only SHT3X-family (including alternates) and SHT4X support changing accuracy + if (sht.mSensorType != SHTSensor::SHTSensorType::SHT3X && sht.mSensorType != SHTSensor::SHTSensorType::SHT3X_ALT && + sht.mSensorType != SHTSensor::SHTSensorType::SHT85 && sht.mSensorType != SHTSensor::SHTSensorType::SHT4X) { + LOG_WARN("%s doesn't support accuracy setting", sensorVariant); + return false; + } + LOG_INFO("%s: setting new accuracy setting", sensorVariant); + accuracy = newAccuracy; + return sht.setAccuracy(accuracy); +} + +bool SHTXXSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + if (sht.readSample()) { + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.temperature = sht.getTemperature(); + measurement->variant.environment_metrics.relative_humidity = sht.getHumidity(); + + LOG_INFO("%s (%s): Got: temp:%fdegC, hum:%f%%rh", sensorName, sensorVariant, + measurement->variant.environment_metrics.temperature, + measurement->variant.environment_metrics.relative_humidity); + + return true; + } else { + LOG_ERROR("%s (%s): read sample failed", sensorName, sensorVariant); + return false; + } +} + +AdminMessageHandleResult SHTXXSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + result = AdminMessageHandleResult::NOT_HANDLED; + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_shtxx_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + // Check for sensor accuracy setting + if (request->sensor_config.shtxx_config.has_set_accuracy) { + SHTSensor::SHTAccuracy newAccuracy; + if (request->sensor_config.shtxx_config.set_accuracy == 0) { + newAccuracy = SHTSensor::SHT_ACCURACY_LOW; + } else if (request->sensor_config.shtxx_config.set_accuracy == 1) { + newAccuracy = SHTSensor::SHT_ACCURACY_MEDIUM; + } else if (request->sensor_config.shtxx_config.set_accuracy == 2) { + newAccuracy = SHTSensor::SHT_ACCURACY_HIGH; + } else { + LOG_ERROR("%s: incorrect accuracy setting", sensorName); + result = AdminMessageHandleResult::HANDLED; + break; + } + this->setAccuracy(newAccuracy); + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SHTXXSensor.h b/src/modules/Telemetry/Sensor/SHTXXSensor.h new file mode 100644 index 000000000..e354e113f --- /dev/null +++ b/src/modules/Telemetry/Sensor/SHTXXSensor.h @@ -0,0 +1,29 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class SHTXXSensor : public TelemetrySensor +{ + private: + SHTSensor sht; + TwoWire *_bus{}; + uint8_t _address{}; + SHTSensor::SHTAccuracy accuracy{}; + bool setAccuracy(SHTSensor::SHTAccuracy newAccuracy); + + public: + SHTXXSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + void getSensorVariant(SHTSensor::SHTSensorType); + const char *sensorVariant{}; + + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/T1000xSensor.cpp b/src/modules/Telemetry/Sensor/T1000xSensor.cpp index b123450ec..1e2a77e8b 100644 --- a/src/modules/Telemetry/Sensor/T1000xSensor.cpp +++ b/src/modules/Telemetry/Sensor/T1000xSensor.cpp @@ -73,11 +73,15 @@ float T1000xSensor::getTemp() float Vout = 0, Rt = 0, temp = 0; float Temp = 0; + // P0.4 is a sensor power enable GPIO, not a VCC ADC pin. + // Read BATTERY_PIN (with voltage divider) and cap at NTC_REF_VCC to estimate the sensor rail voltage. for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { - vcc_vot += analogRead(T1000X_VCC_PIN); + vcc_vot += analogRead(BATTERY_PIN); } vcc_vot = vcc_vot / T1000X_SENSE_SAMPLES; - vcc_vot = 2 * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + vcc_vot = ADC_MULTIPLIER * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vcc_vot; + if (vcc_vot > NTC_REF_VCC) + vcc_vot = NTC_REF_VCC; for (uint32_t i = 0; i < T1000X_SENSE_SAMPLES; i++) { ntc_vot += analogRead(T1000X_NTC_PIN); diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp new file mode 100644 index 000000000..1ecb68c4b --- /dev/null +++ b/src/modules/TrafficManagementModule.cpp @@ -0,0 +1,1415 @@ +#include "TrafficManagementModule.h" + +#if HAS_TRAFFIC_MANAGEMENT + +#include "Default.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "Router.h" +#include "TypeConversions.h" +#include "airtime.h" +#include "concurrency/LockGuard.h" +#include "configuration.h" +#include "mesh-pb-constants.h" +#include "meshUtils.h" +#include +#include + +#define TM_LOG_DEBUG(fmt, ...) LOG_DEBUG("[TM] " fmt, ##__VA_ARGS__) +#define TM_LOG_INFO(fmt, ...) LOG_INFO("[TM] " fmt, ##__VA_ARGS__) +#define TM_LOG_WARN(fmt, ...) LOG_WARN("[TM] " fmt, ##__VA_ARGS__) + +// ============================================================================= +// Anonymous Namespace - Internal Helpers +// ============================================================================= + +namespace +{ + +constexpr uint32_t kMaintenanceIntervalMs = 60 * 1000UL; // Cache cleanup interval +constexpr uint32_t kUnknownResetMs = 60 * 1000UL; // Unknown packet window +constexpr uint8_t kMaxCuckooKicks = 16; // Max displacement chain length + +// NodeInfo direct response: enforced maximum hops by device role +// Both use maxHops logic (respond when hopsAway <= threshold) +// Config value is clamped to these role-based limits +// Note: nodeinfo_direct_response must also be enabled for this to take effect +constexpr uint32_t kRouterDefaultMaxHops = 3; // Routers: max 3 hops (can set lower via config) +constexpr uint32_t kClientDefaultMaxHops = 0; // Clients: direct only (cannot increase) + +/** + * Convert seconds to milliseconds with overflow protection. + */ +uint32_t secsToMs(uint32_t secs) +{ + uint64_t ms = static_cast(secs) * 1000ULL; + if (ms > UINT32_MAX) + return UINT32_MAX; + return static_cast(ms); +} + +/** + * Clamp precision to a valid dedup range. + * Invalid values use the module default precision. + */ +uint8_t sanitizePositionPrecision(uint8_t precision) +{ + if (precision > 0 && precision <= 32) + return precision; + + const uint8_t defaultPrecision = static_cast(default_traffic_mgmt_position_precision_bits); + if (defaultPrecision > 0 && defaultPrecision <= 32) + return defaultPrecision; + + // Someone done messed up if we reach here + return 32; +} + +/** + * Check if a timestamp is within a time window. + * Handles wrap-around correctly using unsigned subtraction. + */ +bool isWithinWindow(uint32_t nowMs, uint32_t startMs, uint32_t intervalMs) +{ + if (intervalMs == 0 || startMs == 0) + return false; + return (nowMs - startMs) < intervalMs; +} + +/** + * Truncate lat/lon to specified precision for position deduplication. + * + * The truncation works by masking off lower bits and rounding to the center + * of the resulting grid cell. This creates a stable truncated value even + * when GPS jitter causes small coordinate changes. + * + * @param value Raw latitude_i or longitude_i from position + * @param precision Number of significant bits to keep (0-32) + * @return Truncated and centered coordinate value + */ +int32_t truncateLatLon(int32_t value, uint8_t precision) +{ + if (precision == 0 || precision >= 32) + return value; + + // Create mask to zero out lower bits + uint32_t mask = UINT32_MAX << (32 - precision); + uint32_t truncated = static_cast(value) & mask; + + // Add half the truncation step to center in the grid cell + truncated += (1u << (31 - precision)); + return static_cast(truncated); +} + +/** + * Saturating increment for uint8_t counters. + * Prevents overflow by capping at UINT8_MAX (255). + */ +inline void saturatingIncrement(uint8_t &counter) +{ + if (counter < UINT8_MAX) + counter++; +} + +/** + * Return a short human-readable name for common port numbers. + * Falls back to "port:" for unknown ports. + */ +const char *portName(int portnum) +{ + switch (portnum) { + case meshtastic_PortNum_TEXT_MESSAGE_APP: + return "text"; + case meshtastic_PortNum_POSITION_APP: + return "position"; + case meshtastic_PortNum_NODEINFO_APP: + return "nodeinfo"; + case meshtastic_PortNum_ROUTING_APP: + return "routing"; + case meshtastic_PortNum_ADMIN_APP: + return "admin"; + case meshtastic_PortNum_TELEMETRY_APP: + return "telemetry"; + case meshtastic_PortNum_TRACEROUTE_APP: + return "traceroute"; + case meshtastic_PortNum_NEIGHBORINFO_APP: + return "neighborinfo"; + case meshtastic_PortNum_STORE_FORWARD_APP: + return "store-forward"; + case meshtastic_PortNum_WAYPOINT_APP: + return "waypoint"; + default: + return nullptr; + } +} + +} // namespace + +// ============================================================================= +// Module Instance +// ============================================================================= + +TrafficManagementModule *trafficManagementModule; + +// ============================================================================= +// Constructor +// ============================================================================= + +TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManagement"), concurrency::OSThread("TrafficManagement") +{ + // Module configuration + isPromiscuous = true; // See all packets, not just those addressed to us + encryptedOk = true; // Can process encrypted packets + stats = meshtastic_TrafficManagementStats_init_zero; + + // Initialize rolling epoch for relative timestamps + cacheEpochMs = millis(); + + // Calculate adaptive time resolutions from config (config changes require reboot) + // Resolution = max(60, min(339, interval/2)) for ~24 hour range with good precision + posTimeResolution = calcTimeResolution(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + rateTimeResolution = calcTimeResolution(moduleConfig.traffic_management.rate_limit_window_secs); + unknownTimeResolution = calcTimeResolution(kUnknownResetMs / 1000); // ~5 min default + + const auto &cfg = moduleConfig.traffic_management; + TM_LOG_INFO("Enabled: pos_dedup=%d nodeinfo_resp=%d rate_limit=%d drop_unknown=%d exhaust_telem=%d exhaust_pos=%d " + "preserve_hops=%d", + cfg.position_dedup_enabled, cfg.nodeinfo_direct_response, cfg.rate_limit_enabled, cfg.drop_unknown_enabled, + cfg.exhaust_hop_telemetry, cfg.exhaust_hop_position, cfg.router_preserve_hops); + TM_LOG_DEBUG("Time resolutions: pos=%us, rate=%us, unknown=%us", posTimeResolution, rateTimeResolution, + unknownTimeResolution); + +// Allocate unified cache (10 bytes/entry for all platforms) +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + const uint16_t allocSize = cacheSize(); + TM_LOG_INFO("Allocating unified cache: %u entries (%u bytes)", allocSize, + static_cast(allocSize * sizeof(UnifiedCacheEntry))); + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + // ESP32 with PSRAM: prefer PSRAM for large allocations + cache = static_cast(ps_calloc(allocSize, sizeof(UnifiedCacheEntry))); + if (cache) { + cacheFromPsram = true; + } else { + TM_LOG_WARN("PSRAM allocation failed, falling back to heap"); + cache = new UnifiedCacheEntry[allocSize](); + } +#else + // All other platforms: heap allocation + cache = new UnifiedCacheEntry[allocSize](); +#endif + +#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + TM_LOG_INFO("Allocating NodeInfo cache: target=%u occupancy=%u%% payload=%u bytes (PSRAM) tags=%u bytes (%u-bit, %u slots, " + "%u buckets x %u)", + static_cast(nodeInfoTargetEntries()), static_cast(nodeInfoTargetOccupancyPercent()), + static_cast(nodeInfoTargetEntries() * sizeof(NodeInfoPayloadEntry)), + static_cast(nodeInfoIndexMetadataBudgetBytes()), static_cast(nodeInfoTagBits()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), + static_cast(nodeInfoBucketSize())); + + nodeInfoIndex = static_cast(calloc(nodeInfoIndexMetadataBudgetBytes(), sizeof(uint8_t))); + if (!nodeInfoIndex) { + TM_LOG_WARN("NodeInfo index allocation failed; direct responses will fall back to NodeDB"); + } else { + nodeInfoPayload = static_cast(ps_calloc(nodeInfoTargetEntries(), sizeof(NodeInfoPayloadEntry))); + if (nodeInfoPayload) { + nodeInfoPayloadFromPsram = true; + TM_LOG_INFO("NodeInfo bucketed cuckoo cache ready"); + } else { + TM_LOG_WARN("NodeInfo PSRAM payload allocation failed; direct responses will fall back to NodeDB"); + free(nodeInfoIndex); + nodeInfoIndex = nullptr; + } + } +#else + TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target"); +#endif + + setIntervalFromNow(kMaintenanceIntervalMs); +} + +// Cache may have been allocated via ps_calloc (PSRAM, C allocator) or new[] (heap). +// Must use the matching deallocator: free() for ps_calloc, delete[] for new[]. +TrafficManagementModule::~TrafficManagementModule() +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + if (cache) { + // Cache may be from ps_calloc (PSRAM, C allocator) or new[] (heap). + // Use the matching deallocator for the allocation source. + if (cacheFromPsram) + free(cache); + else + delete[] cache; + cache = nullptr; + } +#endif + + if (nodeInfoPayload) { + if (nodeInfoPayloadFromPsram) + free(nodeInfoPayload); + else + delete[] nodeInfoPayload; + nodeInfoPayload = nullptr; + } + + if (nodeInfoIndex) { + free(nodeInfoIndex); + nodeInfoIndex = nullptr; + } +} + +// ============================================================================= +// Statistics +// ============================================================================= + +meshtastic_TrafficManagementStats TrafficManagementModule::getStats() const +{ + concurrency::LockGuard guard(&cacheLock); + return stats; +} + +void TrafficManagementModule::resetStats() +{ + concurrency::LockGuard guard(&cacheLock); + stats = meshtastic_TrafficManagementStats_init_zero; +} + +void TrafficManagementModule::recordRouterHopPreserved() +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return; + incrementStat(&stats.router_hops_preserved); +} + +void TrafficManagementModule::incrementStat(uint32_t *field) +{ + concurrency::LockGuard guard(&cacheLock); + (*field)++; +} + +// ============================================================================= +// Cuckoo Hash Table Operations +// ============================================================================= + +/** + * Find an existing entry for the given node. + * + * Cuckoo hashing guarantees that if an entry exists, it's in one of exactly + * two locations: hash1(node) or hash2(node). This provides O(1) lookup. + * + * @param node NodeNum to search for + * @return Pointer to entry if found, nullptr otherwise + */ +TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findEntry(NodeNum node) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)node; + return nullptr; +#else + if (!cache || node == 0) + return nullptr; + + // Check primary location + uint16_t h1 = cuckooHash1(node); + if (cache[h1].node == node) + return &cache[h1]; + + // Check alternate location + uint16_t h2 = cuckooHash2(node); + if (cache[h2].node == node) + return &cache[h2]; + + return nullptr; +#endif +} + +/** + * Find or create an entry for the given node using cuckoo hashing. + * + * If the node exists, returns the existing entry. Otherwise, attempts to + * insert a new entry using cuckoo displacement: + * + * 1. Try to insert at h1(node) - if empty, done + * 2. Try to insert at h2(node) - if empty, done + * 3. Kick existing entry from h1 to its alternate location + * 4. Repeat up to kMaxCuckooKicks times + * 5. If cycle detected or max kicks exceeded, evict oldest entry + * + * @param node NodeNum to find or create + * @param isNew Set to true if a new entry was created + * @return Pointer to entry, or nullptr if allocation failed + */ +TrafficManagementModule::UnifiedCacheEntry *TrafficManagementModule::findOrCreateEntry(NodeNum node, bool *isNew) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)node; + if (isNew) + *isNew = false; + return nullptr; +#else + if (!cache || node == 0) { + if (isNew) + *isNew = false; + return nullptr; + } + + // Check if entry already exists (O(1) lookup) + uint16_t h1 = cuckooHash1(node); + if (cache[h1].node == node) { + if (isNew) + *isNew = false; + return &cache[h1]; + } + + uint16_t h2 = cuckooHash2(node); + if (cache[h2].node == node) { + if (isNew) + *isNew = false; + return &cache[h2]; + } + + // Entry doesn't exist - try to insert + + // Prefer empty slot at h1 + if (cache[h1].node == 0) { + memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); + cache[h1].node = node; + if (isNew) + *isNew = true; + return &cache[h1]; + } + + // Try empty slot at h2 + if (cache[h2].node == 0) { + memset(&cache[h2], 0, sizeof(UnifiedCacheEntry)); + cache[h2].node = node; + if (isNew) + *isNew = true; + return &cache[h2]; + } + + // Both slots occupied - perform cuckoo displacement + // Start by kicking entry at h1 to its alternate location + UnifiedCacheEntry displaced = cache[h1]; + memset(&cache[h1], 0, sizeof(UnifiedCacheEntry)); + cache[h1].node = node; + + for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { + // Find alternate location for displaced entry + uint16_t altH1 = cuckooHash1(displaced.node); + uint16_t altH2 = cuckooHash2(displaced.node); + uint16_t altSlot = (altH1 == h1) ? altH2 : altH1; + + if (cache[altSlot].node == 0) { + // Found empty slot - insert displaced entry + cache[altSlot] = displaced; + if (isNew) + *isNew = true; + return &cache[h1]; + } + + // Kick entry from alternate slot + UnifiedCacheEntry temp = cache[altSlot]; + cache[altSlot] = displaced; + displaced = temp; + h1 = altSlot; + } + + // Cuckoo cycle detected or max kicks exceeded. + // The displaced entry has no valid cuckoo slot โ€” drop it to preserve cache integrity. + // Placing it at an arbitrary slot would make it unreachable by findEntry(). + TM_LOG_DEBUG("Cuckoo cycle, evicting node 0x%08x", displaced.node); + + if (isNew) + *isNew = true; + return &cache[cuckooHash1(node)]; +#endif +} + +const TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findNodeInfoEntry(NodeNum node) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return nullptr; + + uint16_t payloadIndex = findNodeInfoPayloadIndex(node); + if (payloadIndex >= nodeInfoTargetEntries()) + return nullptr; + + return &nodeInfoPayload[payloadIndex]; +#else + (void)node; + return nullptr; +#endif +} + +uint16_t TrafficManagementModule::encodeNodeInfoTag(uint16_t payloadIndex) const +{ + if (payloadIndex >= nodeInfoTargetEntries()) + return 0; + return static_cast(payloadIndex + 1u); +} + +uint16_t TrafficManagementModule::decodeNodeInfoPayloadIndex(uint16_t tag) const +{ + if (tag == 0 || tag > nodeInfoTargetEntries()) + return UINT16_MAX; + return static_cast(tag - 1u); +} + +uint16_t TrafficManagementModule::getNodeInfoTag(uint16_t slot) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) + return 0; + + const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); + const uint16_t byteOffset = static_cast(bitOffset >> 3); + const uint8_t shift = static_cast(bitOffset & 7u); + uint32_t packed = 0; + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset]); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; + + return static_cast((packed >> shift) & nodeInfoTagMask()); +#else + (void)slot; + return 0; +#endif +} + +void TrafficManagementModule::setNodeInfoTag(uint16_t slot, uint16_t tag) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || slot >= nodeInfoIndexSlots()) + return; + + const uint16_t normalizedTag = static_cast(tag & nodeInfoTagMask()); + const uint32_t bitOffset = static_cast(slot) * nodeInfoTagBits(); + const uint16_t byteOffset = static_cast(bitOffset >> 3); + const uint8_t shift = static_cast(bitOffset & 7u); + uint32_t packed = 0; + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset]); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 1u]) << 8; + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + packed |= static_cast(nodeInfoIndex[byteOffset + 2u]) << 16; + + const uint32_t mask = static_cast(nodeInfoTagMask()) << shift; + packed = (packed & ~mask) | ((static_cast(normalizedTag) << shift) & mask); + + if (byteOffset < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset] = static_cast(packed & 0xFFu); + if (static_cast(byteOffset + 1u) < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset + 1u] = static_cast((packed >> 8) & 0xFFu); + if (static_cast(byteOffset + 2u) < nodeInfoIndexMetadataBudgetBytes()) + nodeInfoIndex[byteOffset + 2u] = static_cast((packed >> 16) & 0xFFu); +#else + (void)slot; + (void)tag; +#endif +} + +uint16_t TrafficManagementModule::findNodeInfoPayloadIndex(NodeNum node) const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return UINT16_MAX; + + const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; + + for (uint8_t b = 0; b < 2; b++) { + const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + uint16_t tag = getNodeInfoTag(static_cast(base + slot)); + if (tag == 0) + continue; + + uint16_t payloadIndex = decodeNodeInfoPayloadIndex(tag); + if (payloadIndex >= nodeInfoTargetEntries()) + continue; + + if (nodeInfoPayload[payloadIndex].node == node) + return payloadIndex; + } + } + + return UINT16_MAX; +#else + (void)node; + return UINT16_MAX; +#endif +} + +bool TrafficManagementModule::removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || node == 0 || payloadIndex >= nodeInfoTargetEntries()) + return false; + + const uint16_t payloadTag = encodeNodeInfoTag(payloadIndex); + if (payloadTag == 0) + return false; + const uint16_t buckets[2] = {nodeInfoHash1(node), nodeInfoHash2(node)}; + + for (uint8_t b = 0; b < 2; b++) { + const uint16_t base = static_cast(buckets[b] * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + const uint16_t indexSlot = static_cast(base + slot); + if (getNodeInfoTag(indexSlot) == payloadTag) { + setNodeInfoTag(indexSlot, 0); + return true; + } + } + } + + return false; +#else + (void)node; + (void)payloadIndex; + return false; +#endif +} + +uint16_t TrafficManagementModule::allocateNodeInfoPayloadSlot() +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload) + return UINT16_MAX; + + for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { + uint16_t idx = static_cast((nodeInfoAllocHint + tries) % nodeInfoTargetEntries()); + if (nodeInfoPayload[idx].node == 0) { + nodeInfoAllocHint = static_cast((idx + 1u) % nodeInfoTargetEntries()); + return idx; + } + } +#endif + return UINT16_MAX; +} + +uint16_t TrafficManagementModule::evictNodeInfoPayloadSlot() +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex) + return UINT16_MAX; + + for (uint16_t tries = 0; tries < nodeInfoTargetEntries(); tries++) { + uint16_t idx = static_cast(nodeInfoEvictCursor % nodeInfoTargetEntries()); + nodeInfoEvictCursor = static_cast((nodeInfoEvictCursor + 1u) % nodeInfoTargetEntries()); + + NodeNum oldNode = nodeInfoPayload[idx].node; + if (oldNode == 0) + continue; + + removeNodeInfoIndexEntry(oldNode, idx); // best effort; cache tolerates occasional stale miss + nodeInfoPayload[idx].node = 0; + return idx; + } +#endif + return UINT16_MAX; +} + +bool TrafficManagementModule::tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex || !nodeInfoPayload || bucket >= nodeInfoBucketCount() || tag == 0) + return false; + + const uint16_t base = static_cast(bucket * nodeInfoBucketSize()); + for (uint8_t slot = 0; slot < nodeInfoBucketSize(); slot++) { + const uint16_t indexSlot = static_cast(base + slot); + const uint16_t existingTag = getNodeInfoTag(indexSlot); + if (existingTag == 0) { + setNodeInfoTag(indexSlot, tag); + return true; + } + + // Opportunistically reuse stale tags that point at empty/invalid payload slots. + const uint16_t payloadIndex = decodeNodeInfoPayloadIndex(existingTag); + if (payloadIndex >= nodeInfoTargetEntries() || nodeInfoPayload[payloadIndex].node == 0) { + setNodeInfoTag(indexSlot, tag); + return true; + } + } +#else + (void)bucket; + (void)tag; +#endif + return false; +} + +TrafficManagementModule::NodeInfoPayloadEntry *TrafficManagementModule::findOrCreateNodeInfoEntry(NodeNum node, + bool *usedEmptySlot) +{ + if (usedEmptySlot) + *usedEmptySlot = false; + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || node == 0) + return nullptr; + + uint16_t existing = findNodeInfoPayloadIndex(node); + if (existing < nodeInfoTargetEntries()) + return &nodeInfoPayload[existing]; + + const uint16_t beforeCount = countNodeInfoEntriesLocked(); + + uint16_t payloadIndex = allocateNodeInfoPayloadSlot(); + if (payloadIndex == UINT16_MAX) { + payloadIndex = evictNodeInfoPayloadSlot(); + if (payloadIndex == UINT16_MAX) + return nullptr; + } + + nodeInfoPayload[payloadIndex].node = node; + + // 4-way bucketed cuckoo insertion mirrors Cuckoo Filter practice from + // Fan et al. (CoNEXT 2014): high occupancy with short relocation chains. + uint16_t pending = encodeNodeInfoTag(payloadIndex); + uint16_t h1 = nodeInfoHash1(node); + uint16_t h2 = nodeInfoHash2(node); + + if (!tryInsertNodeInfoEntryInBucket(h1, pending) && !tryInsertNodeInfoEntryInBucket(h2, pending)) { + uint16_t currentBucket = h1; + for (uint8_t kicks = 0; kicks < kMaxCuckooKicks; kicks++) { + const uint16_t base = static_cast(currentBucket * nodeInfoBucketSize()); + const uint16_t kickSlot = static_cast((node + kicks) & (nodeInfoBucketSize() - 1u)); + const uint16_t pos = static_cast(base + kickSlot); + + uint16_t displaced = getNodeInfoTag(pos); + setNodeInfoTag(pos, pending); + pending = displaced; + + uint16_t displacedPayload = decodeNodeInfoPayloadIndex(pending); + if (displacedPayload >= nodeInfoTargetEntries()) { + pending = 0; + break; + } + + NodeNum displacedNode = nodeInfoPayload[displacedPayload].node; + if (displacedNode == 0) { + pending = 0; + break; + } + + uint16_t altH1 = nodeInfoHash1(displacedNode); + uint16_t altH2 = nodeInfoHash2(displacedNode); + uint16_t altBucket = (altH1 == currentBucket) ? altH2 : altH1; + + if (tryInsertNodeInfoEntryInBucket(altBucket, pending)) { + pending = 0; + break; + } + + currentBucket = altBucket; + } + + if (pending != 0) { + uint16_t droppedPayload = decodeNodeInfoPayloadIndex(pending); + if (droppedPayload < nodeInfoTargetEntries()) + nodeInfoPayload[droppedPayload].node = 0; + TM_LOG_DEBUG("NodeInfo bucketed cuckoo overflow, dropped payload idx=%u", + static_cast(droppedPayload < nodeInfoTargetEntries() ? droppedPayload : UINT16_MAX)); + } + } + + uint16_t finalIndex = findNodeInfoPayloadIndex(node); + if (finalIndex >= nodeInfoTargetEntries()) { + // New entry did not survive insertion chain. + if (payloadIndex < nodeInfoTargetEntries() && nodeInfoPayload[payloadIndex].node == node) + nodeInfoPayload[payloadIndex].node = 0; + return nullptr; + } + + if (usedEmptySlot) { + const uint16_t afterCount = countNodeInfoEntriesLocked(); + *usedEmptySlot = afterCount > beforeCount; + } + + return &nodeInfoPayload[finalIndex]; +#else + (void)node; + return nullptr; +#endif +} + +uint16_t TrafficManagementModule::countNodeInfoEntriesLocked() const +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoIndex) + return 0; + + uint16_t count = 0; + for (uint16_t i = 0; i < nodeInfoIndexSlots(); i++) { + if (getNodeInfoTag(i) != 0) + count++; + } + return count; +#else + return 0; +#endif +} + +void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &mp) +{ +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (!nodeInfoPayload || !nodeInfoIndex || mp.decoded.payload.size == 0) + return; + + meshtastic_User user = meshtastic_User_init_zero; + if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &user)) + return; + + // Normalize user.id to the packet sender's node number. + snprintf(user.id, sizeof(user.id), "!%08x", getFrom(&mp)); + + bool usedEmptySlot = false; + uint16_t cachedCount = 0; + { + concurrency::LockGuard guard(&cacheLock); + NodeInfoPayloadEntry *entry = findOrCreateNodeInfoEntry(getFrom(&mp), &usedEmptySlot); + if (!entry) + return; + + // Cache both payload and response metadata so direct replies can use + // richer context than "just the user protobuf" when PSRAM is present. + // This path is intentionally independent from NodeInfoModule/NodeDB. + entry->user = user; + entry->lastObservedMs = millis(); + entry->lastObservedRxTime = mp.rx_time; + entry->sourceChannel = mp.channel; + entry->hasDecodedBitfield = mp.decoded.has_bitfield; + entry->decodedBitfield = mp.decoded.bitfield; + + if (usedEmptySlot) + cachedCount = countNodeInfoEntriesLocked(); + } + + if (usedEmptySlot) { + TM_LOG_INFO("NodeInfo PSRAM cache entries: %u/%u target (%u packed slots, %u-bit tags, %u-byte DRAM index)", + static_cast(cachedCount), static_cast(nodeInfoTargetEntries()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoTagBits()), + static_cast(nodeInfoIndexMetadataBudgetBytes())); + } +#else + (void)mp; +#endif +} + +// ============================================================================= +// Epoch Management +// ============================================================================= + +/** + * Reset the timestamp epoch when relative offsets approach overflow. + * + * Called when epoch age exceeds ~19 hours (approaching 8-bit minute overflow). + * Invalidates all cached per-node traffic state. + */ +void TrafficManagementModule::resetEpoch(uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + TM_LOG_DEBUG("Resetting cache epoch"); + cacheEpochMs = nowMs; + + // Full flush avoids stale dedup identity/counters surviving epoch rollover. + memset(cache, 0, static_cast(cacheSize()) * sizeof(UnifiedCacheEntry)); +#else + (void)nowMs; +#endif +} + +// ============================================================================= +// Position Hash (Compact Mode) +// ============================================================================= + +/** + * Compute 8-bit position fingerprint from truncated lat/lon coordinates. + * + * Unlike a hash, this is deterministic: adjacent grid cells have sequential + * fingerprints, so nearby positions never collide. The fingerprint extracts + * the lower 4 significant bits from each truncated coordinate. + * + * Example with precision=16: + * lat_truncated = 0x12340000 (top 16 bits significant) + * Significant portion = 0x1234, lower 4 bits = 0x4 + * + * fingerprint = (lat_low4 << 4) | lon_low4 = 8 bits total + * + * Collision: Two positions collide only if they differ by a multiple of 16 + * grid cells in BOTH lat and lon dimensions simultaneously - very unlikely + * for typical position update patterns. + * + * @param lat_truncated Precision-truncated latitude + * @param lon_truncated Precision-truncated longitude + * @param precision Number of significant bits (1-32) + * @return 8-bit fingerprint (4 bits lat + 4 bits lon) + */ +uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision) +{ + precision = sanitizePositionPrecision(precision); + + // Guard: if precision < 4, we have fewer bits to work with + // Take min(precision, 4) bits from each coordinate + uint8_t bitsToTake = (precision < 4) ? precision : 4; + + // Shift to move significant bits to bottom, then mask lower bits + // For precision=16: shift by 16 to get the 16 significant bits at bottom + uint8_t shift = 32 - precision; + uint8_t latBits = (static_cast(lat_truncated) >> shift) & ((1u << bitsToTake) - 1); + uint8_t lonBits = (static_cast(lon_truncated) >> shift) & ((1u << bitsToTake) - 1); + + return static_cast((latBits << 4) | lonBits); +} + +// ============================================================================= +// Packet Handling +// ============================================================================= + +// Processing order matters: this module runs BEFORE RoutingModule in the callModules() loop. +// - STOP prevents RoutingModule from calling sniffReceived() โ†’ perhapsRebroadcast(), +// so the packet is fully consumed (not forwarded). +// - ignoreRequest suppresses the default "no one responded" NAK for want_response packets. +// - exhaustRequested is set by alterReceived() and checked by perhapsRebroadcast() to +// force hop_limit=0 on the rebroadcast copy, allowing one final relay hop. +ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return ProcessMessage::CONTINUE; + + ignoreRequest = false; + exhaustRequested = false; // Reset per-packet; may be set by alterReceived() below + exhaustRequestedFrom = 0; + exhaustRequestedId = 0; + incrementStat(&stats.packets_inspected); + + const auto &cfg = moduleConfig.traffic_management; + const uint32_t nowMs = millis(); + + // ------------------------------------------------------------------------- + // Undecoded Packet Handling + // ------------------------------------------------------------------------- + // Packets we can't decode (wrong key, corruption, etc.) may indicate + // a misbehaving node. Track and optionally drop repeat offenders. + + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) { + if (cfg.drop_unknown_enabled && cfg.unknown_packet_threshold > 0) { + if (shouldDropUnknown(&mp, nowMs)) { + logAction("drop", &mp, "unknown"); + incrementStat(&stats.unknown_packet_drops); + ignoreRequest = true; // Suppress NAK for want_response packets + return ProcessMessage::STOP; // Consumed โ€” will not be rebroadcast + } + } + return ProcessMessage::CONTINUE; + } + + // Learn NodeInfo payloads into the dedicated PSRAM cache. + if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) + cacheNodeInfoPacket(mp); + + // ------------------------------------------------------------------------- + // NodeInfo Direct Response + // ------------------------------------------------------------------------- + // When we see a unicast NodeInfo request for a node we know about, + // respond directly from cache instead of forwarding the request. + // STOP prevents the request from being rebroadcast toward the target node, + // and our cached response is sent back to the requestor with hop_limit=0. + + if (cfg.nodeinfo_direct_response && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP && mp.decoded.want_response && + !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) { + if (shouldRespondToNodeInfo(&mp, true)) { + meshtastic_User requester = meshtastic_User_init_zero; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) { + nodeDB->updateUser(getFrom(&mp), requester, mp.channel); + } + logAction("respond", &mp, "nodeinfo-cache"); + incrementStat(&stats.nodeinfo_cache_hits); + ignoreRequest = true; // We responded; suppress default NAK + return ProcessMessage::STOP; // Consumed โ€” request will not be forwarded + } + } + + // ------------------------------------------------------------------------- + // Position Deduplication + // ------------------------------------------------------------------------- + // Drop position broadcasts that haven't moved significantly since the + // last broadcast from this node. Uses truncated coordinates to ignore + // GPS jitter within the configured precision. + + if (!isFromUs(&mp) && !isToUs(&mp)) { + if (cfg.position_dedup_enabled && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position pos = meshtastic_Position_init_zero; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &pos)) { + if (shouldDropPosition(&mp, &pos, nowMs)) { + logAction("drop", &mp, "position-dedup"); + incrementStat(&stats.position_dedup_drops); + ignoreRequest = true; // Suppress NAK + return ProcessMessage::STOP; // Consumed โ€” duplicate will not be rebroadcast + } + } + } + + // --------------------------------------------------------------------- + // Rate Limiting + // --------------------------------------------------------------------- + // Throttle nodes sending too many packets within a time window. + // Excludes routing and admin packets which are essential for mesh operation. + + if (cfg.rate_limit_enabled && cfg.rate_limit_window_secs > 0 && cfg.rate_limit_max_packets > 0) { + if (mp.decoded.portnum != meshtastic_PortNum_ROUTING_APP && mp.decoded.portnum != meshtastic_PortNum_ADMIN_APP) { + if (isRateLimited(mp.from, nowMs)) { + logAction("drop", &mp, "rate-limit"); + incrementStat(&stats.rate_limit_drops); + ignoreRequest = true; // Suppress NAK + return ProcessMessage::STOP; // Consumed โ€” throttled packet will not be rebroadcast + } + } + } + } + + return ProcessMessage::CONTINUE; +} + +void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp) +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return; + + if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) + return; + + if (isFromUs(&mp)) + return; + + // ------------------------------------------------------------------------- + // Relayed Broadcast Hop Exhaustion + // ------------------------------------------------------------------------- + // For relayed telemetry or position broadcasts from other nodes, optionally + // set hop_limit=0 so they don't propagate further through the mesh. + + const auto &cfg = moduleConfig.traffic_management; + const bool isTelemetry = mp.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP; + const bool isPosition = mp.decoded.portnum == meshtastic_PortNum_POSITION_APP; + // Only exhaust telemetry hops when channel is actually congested, mirroring the same + // airtime checks that gate self-generated telemetry in the telemetry modules. + const bool channelBusy = airTime && (!airTime->isTxAllowedChannelUtil(true) || !airTime->isTxAllowedAirUtil()); + const bool shouldExhaust = + ((channelBusy && isTelemetry && cfg.exhaust_hop_telemetry) || (isPosition && cfg.exhaust_hop_position)); + + if (!shouldExhaust || !isBroadcast(mp.to)) + return; + + if (mp.hop_limit > 0) { + const char *reason = isTelemetry ? "exhaust-hop-telemetry" : "exhaust-hop-position"; + logAction("exhaust", &mp, reason); + // Adjust hop_start so downstream nodes compute correct hopsAway (hop_start - hop_limit). + // Without this, hop_limit=0 with original hop_start would show inflated hopsAway. + mp.hop_start = mp.hop_start - mp.hop_limit + 1; + mp.hop_limit = 0; + // Signal perhapsRebroadcast() to allow one final relay with hop_limit=0. + // Without this flag, perhapsRebroadcast() would skip the packet since hop_limit==0. + // The packet-scoped flag is checked in NextHopRouter::perhapsRebroadcast() + // and forces tosend->hop_limit=0, ensuring no further propagation beyond the + // next node. + exhaustRequested = true; + exhaustRequestedFrom = getFrom(&mp); + exhaustRequestedId = mp.id; + incrementStat(&stats.hop_exhausted_packets); + } +} + +// ============================================================================= +// Periodic Maintenance +// ============================================================================= + +int32_t TrafficManagementModule::runOnce() +{ + if (!moduleConfig.has_traffic_management || !moduleConfig.traffic_management.enabled) + return INT32_MAX; + +#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + const uint32_t nowMs = millis(); + + // Check if epoch reset needed (~3.5 hours approaching 8-bit minute overflow) + if (needsEpochReset(nowMs)) { + concurrency::LockGuard guard(&cacheLock); + resetEpoch(nowMs); + return kMaintenanceIntervalMs; + } + + // Calculate TTLs for cache expiration + const uint32_t positionIntervalMs = secsToMs(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + const uint32_t positionTtlMs = positionIntervalMs * 4; + + const uint32_t rateIntervalMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + const uint32_t rateTtlMs = (rateIntervalMs > 0) ? rateIntervalMs * 2 : (10 * 60 * 1000UL); + + const uint32_t unknownTtlMs = kUnknownResetMs * 5; + + // Sweep cache and clear expired entries + uint16_t activeEntries = 0; + uint16_t expiredEntries = 0; + const uint32_t sweepStartMs = millis(); + + concurrency::LockGuard guard(&cacheLock); + for (uint16_t i = 0; i < cacheSize(); i++) { + if (cache[i].node == 0) + continue; + + bool anyValid = false; + + // Check and clear expired position data + if (cache[i].pos_time != 0) { + uint32_t posTimeMs = fromRelativePosTime(cache[i].pos_time); + if (!isWithinWindow(nowMs, posTimeMs, positionTtlMs)) { + cache[i].pos_fingerprint = 0; + cache[i].pos_time = 0; + } else { + anyValid = true; + } + } + + // Check and clear expired rate limit data + if (cache[i].rate_time != 0) { + uint32_t rateTimeMs = fromRelativeRateTime(cache[i].rate_time); + if (!isWithinWindow(nowMs, rateTimeMs, rateTtlMs)) { + cache[i].rate_count = 0; + cache[i].rate_time = 0; + } else { + anyValid = true; + } + } + + // Check and clear expired unknown tracking data + if (cache[i].unknown_time != 0) { + uint32_t unknownTimeMs = fromRelativeUnknownTime(cache[i].unknown_time); + if (!isWithinWindow(nowMs, unknownTimeMs, unknownTtlMs)) { + cache[i].unknown_count = 0; + cache[i].unknown_time = 0; + } else { + anyValid = true; + } + } + + // If all data expired, free the slot entirely + if (!anyValid) { + memset(&cache[i], 0, sizeof(UnifiedCacheEntry)); + expiredEntries++; + } else { + activeEntries++; + } + } + + TM_LOG_DEBUG("Maintenance: %u active, %u expired, %u/%u slots, %lums elapsed", activeEntries, expiredEntries, + static_cast(activeEntries), static_cast(cacheSize()), + static_cast(millis() - sweepStartMs)); + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + if (nodeInfoPayload && nodeInfoIndex) { + TM_LOG_DEBUG("NodeInfo PSRAM cache: %u/%u target (%u packed slots, %u buckets, %u-bit tags, %u-byte index)", + static_cast(countNodeInfoEntriesLocked()), static_cast(nodeInfoTargetEntries()), + static_cast(nodeInfoIndexSlots()), static_cast(nodeInfoBucketCount()), + static_cast(nodeInfoTagBits()), static_cast(nodeInfoIndexMetadataBudgetBytes())); + } +#endif + +#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 + + return kMaintenanceIntervalMs; +} + +// ============================================================================= +// Traffic Management Logic +// ============================================================================= + +bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)p; + (void)pos; + (void)nowMs; + return false; +#else + if (!pos->has_latitude_i || !pos->has_longitude_i) + return false; + + uint8_t precision = Default::getConfiguredOrDefault(moduleConfig.traffic_management.position_precision_bits, + default_traffic_mgmt_position_precision_bits); + precision = sanitizePositionPrecision(precision); + + const int32_t lat_truncated = truncateLatLon(pos->latitude_i, precision); + const int32_t lon_truncated = truncateLatLon(pos->longitude_i, precision); + const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision); + const uint32_t minIntervalMs = secsToMs(Default::getConfiguredOrDefault( + moduleConfig.traffic_management.position_min_interval_secs, default_traffic_mgmt_position_min_interval_secs)); + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); + if (!entry) + return false; + + // Compare fingerprint and check time window + // When minIntervalMs == 0, deduplication is disabled (withinInterval = false means never drop) + const bool hasPositionState = !isNew && entry->pos_time != 0; + const bool samePosition = hasPositionState && entry->pos_fingerprint == fingerprint; + const bool withinInterval = + hasPositionState && (minIntervalMs != 0) && isWithinWindow(nowMs, fromRelativePosTime(entry->pos_time), minIntervalMs); + + TM_LOG_DEBUG("Position dedup 0x%08x: fp=0x%02x prev=0x%02x same=%d within=%d new=%d", p->from, fingerprint, + entry->pos_fingerprint, samePosition, withinInterval, isNew); + + // Update cache entry + entry->pos_fingerprint = fingerprint; + entry->pos_time = toRelativePosTime(nowMs); + + // Drop only if same position AND within the minimum interval + return samePosition && withinInterval; +#endif +} + +bool TrafficManagementModule::shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse) +{ + // Caller already verified: nodeinfo_direct_response, portnum, want_response, + // !isBroadcast, !isToUs, !isFromUs + + if (!isMinHopsFromRequestor(p)) + return false; + + meshtastic_User cachedUser = meshtastic_User_init_zero; + bool hasCachedUser = false; + + // Extra metadata consumed only by the PSRAM-backed cache path. + // Defaults preserve previous behavior when cache metadata is unavailable. + bool cachedHasDecodedBitfield = false; + uint8_t cachedDecodedBitfield = 0; + uint8_t cachedSourceChannel = 0; + uint32_t cachedLastObservedMs = 0; + uint32_t cachedLastObservedRxTime = 0; + + { + concurrency::LockGuard guard(&cacheLock); + const NodeInfoPayloadEntry *entry = findNodeInfoEntry(p->to); + if (entry) { + cachedUser = entry->user; + hasCachedUser = true; + cachedHasDecodedBitfield = entry->hasDecodedBitfield; + cachedDecodedBitfield = entry->decodedBitfield; + cachedSourceChannel = entry->sourceChannel; + cachedLastObservedMs = entry->lastObservedMs; + cachedLastObservedRxTime = entry->lastObservedRxTime; + } + } + + if (!hasCachedUser) { + // If the PSRAM cache exists but misses, we intentionally do not fall back + // to the node-wide table. This keeps the PSRAM direct-reply path separate + // from NodeInfoModule/NodeDB behavior when PSRAM is available. + if (nodeInfoPayload && nodeInfoIndex) { + TM_LOG_DEBUG("NodeInfo PSRAM cache miss for node=0x%08x", p->to); + return false; + } + + // Fallback only when PSRAM cache is unavailable on this target. + // In this mode we use the node-wide table maintained by NodeInfoModule. + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to); + if (!node || !node->has_user) + return false; + cachedUser = TypeConversions::ConvertToUser(node->num, node->user); + } + + if (!sendResponse) + return true; + + meshtastic_MeshPacket *reply = router->allocForSending(); + if (!reply) { + TM_LOG_WARN("NodeInfo direct response dropped: no packet buffer"); + return false; + } + + reply->decoded.portnum = meshtastic_PortNum_NODEINFO_APP; + reply->decoded.payload.size = + pb_encode_to_bytes(reply->decoded.payload.bytes, sizeof(reply->decoded.payload.bytes), &meshtastic_User_msg, &cachedUser); + reply->decoded.want_response = false; + + // Start from cached bitfield metadata when available. This lets direct + // responses preserve more of the original packet semantics (PSRAM path), + // while still enforcing local policy for OK_TO_MQTT below. + if (cachedHasDecodedBitfield) + reply->decoded.bitfield = cachedDecodedBitfield; + else + reply->decoded.bitfield = 0; + + // Respect the node-wide config_ok_to_mqtt setting for direct NodeInfo replies. + // This response is spoofed from another node, so Router::perhapsEncode() + // will not auto-populate the bitfield via config_ok_to_mqtt for us. + reply->decoded.has_bitfield = true; + // Update only the OK_TO_MQTT bit; keep any other cached bits intact. + reply->decoded.bitfield &= ~BITFIELD_OK_TO_MQTT_MASK; + if (config.lora.config_ok_to_mqtt) + reply->decoded.bitfield |= BITFIELD_OK_TO_MQTT_MASK; + + if (hasCachedUser && cachedLastObservedMs != 0) { + uint32_t ageMs = millis() - cachedLastObservedMs; + TM_LOG_DEBUG("NodeInfo PSRAM hit node=0x%08x age=%lu ms src_ch=%u req_ch=%u rx_time=%lu", p->to, + static_cast(ageMs), static_cast(cachedSourceChannel), + static_cast(p->channel), static_cast(cachedLastObservedRxTime)); + } + + // Spoof the sender as the target node so the requestor sees a valid NodeInfo response. + // hop_limit=0 ensures this reply travels only one hop (direct to requestor). + reply->from = p->to; + reply->to = getFrom(p); + reply->channel = p->channel; + reply->decoded.request_id = p->id; + reply->hop_limit = 0; + // hop_start=0 is set explicitly because Router::send() only sets it for isFromUs(), + // and our spoofed from means isFromUs() is false. + reply->hop_start = 0; + reply->next_hop = nodeDB->getLastByteOfNodeNum(getFrom(p)); + reply->priority = meshtastic_MeshPacket_Priority_DEFAULT; + + service->sendToMesh(reply); + return true; +} + +bool TrafficManagementModule::isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const +{ + int8_t hopsAway = getHopsAway(*p, -1); + if (hopsAway < 0) + return false; + + // Both routers and clients use maxHops logic (respond when hopsAway <= threshold) + // Role determines the maximum allowed value (enforced limit, not just default) + bool isRouter = IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE); + + uint32_t roleLimit = isRouter ? kRouterDefaultMaxHops : kClientDefaultMaxHops; + uint32_t configValue = moduleConfig.traffic_management.nodeinfo_direct_response_max_hops; + + // Use config value if set, otherwise use role default, but always clamp to role limit + uint32_t maxHops = (configValue > 0) ? configValue : roleLimit; + if (maxHops > roleLimit) + maxHops = roleLimit; + + bool result = static_cast(hopsAway) <= maxHops; + TM_LOG_DEBUG("NodeInfo hops check: hopsAway=%d maxHops=%u roleLimit=%u isRouter=%d -> %s", hopsAway, maxHops, roleLimit, + isRouter, result ? "respond" : "skip"); + return result; +} + +bool TrafficManagementModule::isRateLimited(NodeNum from, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)from; + (void)nowMs; + return false; +#else + const uint32_t windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + if (windowMs == 0 || moduleConfig.traffic_management.rate_limit_max_packets == 0) + return false; + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(from, &isNew); + if (!entry) + return false; + + // Check if window has expired + if (isNew || !isWithinWindow(nowMs, fromRelativeRateTime(entry->rate_time), windowMs)) { + entry->rate_time = toRelativeRateTime(nowMs); + entry->rate_count = 1; + return false; + } + + // Increment counter (saturates at 255) + saturatingIncrement(entry->rate_count); + + // Check against threshold (uint8_t max is 255, but config is uint32_t) + uint32_t threshold = moduleConfig.traffic_management.rate_limit_max_packets; + if (threshold > 255) + threshold = 255; + + bool limited = entry->rate_count > threshold; + if (limited || entry->rate_count == threshold) { + TM_LOG_DEBUG("Rate limit 0x%08x: count=%u threshold=%u -> %s", from, entry->rate_count, threshold, + limited ? "DROP" : "at-limit"); + } + return limited; +#endif +} + +bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs) +{ +#if TRAFFIC_MANAGEMENT_CACHE_SIZE == 0 + (void)p; + (void)nowMs; + return false; +#else + if (!moduleConfig.traffic_management.drop_unknown_enabled || moduleConfig.traffic_management.unknown_packet_threshold == 0) + return false; + + uint32_t windowMs = kUnknownResetMs; + if (moduleConfig.traffic_management.rate_limit_window_secs > 0) + windowMs = secsToMs(moduleConfig.traffic_management.rate_limit_window_secs); + + bool isNew = false; + concurrency::LockGuard guard(&cacheLock); + UnifiedCacheEntry *entry = findOrCreateEntry(p->from, &isNew); + if (!entry) + return false; + + // Check if window has expired + if (isNew || !isWithinWindow(nowMs, fromRelativeUnknownTime(entry->unknown_time), windowMs)) { + entry->unknown_time = toRelativeUnknownTime(nowMs); + entry->unknown_count = 0; + } + + // Increment counter (saturates at 255) + saturatingIncrement(entry->unknown_count); + + // Check against threshold + uint32_t threshold = moduleConfig.traffic_management.unknown_packet_threshold; + if (threshold > 255) + threshold = 255; + + bool drop = entry->unknown_count > threshold; + if (drop || entry->unknown_count == threshold) { + TM_LOG_DEBUG("Unknown packets 0x%08x: count=%u threshold=%u -> %s", p->from, entry->unknown_count, threshold, + drop ? "DROP" : "at-limit"); + } + return drop; +#endif +} + +void TrafficManagementModule::logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const +{ + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + const char *name = portName(p->decoded.portnum); + if (name) { + TM_LOG_INFO("%s %s from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, name, getFrom(p), p->to, p->hop_limit, + p->hop_start, reason); + } else { + TM_LOG_INFO("%s port=%d from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, p->decoded.portnum, getFrom(p), p->to, + p->hop_limit, p->hop_start, reason); + } + } else { + TM_LOG_INFO("%s encrypted from=0x%08x to=0x%08x hop=%d/%d reason=%s", action, getFrom(p), p->to, p->hop_limit, + p->hop_start, reason); + } +} + +#endif diff --git a/src/modules/TrafficManagementModule.h b/src/modules/TrafficManagementModule.h new file mode 100644 index 000000000..fe3483a8e --- /dev/null +++ b/src/modules/TrafficManagementModule.h @@ -0,0 +1,434 @@ +#pragma once + +#include "MeshModule.h" +#include "concurrency/Lock.h" +#include "concurrency/OSThread.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include "mesh/generated/meshtastic/telemetry.pb.h" + +#if HAS_TRAFFIC_MANAGEMENT + +/** + * TrafficManagementModule - Packet inspection and traffic shaping for mesh networks. + * + * This module provides: + * - Position deduplication (drop redundant position broadcasts) + * - Per-node rate limiting (throttle chatty nodes) + * - Unknown packet filtering (drop undecoded packets from repeat offenders) + * - NodeInfo direct response (answer queries from cache to reduce mesh chatter) + * - Local-only telemetry/position (exhaust hop_limit for local broadcasts) + * - Router hop preservation (maintain hop_limit for router-to-router traffic) + * + * Memory Optimization: + * Uses a unified cache with cuckoo hashing for O(1) lookups and 56% memory reduction + * compared to separate per-feature caches. Timestamps are stored as 8-bit relative + * offsets from a rolling epoch to further reduce memory footprint. + */ +class TrafficManagementModule : public MeshModule, private concurrency::OSThread +{ + public: + TrafficManagementModule(); + ~TrafficManagementModule(); + + // Singleton โ€” no copying or moving + TrafficManagementModule(const TrafficManagementModule &) = delete; + TrafficManagementModule &operator=(const TrafficManagementModule &) = delete; + + meshtastic_TrafficManagementStats getStats() const; + void resetStats(); + void recordRouterHopPreserved(); + + /** + * Check if this packet should have its hops exhausted. + * Called from perhapsRebroadcast() to force hop_limit = 0 regardless of + * router_preserve_hops or favorite node logic. + */ + bool shouldExhaustHops(const meshtastic_MeshPacket &mp) const + { + return exhaustRequested && exhaustRequestedFrom == getFrom(&mp) && exhaustRequestedId == mp.id; + } + + protected: + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + void alterReceived(meshtastic_MeshPacket &mp) override; + int32_t runOnce() override; + // Protected so test shims can force epoch rollover behavior. + void resetEpoch(uint32_t nowMs); + + private: + // ========================================================================= + // Unified Cache Entry (10 bytes) - Same for ALL platforms + // ========================================================================= + // + // A single compact structure used across ESP32, NRF52, and all other platforms. + // Memory: 10 bytes ร— 2048 entries = 20KB + // + // Position Fingerprinting: + // Instead of storing full coordinates (8 bytes) or a computed hash, + // we store an 8-bit fingerprint derived deterministically from the + // truncated lat/lon. This extracts the lower 4 significant bits from + // each coordinate: fingerprint = (lat_low4 << 4) | lon_low4 + // + // Benefits over hash: + // - Adjacent grid cells have sequential fingerprints (no collision) + // - Two positions only collide if 16+ grid cells apart in BOTH dimensions + // - Deterministic: same input always produces same output + // + // Adaptive Timestamp Resolution: + // All timestamps use 8-bit values with adaptive resolution calculated + // from config at startup. Resolution = max(60, min(339, interval/2)). + // - Min 60 seconds ensures reasonable precision + // - Max 339 seconds allows ~24 hour range (255 * 339 = 86445 sec) + // - interval/2 ensures at least 2 ticks per configured interval + // + // Layout: + // [0-3] node - NodeNum (4 bytes) + // [4] pos_fingerprint - 4 bits lat + 4 bits lon (1 byte) + // [5] rate_count - Packets in current window (1 byte) + // [6] unknown_count - Unknown packets count (1 byte) + // [7] pos_time - Position timestamp (1 byte, adaptive resolution) + // [8] rate_time - Rate window start (1 byte, adaptive resolution) + // [9] unknown_time - Unknown tracking start (1 byte, adaptive resolution) + // + struct __attribute__((packed)) UnifiedCacheEntry { + NodeNum node; // 4 bytes - Node identifier (0 = empty slot) + uint8_t pos_fingerprint; // 1 byte - Lower 4 bits of lat + lon + uint8_t rate_count; // 1 byte - Packet count (saturates at 255) + uint8_t unknown_count; // 1 byte - Unknown packet count (saturates at 255) + uint8_t pos_time; // 1 byte - Position timestamp (adaptive resolution) + uint8_t rate_time; // 1 byte - Rate window start (adaptive resolution) + uint8_t unknown_time; // 1 byte - Unknown tracking start (adaptive resolution) + }; + static_assert(sizeof(UnifiedCacheEntry) == 10, "UnifiedCacheEntry should be 10 bytes"); + + // ========================================================================= + // Cuckoo Hash Table Implementation + // ========================================================================= + // + // Cuckoo hashing provides O(1) worst-case lookup time using two hash functions. + // Each key can be in one of two possible locations (h1 or h2). On collision, + // the existing entry is "kicked" to its alternate location. + // + // Benefits over linear scan: + // - O(1) lookup vs O(n) - critical at packet processing rates + // - O(1) insertion (amortized) with simple eviction on cycles + // - ~95% load factor achievable + // + // Cache size rounds to power-of-2 for fast modulo via bitmask. + // TRAFFIC_MANAGEMENT_CACHE_SIZE=2000 โ†’ cacheSize()=2048 + // + static constexpr uint16_t cacheSize(); + static constexpr uint16_t cacheMask(); + + // Hash functions for cuckoo hashing + inline uint16_t cuckooHash1(NodeNum node) const { return node & cacheMask(); } + inline uint16_t cuckooHash2(NodeNum node) const { return ((node * 2654435769u) >> (32 - cuckooHashBits())) & cacheMask(); } + static constexpr uint8_t cuckooHashBits(); + + // NodeInfo cache configuration (PSRAM path): + // - Payload lives in PSRAM + // - DRAM keeps packed 12-bit tags with 4-way bucketed cuckoo hashing + // (Fan et al., CoNEXT 2014). Tag value 0 is reserved as "empty". + static constexpr uint16_t kNodeInfoIndexMetadataBudgetBytes = 3072; // 3KB DRAM tag store + static constexpr uint8_t kNodeInfoTargetOccupancyPercent = 95; + static constexpr uint8_t kNodeInfoBucketSize = 4; + static constexpr uint8_t kNodeInfoTagBits = 12; + static constexpr uint16_t kNodeInfoTagMask = static_cast((1u << kNodeInfoTagBits) - 1u); + static constexpr uint16_t kNodeInfoIndexSlotsRaw = + static_cast((kNodeInfoIndexMetadataBudgetBytes * 8u) / kNodeInfoTagBits); + static constexpr uint16_t kNodeInfoIndexSlots = + static_cast(kNodeInfoIndexSlotsRaw - (kNodeInfoIndexSlotsRaw % kNodeInfoBucketSize)); + static constexpr uint16_t kNodeInfoTargetEntries = + static_cast((kNodeInfoIndexSlots * kNodeInfoTargetOccupancyPercent) / 100u); + static_assert((kNodeInfoIndexSlots % kNodeInfoBucketSize) == 0, "NodeInfo slot count must align to bucket size"); + static_assert(kNodeInfoTargetEntries < (1u << kNodeInfoTagBits), "NodeInfo tag bits must encode payload index"); + + static constexpr uint16_t nodeInfoTargetEntries(); + static constexpr uint16_t nodeInfoIndexMetadataBudgetBytes(); + static constexpr uint8_t nodeInfoTargetOccupancyPercent(); + static constexpr uint8_t nodeInfoBucketSize(); + static constexpr uint8_t nodeInfoTagBits(); + static constexpr uint16_t nodeInfoTagMask(); + static constexpr uint16_t nodeInfoIndexSlots(); + static constexpr uint16_t nodeInfoBucketCount(); + static constexpr uint16_t nodeInfoBucketMask(); + static constexpr uint8_t nodeInfoBucketHashBits(); + inline uint16_t nodeInfoHash1(NodeNum node) const { return node & nodeInfoBucketMask(); } + inline uint16_t nodeInfoHash2(NodeNum node) const + { + return ((node * 2246822519u) >> (32 - nodeInfoBucketHashBits())) & nodeInfoBucketMask(); + } + + // ========================================================================= + // Adaptive Timestamp Resolution + // ========================================================================= + // + // All timestamps use 8-bit values with adaptive resolution calculated from + // config at startup. This allows ~24 hour range while maintaining precision. + // + // Resolution formula: max(60, min(339, interval/2)) + // - 60 sec minimum ensures reasonable precision + // - 339 sec maximum allows 24 hour range (255 * 339 โ‰ˆ 86400 sec) + // - interval/2 ensures at least 2 ticks per configured interval + // + // Since config changes require reboot, resolution is calculated once. + // + uint32_t cacheEpochMs = 0; + uint16_t posTimeResolution = 60; // Seconds per tick for position + uint16_t rateTimeResolution = 60; // Seconds per tick for rate limiting + uint16_t unknownTimeResolution = 60; // Seconds per tick for unknown tracking + + // Calculate resolution from configured interval (called once at startup) + static uint16_t calcTimeResolution(uint32_t intervalSecs) + { + // Resolution = interval/2 to ensure at least 2 ticks per interval + // Clamped to [60, 339] for min precision and max 24h range + uint32_t res = (intervalSecs > 0) ? (intervalSecs / 2) : 60; + if (res < 60) + res = 60; + if (res > 339) + res = 339; + return static_cast(res); + } + + // Convert to/from 8-bit relative timestamps with given resolution + uint8_t toRelativeTime(uint32_t nowMs, uint16_t resolutionSecs) const + { + uint32_t ticks = (nowMs - cacheEpochMs) / (resolutionSecs * 1000UL); + return (ticks > UINT8_MAX) ? UINT8_MAX : static_cast(ticks); + } + uint32_t fromRelativeTime(uint8_t ticks, uint16_t resolutionSecs) const + { + return cacheEpochMs + (static_cast(ticks) * resolutionSecs * 1000UL); + } + + // Convenience wrappers for each timestamp type + uint8_t toRelativePosTime(uint32_t nowMs) const { return toRelativeTime(nowMs, posTimeResolution); } + uint32_t fromRelativePosTime(uint8_t t) const { return fromRelativeTime(t, posTimeResolution); } + + uint8_t toRelativeRateTime(uint32_t nowMs) const { return toRelativeTime(nowMs, rateTimeResolution); } + uint32_t fromRelativeRateTime(uint8_t t) const { return fromRelativeTime(t, rateTimeResolution); } + + uint8_t toRelativeUnknownTime(uint32_t nowMs) const { return toRelativeTime(nowMs, unknownTimeResolution); } + uint32_t fromRelativeUnknownTime(uint8_t t) const { return fromRelativeTime(t, unknownTimeResolution); } + + // Epoch reset when any timestamp approaches overflow + // With max resolution of 339 sec, 200 ticks = ~19 hours (safe margin for 24h max) + bool needsEpochReset(uint32_t nowMs) const + { + uint16_t maxRes = posTimeResolution; + if (rateTimeResolution > maxRes) + maxRes = rateTimeResolution; + if (unknownTimeResolution > maxRes) + maxRes = unknownTimeResolution; + return (nowMs - cacheEpochMs) > (200UL * maxRes * 1000UL); + } + // ========================================================================= + // Position Fingerprint + // ========================================================================= + // + // Computes 8-bit fingerprint from truncated lat/lon coordinates. + // Extracts lower 4 significant bits from each coordinate. + // + // fingerprint = (lat_low4 << 4) | lon_low4 + // + // Unlike a hash, adjacent grid cells have sequential fingerprints, + // so nearby positions never collide. Collisions only occur for + // positions 16+ grid cells apart in both dimensions. + // + // Guards: If precision < 4 bits, uses min(precision, 4) bits. + // + static uint8_t computePositionFingerprint(int32_t lat_truncated, int32_t lon_truncated, uint8_t precision); + + // ========================================================================= + // Cache Storage + // ========================================================================= + + mutable concurrency::Lock cacheLock; // Protects all cache access + UnifiedCacheEntry *cache = nullptr; // Cuckoo hash table (unified for all platforms) + bool cacheFromPsram = false; // Tracks allocator for correct deallocation + + struct NodeInfoPayloadEntry { + // Node identifier associated with this payload slot. + // 0 means the slot is currently unused. + NodeNum node; + + // Cached NODEINFO_APP payload body. This is separate from NodeDB and is only + // used by the PSRAM-backed direct-response path in this module. + meshtastic_User user; + + // Extra response metadata captured from the latest observed NODEINFO_APP + // packet for this node. shouldRespondToNodeInfo() uses this metadata when + // building spoofed replies for requesting clients. + + // Last local uptime tick (millis) when this entry was refreshed. + uint32_t lastObservedMs; + + // Last RTC/packet timestamp (seconds) observed for this NodeInfo frame. + // If unavailable in packet, remains 0. + uint32_t lastObservedRxTime; + + // Channel where we most recently heard this node's NodeInfo. + uint8_t sourceChannel; + + // Cached decoded bitfield metadata from the source packet. + // We preserve non-OK_TO_MQTT bits in direct replies when available. + bool hasDecodedBitfield; + uint8_t decodedBitfield; + }; + + NodeInfoPayloadEntry *nodeInfoPayload = nullptr; // NodeInfo payloads in PSRAM + bool nodeInfoPayloadFromPsram = false; // Tracks allocator for correct deallocation + uint8_t *nodeInfoIndex = nullptr; // Packed 12-bit NodeInfo tags in DRAM + uint16_t nodeInfoAllocHint = 0; + uint16_t nodeInfoEvictCursor = 0; + + meshtastic_TrafficManagementStats stats; + + // Flag set during alterReceived() when packet should be exhausted. + // Checked by perhapsRebroadcast() to force hop_limit = 0 only for the + // matching packet key (from + id). Reset at start of handleReceived(). + bool exhaustRequested = false; + NodeNum exhaustRequestedFrom = 0; + PacketId exhaustRequestedId = 0; + + // ========================================================================= + // Cache Operations + // ========================================================================= + + // Find or create entry for node using cuckoo hashing + // Returns nullptr if cache is full and eviction fails + UnifiedCacheEntry *findOrCreateEntry(NodeNum node, bool *isNew); + + // Find existing entry (no creation) + UnifiedCacheEntry *findEntry(NodeNum node); + + // NodeInfo cache operations (bucketed cuckoo index + PSRAM payloads) + const NodeInfoPayloadEntry *findNodeInfoEntry(NodeNum node) const; + NodeInfoPayloadEntry *findOrCreateNodeInfoEntry(NodeNum node, bool *usedEmptySlot); + uint16_t findNodeInfoPayloadIndex(NodeNum node) const; + bool removeNodeInfoIndexEntry(NodeNum node, uint16_t payloadIndex); + uint16_t allocateNodeInfoPayloadSlot(); + uint16_t evictNodeInfoPayloadSlot(); + bool tryInsertNodeInfoEntryInBucket(uint16_t bucket, uint16_t tag); + uint16_t encodeNodeInfoTag(uint16_t payloadIndex) const; + uint16_t decodeNodeInfoPayloadIndex(uint16_t tag) const; + uint16_t getNodeInfoTag(uint16_t slot) const; + void setNodeInfoTag(uint16_t slot, uint16_t tag); + uint16_t countNodeInfoEntriesLocked() const; + void cacheNodeInfoPacket(const meshtastic_MeshPacket &mp); + + // ========================================================================= + // Traffic Management Logic + // ========================================================================= + + bool shouldDropPosition(const meshtastic_MeshPacket *p, const meshtastic_Position *pos, uint32_t nowMs); + bool shouldRespondToNodeInfo(const meshtastic_MeshPacket *p, bool sendResponse); + bool isMinHopsFromRequestor(const meshtastic_MeshPacket *p) const; + bool isRateLimited(NodeNum from, uint32_t nowMs); + bool shouldDropUnknown(const meshtastic_MeshPacket *p, uint32_t nowMs); + + void logAction(const char *action, const meshtastic_MeshPacket *p, const char *reason) const; + void incrementStat(uint32_t *field); +}; + +// ========================================================================= +// Compile-time Cache Size Calculations +// ========================================================================= +// +// Round TRAFFIC_MANAGEMENT_CACHE_SIZE up to next power of 2 for efficient +// cuckoo hash indexing (allows bitmask instead of modulo). +// +// These use C++11-compatible constexpr (single return statement). +// + +namespace detail +{ +// Helper: round up to next power of 2 using bit manipulation +constexpr uint16_t nextPow2(uint16_t n) +{ + return n == 0 ? 0 : (((n - 1) | ((n - 1) >> 1) | ((n - 1) >> 2) | ((n - 1) >> 4) | ((n - 1) >> 8)) + 1); +} + +// Helper: floor(log2(n)) for n >= 0, C++11-compatible constexpr. +constexpr uint8_t log2Floor(uint16_t n) +{ + return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n >> 1))); +} + +// Helper: ceil(log2(n)) for n >= 1, C++11-compatible constexpr. +constexpr uint8_t log2Ceil(uint16_t n) +{ + return n <= 1 ? 0 : static_cast(1 + log2Floor(static_cast(n - 1))); +} +} // namespace detail + +constexpr uint16_t TrafficManagementModule::cacheSize() +{ + return detail::nextPow2(TRAFFIC_MANAGEMENT_CACHE_SIZE); +} + +constexpr uint16_t TrafficManagementModule::cacheMask() +{ + return cacheSize() > 0 ? cacheSize() - 1 : 0; +} + +constexpr uint8_t TrafficManagementModule::cuckooHashBits() +{ + return detail::log2Floor(cacheSize()); +} + +constexpr uint16_t TrafficManagementModule::nodeInfoTargetEntries() +{ + return kNodeInfoTargetEntries; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoIndexMetadataBudgetBytes() +{ + return kNodeInfoIndexMetadataBudgetBytes; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoTargetOccupancyPercent() +{ + return kNodeInfoTargetOccupancyPercent; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoBucketSize() +{ + return kNodeInfoBucketSize; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoTagBits() +{ + return kNodeInfoTagBits; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoTagMask() +{ + return kNodeInfoTagMask; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoIndexSlots() +{ + return kNodeInfoIndexSlots; +} + +constexpr uint16_t TrafficManagementModule::nodeInfoBucketCount() +{ + return static_cast(nodeInfoIndexSlots() / nodeInfoBucketSize()); +} + +constexpr uint16_t TrafficManagementModule::nodeInfoBucketMask() +{ + return nodeInfoBucketCount() > 0 ? nodeInfoBucketCount() - 1 : 0; +} + +constexpr uint8_t TrafficManagementModule::nodeInfoBucketHashBits() +{ + return detail::log2Floor(nodeInfoBucketCount()); +} + +extern TrafficManagementModule *trafficManagementModule; + +#endif diff --git a/src/modules/esp32/AudioModule.cpp b/src/modules/esp32/AudioModule.cpp index 77cc94359..37e3e9184 100644 --- a/src/modules/esp32/AudioModule.cpp +++ b/src/modules/esp32/AudioModule.cpp @@ -100,7 +100,7 @@ AudioModule::AudioModule() : SinglePortModule("Audio", meshtastic_PortNum_AUDIO_ // moduleConfig.audio.i2s_sck = 14; // moduleConfig.audio.ptt_pin = 39; - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { LOG_INFO("Set up codec2 in mode %u", (moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); codec2 = codec2_create((moduleConfig.audio.bitrate ? moduleConfig.audio.bitrate : AUDIO_MODULE_MODE) - 1); memcpy(tx_header.magic, c2_magic, sizeof(c2_magic)); @@ -143,7 +143,7 @@ void AudioModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int int32_t AudioModule::runOnce() { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { esp_err_t res; if (firstTime) { // Set up I2S Processor configuration. This will produce 16bit samples at 8 kHz instead of 12 from the ADC @@ -270,7 +270,7 @@ void AudioModule::sendPayload(NodeNum dest, bool wantReplies) ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp) { - if ((moduleConfig.audio.codec2_enabled) && (myRegion->audioPermitted)) { + if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) { auto &p = mp.decoded; if (!isFromUs(&mp)) { memcpy(rx_encode_frame, p.payload.bytes, p.payload.size); diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h old mode 100755 new mode 100644 index f08ee00f9..d2205fd2a --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -4,12 +4,15 @@ #include "configuration.h" -#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER #include "../concurrency/OSThread.h" #ifdef HAS_BMA423 #include "BMA423Sensor.h" #endif +#ifdef HAS_BMI270 +#include "BMI270Sensor.h" +#endif #include "BMM150Sensor.h" #include "BMX160Sensor.h" #include "ICM20948Sensor.h" @@ -111,6 +114,11 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::BMM150: sensor = new BMM150Sensor(device); break; +#ifdef HAS_BMI270 + case ScanI2C::DeviceType::BMI270: + sensor = new BMI270Sensor(device); + break; +#endif #ifdef HAS_QMA6100P case ScanI2C::DeviceType::QMA6100P: sensor = new QMA6100PSensor(device); diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/BMA423Sensor.h b/src/motion/BMA423Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/BMI270Sensor.cpp b/src/motion/BMI270Sensor.cpp new file mode 100644 index 000000000..bc547529d --- /dev/null +++ b/src/motion/BMI270Sensor.cpp @@ -0,0 +1,605 @@ +#include "BMI270Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) + +#include + +// BMI270 registers used +#define BMI270_REG_ACC_X_LSB 0x0C +#define BMI270_REG_INTERNAL_STATUS 0x21 +#define BMI270_REG_ACC_CONF 0x40 +#define BMI270_REG_ACC_RANGE 0x41 +#define BMI270_REG_INIT_CTRL 0x59 +#define BMI270_REG_INIT_ADDR_0 0x5B +#define BMI270_REG_INIT_ADDR_1 0x5C +#define BMI270_REG_INIT_DATA 0x5E +#define BMI270_REG_PWR_CONF 0x7C +#define BMI270_REG_PWR_CTRL 0x7D +#define BMI270_REG_CMD 0x7E + +// Commands and configuration values +#define BMI270_CMD_SOFTRESET 0xB6 +#define BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED 0x00 +#define BMI270_PWR_CTRL_ACC_EN 0x04 +#define BMI270_ACC_ODR_50HZ 0x07 +#define BMI270_ACC_BWP_NORMAL 0x20 +#define BMI270_ACC_FILTER_PERF 0x80 +#define BMI270_ACC_RANGE_2G 0x00 +#define BMI270_INIT_OK 0x01 + +// BMI270 config file - 8192 bytes from official Bosch BMI270 API +static const uint8_t bmi270_config_file[] PROGMEM = { + 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x3d, 0xb1, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x91, 0x03, 0x80, 0x2e, 0xbc, 0xb0, 0x80, + 0x2e, 0xa3, 0x03, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x00, 0xb0, 0x50, 0x30, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0x21, 0x2e, + 0x6a, 0xf5, 0x80, 0x2e, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x01, 0x00, 0x22, 0x00, 0x75, 0x00, 0x00, 0x10, 0x00, + 0x10, 0xd1, 0x00, 0xb3, 0x43, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0xe0, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x19, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xe0, 0xaa, 0x38, 0x05, 0xe0, 0x90, 0x30, 0xfa, 0x00, + 0x96, 0x00, 0x4b, 0x09, 0x11, 0x00, 0x11, 0x00, 0x02, 0x00, 0x2d, 0x01, 0xd4, 0x7b, 0x3b, 0x01, 0xdb, 0x7a, 0x04, 0x00, 0x3f, + 0x7b, 0xcd, 0x6c, 0xc3, 0x04, 0x85, 0x09, 0xc3, 0x04, 0xec, 0xe6, 0x0c, 0x46, 0x01, 0x00, 0x27, 0x00, 0x19, 0x00, 0x96, 0x00, + 0xa0, 0x00, 0x01, 0x00, 0x0c, 0x00, 0xf0, 0x3c, 0x00, 0x01, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x32, + 0x00, 0x05, 0x00, 0xee, 0x06, 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x04, 0x00, 0xa8, 0x05, 0xee, 0x06, 0x00, 0x04, 0xbc, 0x02, + 0xb3, 0x00, 0x85, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xb4, 0x00, 0x01, 0x00, 0xb9, 0x00, 0x01, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0xde, 0x00, 0xeb, 0x00, 0xda, 0x00, 0x00, 0x0c, 0xff, 0x0f, 0x00, 0x04, 0xc0, + 0x00, 0x5b, 0xf5, 0xc9, 0x01, 0x1e, 0xf2, 0x80, 0x00, 0x3f, 0xff, 0x19, 0xf4, 0x58, 0xf5, 0x66, 0xf5, 0x64, 0xf5, 0xc0, 0xf1, + 0xf0, 0x00, 0xe0, 0x00, 0xcd, 0x01, 0xd3, 0x01, 0xdb, 0x01, 0xff, 0x7f, 0xff, 0x01, 0xe4, 0x00, 0x74, 0xf7, 0xf3, 0x00, 0xfa, + 0x00, 0xff, 0x3f, 0xca, 0x03, 0x6c, 0x38, 0x56, 0xfe, 0x44, 0xfd, 0xbc, 0x02, 0xf9, 0x06, 0x00, 0xfc, 0x12, 0x02, 0xae, 0x01, + 0x58, 0xfa, 0x9a, 0xfd, 0x77, 0x05, 0xbb, 0x02, 0x96, 0x01, 0x95, 0x01, 0x7f, 0x01, 0x82, 0x01, 0x89, 0x01, 0x87, 0x01, 0x88, + 0x01, 0x8a, 0x01, 0x8c, 0x01, 0x8f, 0x01, 0x8d, 0x01, 0x92, 0x01, 0x91, 0x01, 0xdd, 0x00, 0x9f, 0x01, 0x7e, 0x01, 0xdb, 0x00, + 0xb6, 0x01, 0x70, 0x69, 0x26, 0xd3, 0x9c, 0x07, 0x1f, 0x05, 0x9d, 0x00, 0x00, 0x08, 0xbc, 0x05, 0x37, 0xfa, 0xa2, 0x01, 0xaa, + 0x01, 0xa1, 0x01, 0xa8, 0x01, 0xa0, 0x01, 0xa8, 0x05, 0xb4, 0x01, 0xb4, 0x01, 0xce, 0x00, 0xd0, 0x00, 0xfc, 0x00, 0xc5, 0x01, + 0xff, 0xfb, 0xb1, 0x00, 0x00, 0x38, 0x00, 0x30, 0xfd, 0xf5, 0xfc, 0xf5, 0xcd, 0x01, 0xa0, 0x00, 0x5f, 0xff, 0x00, 0x40, 0xff, + 0x00, 0x00, 0x80, 0x6d, 0x0f, 0xeb, 0x00, 0x7f, 0xff, 0xc2, 0xf5, 0x68, 0xf7, 0xb3, 0xf1, 0x67, 0x0f, 0x5b, 0x0f, 0x61, 0x0f, + 0x80, 0x0f, 0x58, 0xf7, 0x5b, 0xf7, 0x83, 0x0f, 0x86, 0x00, 0x72, 0x0f, 0x85, 0x0f, 0xc6, 0xf1, 0x7f, 0x0f, 0x6c, 0xf7, 0x00, + 0xe0, 0x00, 0xff, 0xd1, 0xf5, 0x87, 0x0f, 0x8a, 0x0f, 0xff, 0x03, 0xf0, 0x3f, 0x8b, 0x00, 0x8e, 0x00, 0x90, 0x00, 0xb9, 0x00, + 0x2d, 0xf5, 0xca, 0xf5, 0xcb, 0x01, 0x20, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x50, 0x98, + 0x2e, 0xd7, 0x0e, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0xf0, 0x7f, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x00, 0x2e, + 0x01, 0x80, 0x08, 0xa2, 0xfb, 0x2f, 0x98, 0x2e, 0xba, 0x03, 0x21, 0x2e, 0x19, 0x00, 0x01, 0x2e, 0xee, 0x00, 0x00, 0xb2, 0x07, + 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x03, 0x2f, 0x01, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x07, 0xcc, 0x01, 0x2e, 0xdd, 0x00, + 0x00, 0xb2, 0x27, 0x2f, 0x05, 0x2e, 0x8a, 0x00, 0x05, 0x52, 0x98, 0x2e, 0xc7, 0xc1, 0x03, 0x2e, 0xe9, 0x00, 0x40, 0xb2, 0xf0, + 0x7f, 0x08, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xe9, 0x00, 0x98, 0x2e, 0xb4, 0xb1, + 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x10, 0x2f, 0x05, 0x50, 0x98, 0x2e, 0x4d, 0xc3, 0x05, 0x50, 0x98, 0x2e, 0x5a, 0xc7, 0x98, + 0x2e, 0xf9, 0xb4, 0x98, 0x2e, 0x54, 0xb2, 0x98, 0x2e, 0x67, 0xb6, 0x98, 0x2e, 0x17, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, + 0x01, 0x2e, 0xef, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x7a, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xef, 0x00, 0x01, 0x2e, 0xd4, + 0x00, 0x04, 0xae, 0x0b, 0x2f, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x05, 0x52, 0x98, 0x2e, 0x8e, 0x0e, 0x00, 0xb2, + 0x02, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x01, 0x2e, 0x7d, 0x00, 0x00, 0x90, 0x90, 0x2e, 0xf1, 0x02, 0x01, 0x2e, 0xd7, + 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x2f, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7b, 0x00, 0x00, 0xb2, + 0x12, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0x90, 0x02, 0x2f, 0x98, 0x2e, 0x1f, 0x0e, 0x09, 0x2d, 0x98, 0x2e, 0x81, 0x0d, 0x01, + 0x2e, 0xd4, 0x00, 0x04, 0x90, 0x02, 0x2f, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, + 0x7c, 0x00, 0x00, 0xb2, 0x90, 0x2e, 0x09, 0x03, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x31, 0x01, 0x08, 0x00, 0xb2, 0x04, 0x2f, 0x98, + 0x2e, 0x47, 0xcb, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x61, 0x2f, + 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x98, 0xbc, 0x98, 0xb8, 0x05, 0xb2, 0x0f, 0x58, 0x23, 0x2f, 0x07, 0x90, 0x09, + 0x54, 0x00, 0x30, 0x37, 0x2f, 0x15, 0x41, 0x04, 0x41, 0xdc, 0xbe, 0x44, 0xbe, 0xdc, 0xba, 0x2c, 0x01, 0x61, 0x00, 0x0f, 0x56, + 0x4a, 0x0f, 0x0c, 0x2f, 0xd1, 0x42, 0x94, 0xb8, 0xc1, 0x42, 0x11, 0x30, 0x05, 0x2e, 0x6a, 0xf7, 0x2c, 0xbd, 0x2f, 0xb9, 0x80, + 0xb2, 0x08, 0x22, 0x98, 0x2e, 0xc3, 0xb7, 0x21, 0x2d, 0x61, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x98, 0x2e, 0xc3, 0xb7, 0x00, 0x30, + 0x21, 0x2e, 0x5a, 0xf5, 0x18, 0x2d, 0xe1, 0x7f, 0x50, 0x30, 0x98, 0x2e, 0xfa, 0x03, 0x0f, 0x52, 0x07, 0x50, 0x50, 0x42, 0x70, + 0x30, 0x0d, 0x54, 0x42, 0x42, 0x7e, 0x82, 0xe2, 0x6f, 0x80, 0xb2, 0x42, 0x42, 0x05, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x30, + 0x98, 0x2e, 0xc3, 0xb7, 0x03, 0x2d, 0x60, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x06, 0x90, 0x18, 0x2f, 0x01, + 0x2e, 0x76, 0x00, 0x0b, 0x54, 0x07, 0x52, 0xe0, 0x7f, 0x98, 0x2e, 0x7a, 0xc1, 0xe1, 0x6f, 0x08, 0x1a, 0x40, 0x30, 0x08, 0x2f, + 0x21, 0x2e, 0xd4, 0x00, 0x20, 0x30, 0x98, 0x2e, 0xaf, 0xb7, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x05, 0x2d, 0x98, 0x2e, 0x38, + 0x0e, 0x00, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x18, 0x2d, 0x01, 0x2e, 0xd4, 0x00, 0x03, 0xaa, + 0x01, 0x2f, 0x98, 0x2e, 0x45, 0x0e, 0x01, 0x2e, 0xd4, 0x00, 0x3f, 0x80, 0x03, 0xa2, 0x01, 0x2f, 0x00, 0x2e, 0x02, 0x2d, 0x98, + 0x2e, 0x5b, 0x0e, 0x30, 0x30, 0x98, 0x2e, 0xce, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, + 0x01, 0x2e, 0x77, 0x00, 0x00, 0xb2, 0x24, 0x2f, 0x98, 0x2e, 0xf5, 0xcb, 0x03, 0x2e, 0xd5, 0x00, 0x11, 0x54, 0x01, 0x0a, 0xbc, + 0x84, 0x83, 0x86, 0x21, 0x2e, 0xc9, 0x01, 0xe0, 0x40, 0x13, 0x52, 0xc4, 0x40, 0x82, 0x40, 0xa8, 0xb9, 0x52, 0x42, 0x43, 0xbe, + 0x53, 0x42, 0x04, 0x0a, 0x50, 0x42, 0xe1, 0x7f, 0xf0, 0x31, 0x41, 0x40, 0xf2, 0x6f, 0x25, 0xbd, 0x08, 0x08, 0x02, 0x0a, 0xd0, + 0x7f, 0x98, 0x2e, 0xa8, 0xcf, 0x06, 0xbc, 0xd1, 0x6f, 0xe2, 0x6f, 0x08, 0x0a, 0x80, 0x42, 0x98, 0x2e, 0x58, 0xb7, 0x00, 0x30, + 0x21, 0x2e, 0xee, 0x00, 0x21, 0x2e, 0x77, 0x00, 0x21, 0x2e, 0xdd, 0x00, 0x80, 0x2e, 0xf4, 0x01, 0x1a, 0x24, 0x22, 0x00, 0x80, + 0x2e, 0xec, 0x01, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0xfb, 0x6f, 0x01, 0x30, 0x71, 0x54, 0x11, 0x42, + 0x42, 0x0e, 0xfc, 0x2f, 0xc0, 0x2e, 0x01, 0x42, 0xf0, 0x5f, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x01, 0x34, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x06, 0x32, 0x0f, 0x2e, 0x61, 0xf5, 0xfe, 0x09, 0xc0, 0xb3, 0x04, 0x2f, 0x17, 0x30, 0x2f, 0x2e, + 0xef, 0x00, 0x2d, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x46, + 0x30, 0x0f, 0x2e, 0xa4, 0xf1, 0xbe, 0x09, 0x80, 0xb3, 0x06, 0x2f, 0x0d, 0x2e, 0xd4, 0x00, 0x84, 0xaf, 0x02, 0x2f, 0x16, 0x30, + 0x2d, 0x2e, 0x7b, 0x00, 0x86, 0x30, 0x2d, 0x2e, 0x60, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x01, 0x2e, 0x77, + 0xf7, 0x09, 0xbc, 0x0f, 0xb8, 0x00, 0xb2, 0x10, 0x50, 0xfb, 0x7f, 0x10, 0x30, 0x0b, 0x2f, 0x03, 0x2e, 0x8a, 0x00, 0x96, 0xbc, + 0x9f, 0xb8, 0x40, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0x68, 0xf7, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x07, 0x2f, 0x03, 0x2e, 0x7e, + 0x00, 0x41, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xdc, 0x03, 0x03, 0x2c, 0x00, 0x30, 0x21, 0x2e, 0x7e, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, + 0xb8, 0x2e, 0x20, 0x50, 0xe0, 0x7f, 0xfb, 0x7f, 0x00, 0x2e, 0x27, 0x50, 0x98, 0x2e, 0x3b, 0xc8, 0x29, 0x50, 0x98, 0x2e, 0xa7, + 0xc8, 0x01, 0x50, 0x98, 0x2e, 0x55, 0xcc, 0xe1, 0x6f, 0x2b, 0x50, 0x98, 0x2e, 0xe0, 0xc9, 0xfb, 0x6f, 0x00, 0x30, 0xe0, 0x5f, + 0x21, 0x2e, 0x7e, 0x00, 0xb8, 0x2e, 0x73, 0x50, 0x01, 0x30, 0x57, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x21, + 0x2e, 0x59, 0xf5, 0x10, 0x30, 0xc0, 0x2e, 0x21, 0x2e, 0x4a, 0xf1, 0x90, 0x50, 0xf7, 0x7f, 0xe6, 0x7f, 0xd5, 0x7f, 0xc4, 0x7f, + 0xb3, 0x7f, 0xa1, 0x7f, 0x90, 0x7f, 0x82, 0x7f, 0x7b, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x90, 0x2e, 0x97, 0xb0, 0x03, + 0x2e, 0x8f, 0x00, 0x07, 0x2e, 0x91, 0x00, 0x05, 0x2e, 0xb1, 0x00, 0x3f, 0xba, 0x9f, 0xb8, 0x01, 0x2e, 0xb1, 0x00, 0xa3, 0xbd, + 0x4c, 0x0a, 0x05, 0x2e, 0xb1, 0x00, 0x04, 0xbe, 0xbf, 0xb9, 0xcb, 0x0a, 0x4f, 0xba, 0x22, 0xbd, 0x01, 0x2e, 0xb3, 0x00, 0xdc, + 0x0a, 0x2f, 0xb9, 0x03, 0x2e, 0xb8, 0x00, 0x0a, 0xbe, 0x9a, 0x0a, 0xcf, 0xb9, 0x9b, 0xbc, 0x01, 0x2e, 0x97, 0x00, 0x9f, 0xb8, + 0x93, 0x0a, 0x0f, 0xbc, 0x91, 0x0a, 0x0f, 0xb8, 0x90, 0x0a, 0x25, 0x2e, 0x18, 0x00, 0x05, 0x2e, 0xc1, 0xf5, 0x2e, 0xbd, 0x2e, + 0xb9, 0x01, 0x2e, 0x19, 0x00, 0x31, 0x30, 0x8a, 0x04, 0x00, 0x90, 0x07, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xa2, 0x03, 0x2f, + 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x0c, 0x2f, 0x19, 0x50, 0x05, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x05, 0x2e, 0x78, 0x00, 0x80, + 0x90, 0x10, 0x30, 0x01, 0x2f, 0x21, 0x2e, 0x78, 0x00, 0x25, 0x2e, 0xdd, 0x00, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x02, 0x30, + 0x01, 0x30, 0x04, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x00, 0x2f, 0x21, 0x30, 0x01, 0x2e, 0xea, 0x00, 0x08, 0x1a, 0x0e, + 0x2f, 0x23, 0x2e, 0xea, 0x00, 0x33, 0x30, 0x1b, 0x50, 0x0b, 0x09, 0x01, 0x40, 0x17, 0x56, 0x46, 0xbe, 0x4b, 0x08, 0x4c, 0x0a, + 0x01, 0x42, 0x0a, 0x80, 0x15, 0x52, 0x01, 0x42, 0x00, 0x2e, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x1f, 0x2f, 0x03, 0x2e, 0xc0, + 0xf5, 0xf0, 0x30, 0x48, 0x08, 0x47, 0xaa, 0x74, 0x30, 0x07, 0x2e, 0x7a, 0x00, 0x61, 0x22, 0x4b, 0x1a, 0x05, 0x2f, 0x07, 0x2e, + 0x66, 0xf5, 0xbf, 0xbd, 0xbf, 0xb9, 0xc0, 0x90, 0x0b, 0x2f, 0x1d, 0x56, 0x2b, 0x30, 0xd2, 0x42, 0xdb, 0x42, 0x01, 0x04, 0xc2, + 0x42, 0x04, 0xbd, 0xfe, 0x80, 0x81, 0x84, 0x23, 0x2e, 0x7a, 0x00, 0x02, 0x42, 0x02, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x05, 0x2e, + 0xd6, 0x00, 0x81, 0x84, 0x25, 0x2e, 0xd6, 0x00, 0x02, 0x31, 0x25, 0x2e, 0x60, 0xf5, 0x05, 0x2e, 0x8a, 0x00, 0x0b, 0x50, 0x90, + 0x08, 0x80, 0xb2, 0x0b, 0x2f, 0x05, 0x2e, 0xca, 0xf5, 0xf0, 0x3e, 0x90, 0x08, 0x25, 0x2e, 0xca, 0xf5, 0x05, 0x2e, 0x59, 0xf5, + 0xe0, 0x3f, 0x90, 0x08, 0x25, 0x2e, 0x59, 0xf5, 0x90, 0x6f, 0xa1, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0xe6, 0x6f, 0xf7, + 0x6f, 0x7b, 0x6f, 0x82, 0x6f, 0x70, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0x90, 0x7f, 0xe5, 0x7f, 0xd4, 0x7f, 0xc3, 0x7f, 0xb1, 0x7f, + 0xa2, 0x7f, 0x87, 0x7f, 0xf6, 0x7f, 0x7b, 0x7f, 0x00, 0x2e, 0x01, 0x2e, 0x60, 0xf5, 0x60, 0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x02, + 0x30, 0x63, 0x6f, 0x15, 0x52, 0x50, 0x7f, 0x62, 0x7f, 0x5a, 0x2c, 0x02, 0x32, 0x1a, 0x09, 0x00, 0xb3, 0x14, 0x2f, 0x00, 0xb2, + 0x03, 0x2f, 0x09, 0x2e, 0x18, 0x00, 0x00, 0x91, 0x0c, 0x2f, 0x43, 0x7f, 0x98, 0x2e, 0x97, 0xb7, 0x1f, 0x50, 0x02, 0x8a, 0x02, + 0x32, 0x04, 0x30, 0x25, 0x2e, 0x64, 0xf5, 0x15, 0x52, 0x50, 0x6f, 0x43, 0x6f, 0x44, 0x43, 0x25, 0x2e, 0x60, 0xf5, 0xd9, 0x08, + 0xc0, 0xb2, 0x36, 0x2f, 0x98, 0x2e, 0x3e, 0xb7, 0x00, 0xb2, 0x06, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x02, 0x2f, 0x50, + 0x6f, 0x00, 0x90, 0x0a, 0x2f, 0x01, 0x2e, 0x79, 0x00, 0x00, 0x90, 0x19, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x79, 0x00, 0x00, 0x30, + 0x98, 0x2e, 0xdc, 0x03, 0x13, 0x2d, 0x01, 0x2e, 0xc3, 0xf5, 0x0c, 0xbc, 0x0f, 0xb8, 0x12, 0x30, 0x10, 0x04, 0x03, 0xb0, 0x26, + 0x25, 0x21, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x10, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x02, 0x30, 0x60, 0x7f, 0x25, 0x2e, + 0x79, 0x00, 0x60, 0x6f, 0x00, 0x90, 0x05, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xea, 0x00, 0x15, 0x50, 0x21, 0x2e, 0x64, 0xf5, 0x15, + 0x52, 0x23, 0x2e, 0x60, 0xf5, 0x02, 0x32, 0x50, 0x6f, 0x00, 0x90, 0x02, 0x2f, 0x03, 0x30, 0x27, 0x2e, 0x78, 0x00, 0x07, 0x2e, + 0x60, 0xf5, 0x1a, 0x09, 0x00, 0x91, 0xa3, 0x2f, 0x19, 0x09, 0x00, 0x91, 0xa0, 0x2f, 0x90, 0x6f, 0xa2, 0x6f, 0xb1, 0x6f, 0xc3, + 0x6f, 0xd4, 0x6f, 0xe5, 0x6f, 0x7b, 0x6f, 0xf6, 0x6f, 0x87, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, + 0x26, 0x30, 0x0f, 0x2e, 0x61, 0xf5, 0x2f, 0x2e, 0x7c, 0x00, 0x0f, 0x2e, 0x7c, 0x00, 0xbe, 0x09, 0xa2, 0x7f, 0x80, 0x7f, 0x80, + 0xb3, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0x91, 0x7f, 0x7b, 0x7f, 0x0b, 0x2f, 0x23, 0x50, 0x1a, 0x25, 0x12, 0x40, 0x42, 0x7f, + 0x74, 0x82, 0x12, 0x40, 0x52, 0x7f, 0x00, 0x2e, 0x00, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0x6a, 0xd6, 0x81, 0x30, 0x01, 0x2e, 0x7c, + 0x00, 0x01, 0x08, 0x00, 0xb2, 0x42, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0x89, 0x00, 0x97, 0xbc, 0x06, 0xbc, 0x9f, 0xb8, + 0x0f, 0xb8, 0x00, 0x90, 0x23, 0x2e, 0xd8, 0x00, 0x10, 0x30, 0x01, 0x30, 0x2a, 0x2f, 0x03, 0x2e, 0xd4, 0x00, 0x44, 0xb2, 0x05, + 0x2f, 0x47, 0xb2, 0x00, 0x30, 0x2d, 0x2f, 0x21, 0x2e, 0x7c, 0x00, 0x2b, 0x2d, 0x03, 0x2e, 0xfd, 0xf5, 0x9e, 0xbc, 0x9f, 0xb8, + 0x40, 0x90, 0x14, 0x2f, 0x03, 0x2e, 0xfc, 0xf5, 0x99, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0x49, 0xf1, 0x25, + 0x54, 0x4a, 0x08, 0x40, 0x90, 0x08, 0x2f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x10, 0x30, 0x03, 0x2f, 0x50, 0x30, 0x21, 0x2e, + 0xd4, 0x00, 0x10, 0x2d, 0x98, 0x2e, 0xaf, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7c, 0x00, 0x0a, 0x2d, 0x05, 0x2e, 0x69, 0xf7, 0x2d, + 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x01, 0x2f, 0x21, 0x2e, 0x7d, 0x00, 0x23, 0x2e, 0x7c, 0x00, 0xe0, 0x31, 0x21, 0x2e, 0x61, 0xf5, + 0xf6, 0x6f, 0xe7, 0x6f, 0x80, 0x6f, 0xa2, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0x7b, 0x6f, 0x91, 0x6f, 0x40, 0x5f, 0xc8, + 0x2e, 0x60, 0x51, 0x0a, 0x25, 0x36, 0x88, 0xf4, 0x7f, 0xeb, 0x7f, 0x00, 0x32, 0x31, 0x52, 0x32, 0x30, 0x13, 0x30, 0x98, 0x2e, + 0x15, 0xcb, 0x0a, 0x25, 0x33, 0x84, 0xd2, 0x7f, 0x43, 0x30, 0x05, 0x50, 0x2d, 0x52, 0x98, 0x2e, 0x95, 0xc1, 0xd2, 0x6f, 0x27, + 0x52, 0x98, 0x2e, 0xd7, 0xc7, 0x2a, 0x25, 0xb0, 0x86, 0xc0, 0x7f, 0xd3, 0x7f, 0xaf, 0x84, 0x29, 0x50, 0xf1, 0x6f, 0x98, 0x2e, + 0x4d, 0xc8, 0x2a, 0x25, 0xae, 0x8a, 0xaa, 0x88, 0xf2, 0x6e, 0x2b, 0x50, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x7f, 0x98, 0x2e, 0xb6, + 0xc8, 0xe0, 0x6e, 0x00, 0xb2, 0x32, 0x2f, 0x33, 0x54, 0x83, 0x86, 0xf1, 0x6f, 0xc3, 0x7f, 0x04, 0x30, 0x30, 0x30, 0xf4, 0x7f, + 0xd0, 0x7f, 0xb2, 0x7f, 0xe3, 0x30, 0xc5, 0x6f, 0x56, 0x40, 0x45, 0x41, 0x28, 0x08, 0x03, 0x14, 0x0e, 0xb4, 0x08, 0xbc, 0x82, + 0x40, 0x10, 0x0a, 0x2f, 0x54, 0x26, 0x05, 0x91, 0x7f, 0x44, 0x28, 0xa3, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x08, 0xb9, 0x33, 0x30, + 0x53, 0x09, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x6f, 0x83, 0x17, 0x47, 0x40, 0x6c, 0x15, 0xb2, 0x6f, 0xbe, 0x09, 0x75, 0x0b, 0x90, + 0x42, 0x45, 0x42, 0x51, 0x0e, 0x32, 0xbc, 0x02, 0x89, 0xa1, 0x6f, 0x7e, 0x86, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0x04, 0x30, + 0x91, 0x6f, 0xd6, 0x2f, 0xeb, 0x6f, 0xa0, 0x5e, 0xb8, 0x2e, 0x03, 0x2e, 0x97, 0x00, 0x1b, 0xbc, 0x60, 0x50, 0x9f, 0xbc, 0x0c, + 0xb8, 0xf0, 0x7f, 0x40, 0xb2, 0xeb, 0x7f, 0x2b, 0x2f, 0x03, 0x2e, 0x7f, 0x00, 0x41, 0x40, 0x01, 0x2e, 0xc8, 0x00, 0x01, 0x1a, + 0x11, 0x2f, 0x37, 0x58, 0x23, 0x2e, 0xc8, 0x00, 0x10, 0x41, 0xa0, 0x7f, 0x38, 0x81, 0x01, 0x41, 0xd0, 0x7f, 0xb1, 0x7f, 0x98, + 0x2e, 0x64, 0xcf, 0xd0, 0x6f, 0x07, 0x80, 0xa1, 0x6f, 0x11, 0x42, 0x00, 0x2e, 0xb1, 0x6f, 0x01, 0x42, 0x11, 0x30, 0x01, 0x2e, + 0xfc, 0x00, 0x00, 0xa8, 0x03, 0x30, 0xcb, 0x22, 0x4a, 0x25, 0x01, 0x2e, 0x7f, 0x00, 0x3c, 0x89, 0x35, 0x52, 0x05, 0x54, 0x98, + 0x2e, 0xc4, 0xce, 0xc1, 0x6f, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x04, 0x2d, 0x01, 0x30, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, + 0xeb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0x03, 0x2e, 0xb3, 0x00, 0x02, 0x32, 0xf0, 0x30, 0x03, 0x31, 0x30, 0x50, 0x8a, 0x08, 0x08, + 0x08, 0xcb, 0x08, 0xe0, 0x7f, 0x80, 0xb2, 0xf3, 0x7f, 0xdb, 0x7f, 0x25, 0x2f, 0x03, 0x2e, 0xca, 0x00, 0x41, 0x90, 0x04, 0x2f, + 0x01, 0x30, 0x23, 0x2e, 0xca, 0x00, 0x98, 0x2e, 0x3f, 0x03, 0xc0, 0xb2, 0x05, 0x2f, 0x03, 0x2e, 0xda, 0x00, 0x00, 0x30, 0x41, + 0x04, 0x23, 0x2e, 0xda, 0x00, 0x98, 0x2e, 0x92, 0xb2, 0x10, 0x25, 0xf0, 0x6f, 0x00, 0xb2, 0x05, 0x2f, 0x01, 0x2e, 0xda, 0x00, + 0x02, 0x30, 0x10, 0x04, 0x21, 0x2e, 0xda, 0x00, 0x40, 0xb2, 0x01, 0x2f, 0x23, 0x2e, 0xc8, 0x01, 0xdb, 0x6f, 0xe0, 0x6f, 0xd0, + 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x01, 0x30, 0xe0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x11, 0x30, 0x23, 0x2e, 0xca, 0x00, 0xdb, 0x6f, + 0xd0, 0x5f, 0xb8, 0x2e, 0xd0, 0x50, 0x0a, 0x25, 0x33, 0x84, 0x55, 0x50, 0xd2, 0x7f, 0xe2, 0x7f, 0x03, 0x8c, 0xc0, 0x7f, 0xbb, + 0x7f, 0x00, 0x30, 0x05, 0x5a, 0x39, 0x54, 0x51, 0x41, 0xa5, 0x7f, 0x96, 0x7f, 0x80, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x05, 0x30, + 0xf5, 0x7f, 0x20, 0x25, 0x91, 0x6f, 0x3b, 0x58, 0x3d, 0x5c, 0x3b, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xc1, 0x6f, 0xd5, 0x6f, 0x52, + 0x40, 0x50, 0x43, 0xc1, 0x7f, 0xd5, 0x7f, 0x10, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x86, 0x6f, + 0x30, 0x28, 0x92, 0x6f, 0x82, 0x8c, 0xa5, 0x6f, 0x6f, 0x52, 0x69, 0x0e, 0x39, 0x54, 0xdb, 0x2f, 0x19, 0xa0, 0x15, 0x30, 0x03, + 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x81, 0x01, 0x0a, 0x2d, 0x01, 0x2e, 0x81, 0x01, 0x05, 0x28, 0x42, 0x36, 0x21, 0x2e, 0x81, 0x01, + 0x02, 0x0e, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50, 0x12, 0x30, 0x01, 0x40, 0x98, 0x2e, 0xfe, 0xc9, 0x51, 0x6f, 0x0b, + 0x5c, 0x8e, 0x0e, 0x3b, 0x6f, 0x57, 0x58, 0x02, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x45, 0x6f, 0x2a, 0x8d, 0xd2, 0x7f, 0xcb, 0x7f, + 0x13, 0x2f, 0x02, 0x30, 0x3f, 0x50, 0xd2, 0x7f, 0xa8, 0x0e, 0x0e, 0x2f, 0xc0, 0x6f, 0x53, 0x54, 0x02, 0x00, 0x51, 0x54, 0x42, + 0x0e, 0x10, 0x30, 0x59, 0x52, 0x02, 0x30, 0x01, 0x2f, 0x00, 0x2e, 0x03, 0x2d, 0x50, 0x42, 0x42, 0x42, 0x12, 0x30, 0xd2, 0x7f, + 0x80, 0xb2, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x80, 0x01, 0x12, 0x2d, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x80, 0x05, 0x2e, 0x80, + 0x01, 0x11, 0x30, 0x91, 0x28, 0x00, 0x40, 0x25, 0x2e, 0x80, 0x01, 0x10, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x7f, 0x01, 0x01, 0x90, + 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x00, 0x2e, 0xa0, 0x41, 0x01, 0x90, 0xa6, 0x7f, 0x90, 0x2e, 0xe3, 0xb4, 0x01, 0x2e, 0x95, + 0x01, 0x00, 0xa8, 0x90, 0x2e, 0xe3, 0xb4, 0x5b, 0x54, 0x95, 0x80, 0x82, 0x40, 0x80, 0xb2, 0x02, 0x40, 0x2d, 0x8c, 0x3f, 0x52, + 0x96, 0x7f, 0x90, 0x2e, 0xc2, 0xb3, 0x29, 0x0e, 0x76, 0x2f, 0x01, 0x2e, 0xc9, 0x00, 0x00, 0x40, 0x81, 0x28, 0x45, 0x52, 0xb3, + 0x30, 0x98, 0x2e, 0x0f, 0xca, 0x5d, 0x54, 0x80, 0x7f, 0x00, 0x2e, 0xa1, 0x40, 0x72, 0x7f, 0x82, 0x80, 0x82, 0x40, 0x60, 0x7f, + 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x62, 0x6f, 0x05, 0x30, 0x87, 0x40, 0xc0, 0x91, 0x04, 0x30, 0x05, + 0x2f, 0x05, 0x2e, 0x83, 0x01, 0x80, 0xb2, 0x14, 0x30, 0x00, 0x2f, 0x04, 0x30, 0x05, 0x2e, 0xc9, 0x00, 0x73, 0x6f, 0x81, 0x40, + 0xe2, 0x40, 0x69, 0x04, 0x11, 0x0f, 0xe1, 0x40, 0x16, 0x30, 0xfe, 0x29, 0xcb, 0x40, 0x02, 0x2f, 0x83, 0x6f, 0x83, 0x0f, 0x22, + 0x2f, 0x47, 0x56, 0x13, 0x0f, 0x12, 0x30, 0x77, 0x2f, 0x49, 0x54, 0x42, 0x0e, 0x12, 0x30, 0x73, 0x2f, 0x00, 0x91, 0x0a, 0x2f, + 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa8, 0x02, 0x30, 0x6c, 0x2f, 0x63, 0x50, 0x00, 0x2e, 0x17, 0x42, 0x05, 0x42, 0x68, 0x2c, 0x12, + 0x30, 0x0b, 0x25, 0x08, 0x0f, 0x50, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x03, 0x2d, 0x40, 0x30, 0x21, 0x2e, 0x83, 0x01, + 0x2b, 0x2e, 0x85, 0x01, 0x5a, 0x2c, 0x12, 0x30, 0x00, 0x91, 0x2b, 0x25, 0x04, 0x2f, 0x63, 0x50, 0x02, 0x30, 0x17, 0x42, 0x17, + 0x2c, 0x02, 0x42, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x05, 0x2e, 0xc9, 0x00, 0x81, 0x84, 0x5b, 0x30, + 0x82, 0x40, 0x37, 0x2e, 0x83, 0x01, 0x02, 0x0e, 0x07, 0x2f, 0x5f, 0x52, 0x40, 0x30, 0x62, 0x40, 0x41, 0x40, 0x91, 0x0e, 0x01, + 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x05, 0x30, 0x2b, 0x2e, 0x85, 0x01, 0x12, 0x30, 0x36, 0x2c, 0x16, 0x30, 0x15, 0x25, 0x81, 0x7f, + 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x19, 0xa2, 0x16, 0x30, 0x15, 0x2f, 0x05, 0x2e, 0x97, 0x01, 0x80, + 0x6f, 0x82, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x86, 0x01, 0x06, 0x28, 0x21, 0x2e, 0x86, 0x01, 0x0b, 0x2d, 0x03, 0x2e, 0x87, 0x01, + 0x5f, 0x54, 0x4e, 0x28, 0x91, 0x42, 0x00, 0x2e, 0x82, 0x40, 0x90, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x88, 0x01, 0x02, 0x30, 0x13, + 0x2c, 0x05, 0x30, 0xc0, 0x6f, 0x08, 0x1c, 0xa8, 0x0f, 0x16, 0x30, 0x05, 0x30, 0x5b, 0x50, 0x09, 0x2f, 0x02, 0x80, 0x2d, 0x2e, + 0x82, 0x01, 0x05, 0x42, 0x05, 0x80, 0x00, 0x2e, 0x02, 0x42, 0x3e, 0x80, 0x00, 0x2e, 0x06, 0x42, 0x02, 0x30, 0x90, 0x6f, 0x3e, + 0x88, 0x01, 0x40, 0x04, 0x41, 0x4c, 0x28, 0x01, 0x42, 0x07, 0x80, 0x10, 0x25, 0x24, 0x40, 0x00, 0x40, 0x00, 0xa8, 0xf5, 0x22, + 0x23, 0x29, 0x44, 0x42, 0x7a, 0x82, 0x7e, 0x88, 0x43, 0x40, 0x04, 0x41, 0x00, 0xab, 0xf5, 0x23, 0xdf, 0x28, 0x43, 0x42, 0xd9, + 0xa0, 0x14, 0x2f, 0x00, 0x90, 0x02, 0x2f, 0xd2, 0x6f, 0x81, 0xb2, 0x05, 0x2f, 0x63, 0x54, 0x06, 0x28, 0x90, 0x42, 0x85, 0x42, + 0x09, 0x2c, 0x02, 0x30, 0x5b, 0x50, 0x03, 0x80, 0x29, 0x2e, 0x7e, 0x01, 0x2b, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x12, 0x30, 0x2b, + 0x2e, 0x83, 0x01, 0x45, 0x82, 0x00, 0x2e, 0x40, 0x40, 0x7a, 0x82, 0x02, 0xa0, 0x08, 0x2f, 0x63, 0x50, 0x3b, 0x30, 0x15, 0x42, + 0x05, 0x42, 0x37, 0x80, 0x37, 0x2e, 0x7e, 0x01, 0x05, 0x42, 0x12, 0x30, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x8c, 0x40, 0x40, 0x84, + 0x41, 0x7a, 0x8c, 0x04, 0x0f, 0x03, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa4, 0x04, 0x2f, 0x2b, 0x2e, 0x82, 0x01, 0x98, 0x2e, + 0xf3, 0x03, 0x12, 0x30, 0x81, 0x90, 0x61, 0x52, 0x08, 0x2f, 0x65, 0x42, 0x65, 0x42, 0x43, 0x80, 0x39, 0x84, 0x82, 0x88, 0x05, + 0x42, 0x45, 0x42, 0x85, 0x42, 0x05, 0x43, 0x00, 0x2e, 0x80, 0x41, 0x00, 0x90, 0x90, 0x2e, 0xe1, 0xb4, 0x65, 0x54, 0xc1, 0x6f, + 0x80, 0x40, 0x00, 0xb2, 0x43, 0x58, 0x69, 0x50, 0x44, 0x2f, 0x55, 0x5c, 0xb7, 0x87, 0x8c, 0x0f, 0x0d, 0x2e, 0x96, 0x01, 0xc4, + 0x40, 0x36, 0x2f, 0x41, 0x56, 0x8b, 0x0e, 0x2a, 0x2f, 0x0b, 0x52, 0xa1, 0x0e, 0x0a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x14, 0x25, + 0x98, 0x2e, 0xfe, 0xc9, 0x4b, 0x54, 0x02, 0x0f, 0x69, 0x50, 0x05, 0x30, 0x65, 0x54, 0x15, 0x2f, 0x03, 0x2e, 0x8e, 0x01, 0x4d, + 0x5c, 0x8e, 0x0f, 0x3a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x98, 0x2e, 0xfe, 0xc9, 0x4f, 0x54, 0x82, 0x0f, 0x05, 0x30, 0x69, 0x50, + 0x65, 0x54, 0x30, 0x2f, 0x6d, 0x52, 0x15, 0x30, 0x42, 0x8c, 0x45, 0x42, 0x04, 0x30, 0x2b, 0x2c, 0x84, 0x43, 0x6b, 0x52, 0x42, + 0x8c, 0x00, 0x2e, 0x85, 0x43, 0x15, 0x30, 0x24, 0x2c, 0x45, 0x42, 0x8e, 0x0f, 0x20, 0x2f, 0x0d, 0x2e, 0x8e, 0x01, 0xb1, 0x0e, + 0x1c, 0x2f, 0x23, 0x2e, 0x8e, 0x01, 0x1a, 0x2d, 0x0e, 0x0e, 0x17, 0x2f, 0xa1, 0x0f, 0x15, 0x2f, 0x23, 0x2e, 0x8d, 0x01, 0x13, + 0x2d, 0x98, 0x2e, 0x74, 0xc0, 0x43, 0x54, 0xc2, 0x0e, 0x0a, 0x2f, 0x65, 0x50, 0x04, 0x80, 0x0b, 0x30, 0x06, 0x82, 0x0b, 0x42, + 0x79, 0x80, 0x41, 0x40, 0x12, 0x30, 0x25, 0x2e, 0x8c, 0x01, 0x01, 0x42, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x84, 0x82, 0x43, + 0x84, 0xbe, 0x8c, 0x84, 0x40, 0x86, 0x41, 0x26, 0x29, 0x94, 0x42, 0xbe, 0x8e, 0xd5, 0x7f, 0x19, 0xa1, 0x43, 0x40, 0x0b, 0x2e, + 0x8c, 0x01, 0x84, 0x40, 0xc7, 0x41, 0x5d, 0x29, 0x27, 0x29, 0x45, 0x42, 0x84, 0x42, 0xc2, 0x7f, 0x01, 0x2f, 0xc0, 0xb3, 0x1d, + 0x2f, 0x05, 0x2e, 0x94, 0x01, 0x99, 0xa0, 0x01, 0x2f, 0x80, 0xb3, 0x13, 0x2f, 0x80, 0xb3, 0x18, 0x2f, 0xc0, 0xb3, 0x16, 0x2f, + 0x12, 0x40, 0x01, 0x40, 0x92, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x92, 0x6f, 0x10, 0x0f, 0x20, 0x30, 0x03, 0x2f, 0x10, 0x30, 0x21, + 0x2e, 0x7e, 0x01, 0x0a, 0x2d, 0x21, 0x2e, 0x7e, 0x01, 0x07, 0x2d, 0x20, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x03, 0x2d, 0x10, 0x30, + 0x21, 0x2e, 0x7e, 0x01, 0xc2, 0x6f, 0x01, 0x2e, 0xc9, 0x00, 0xbc, 0x84, 0x02, 0x80, 0x82, 0x40, 0x00, 0x40, 0x90, 0x0e, 0xd5, + 0x6f, 0x02, 0x2f, 0x15, 0x30, 0x98, 0x2e, 0xf3, 0x03, 0x41, 0x91, 0x05, 0x30, 0x07, 0x2f, 0x67, 0x50, 0x3d, 0x80, 0x2b, 0x2e, + 0x8f, 0x01, 0x05, 0x42, 0x04, 0x80, 0x00, 0x2e, 0x05, 0x42, 0x02, 0x2c, 0x00, 0x30, 0x00, 0x30, 0xa2, 0x6f, 0x98, 0x8a, 0x86, + 0x40, 0x80, 0xa7, 0x05, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0xc0, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x06, 0x25, 0x1a, 0x25, 0xe2, 0x6f, + 0x76, 0x82, 0x96, 0x40, 0x56, 0x43, 0x51, 0x0e, 0xfb, 0x2f, 0xbb, 0x6f, 0x30, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xb8, 0x00, 0x01, + 0x31, 0x41, 0x08, 0x40, 0xb2, 0x20, 0x50, 0xf2, 0x30, 0x02, 0x08, 0xfb, 0x7f, 0x01, 0x30, 0x10, 0x2f, 0x05, 0x2e, 0xcc, 0x00, + 0x81, 0x90, 0xe0, 0x7f, 0x03, 0x2f, 0x23, 0x2e, 0xcc, 0x00, 0x98, 0x2e, 0x55, 0xb6, 0x98, 0x2e, 0x1d, 0xb5, 0x10, 0x25, 0xfb, + 0x6f, 0xe0, 0x6f, 0xe0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x98, 0x2e, 0x95, 0xcf, 0x10, 0x30, 0x21, 0x2e, 0xcc, 0x00, 0xfb, 0x6f, + 0xe0, 0x5f, 0xb8, 0x2e, 0x00, 0x51, 0x05, 0x58, 0xeb, 0x7f, 0x2a, 0x25, 0x89, 0x52, 0x6f, 0x5a, 0x89, 0x50, 0x13, 0x41, 0x06, + 0x40, 0xb3, 0x01, 0x16, 0x42, 0xcb, 0x16, 0x06, 0x40, 0xf3, 0x02, 0x13, 0x42, 0x65, 0x0e, 0xf5, 0x2f, 0x05, 0x40, 0x14, 0x30, + 0x2c, 0x29, 0x04, 0x42, 0x08, 0xa1, 0x00, 0x30, 0x90, 0x2e, 0x52, 0xb6, 0xb3, 0x88, 0xb0, 0x8a, 0xb6, 0x84, 0xa4, 0x7f, 0xc4, + 0x7f, 0xb5, 0x7f, 0xd5, 0x7f, 0x92, 0x7f, 0x73, 0x30, 0x04, 0x30, 0x55, 0x40, 0x42, 0x40, 0x8a, 0x17, 0xf3, 0x08, 0x6b, 0x01, + 0x90, 0x02, 0x53, 0xb8, 0x4b, 0x82, 0xad, 0xbe, 0x71, 0x7f, 0x45, 0x0a, 0x09, 0x54, 0x84, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0xa3, + 0x6f, 0x7b, 0x54, 0xd0, 0x42, 0xa3, 0x7f, 0xf2, 0x7f, 0x60, 0x7f, 0x20, 0x25, 0x71, 0x6f, 0x75, 0x5a, 0x77, 0x58, 0x79, 0x5c, + 0x75, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xb1, 0x6f, 0x62, 0x6f, 0x50, 0x42, 0xb1, 0x7f, 0xb3, 0x30, 0x10, 0x25, 0x98, 0x2e, 0x0f, + 0xca, 0x84, 0x6f, 0x20, 0x29, 0x71, 0x6f, 0x92, 0x6f, 0xa5, 0x6f, 0x76, 0x82, 0x6a, 0x0e, 0x73, 0x30, 0x00, 0x30, 0xd0, 0x2f, + 0xd2, 0x6f, 0xd1, 0x7f, 0xb4, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x02, 0x0a, 0xc2, 0x6f, 0xc0, 0x7f, 0x98, + 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x42, 0x0a, 0xc0, 0x6f, 0x08, 0x17, 0x41, 0x18, 0x89, 0x16, 0xe1, 0x18, 0xd0, 0x18, + 0xa1, 0x7f, 0x27, 0x25, 0x16, 0x25, 0x98, 0x2e, 0x79, 0xc0, 0x8b, 0x54, 0x90, 0x7f, 0xb3, 0x30, 0x82, 0x40, 0x80, 0x90, 0x0d, + 0x2f, 0x7d, 0x52, 0x92, 0x6f, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0e, 0x06, 0x2f, 0x8b, 0x50, 0x14, 0x30, 0x42, 0x6f, + 0x51, 0x6f, 0x14, 0x42, 0x12, 0x42, 0x01, 0x42, 0x00, 0x2e, 0x31, 0x6f, 0x98, 0x2e, 0x74, 0xc0, 0x41, 0x6f, 0x80, 0x7f, 0x98, + 0x2e, 0x74, 0xc0, 0x82, 0x6f, 0x10, 0x04, 0x43, 0x52, 0x01, 0x0f, 0x05, 0x2e, 0xcb, 0x00, 0x00, 0x30, 0x04, 0x30, 0x21, 0x2f, + 0x51, 0x6f, 0x43, 0x58, 0x8c, 0x0e, 0x04, 0x30, 0x1c, 0x2f, 0x85, 0x88, 0x41, 0x6f, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x16, + 0x2f, 0x84, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x0f, 0x2f, 0x82, 0x88, 0x31, 0x6f, 0x04, 0x41, + 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x08, 0x2f, 0x83, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x02, 0x2f, 0x21, + 0x2e, 0xad, 0x01, 0x14, 0x30, 0x00, 0x91, 0x14, 0x2f, 0x03, 0x2e, 0xa1, 0x01, 0x41, 0x90, 0x0e, 0x2f, 0x03, 0x2e, 0xad, 0x01, + 0x14, 0x30, 0x4c, 0x28, 0x23, 0x2e, 0xad, 0x01, 0x46, 0xa0, 0x06, 0x2f, 0x81, 0x84, 0x8d, 0x52, 0x48, 0x82, 0x82, 0x40, 0x21, + 0x2e, 0xa1, 0x01, 0x42, 0x42, 0x5c, 0x2c, 0x02, 0x30, 0x05, 0x2e, 0xaa, 0x01, 0x80, 0xb2, 0x02, 0x30, 0x55, 0x2f, 0x03, 0x2e, + 0xa9, 0x01, 0x92, 0x6f, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x4a, 0x2f, 0xa2, + 0x6f, 0x87, 0x52, 0x91, 0x00, 0x85, 0x52, 0x51, 0x0e, 0x02, 0x2f, 0x00, 0x2e, 0x43, 0x2c, 0x02, 0x30, 0xc2, 0x6f, 0x7f, 0x52, + 0x91, 0x0e, 0x02, 0x30, 0x3c, 0x2f, 0x51, 0x6f, 0x81, 0x54, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0xb3, 0x30, 0x21, 0x25, 0x98, + 0x2e, 0x0f, 0xca, 0x32, 0x6f, 0xc0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x42, 0x6f, 0xb0, 0x7f, 0xb3, 0x30, + 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x28, 0x83, 0x52, 0x98, 0x2e, 0xfe, 0xc9, 0xc2, 0x6f, 0x90, 0x0f, 0x00, + 0x30, 0x02, 0x30, 0x1d, 0x2f, 0x05, 0x2e, 0xa1, 0x01, 0x80, 0xb2, 0x12, 0x30, 0x0f, 0x2f, 0x42, 0x6f, 0x03, 0x2e, 0xab, 0x01, + 0x91, 0x0e, 0x02, 0x30, 0x12, 0x2f, 0x52, 0x6f, 0x03, 0x2e, 0xac, 0x01, 0x91, 0x0f, 0x02, 0x30, 0x0c, 0x2f, 0x21, 0x2e, 0xaa, + 0x01, 0x0a, 0x2c, 0x12, 0x30, 0x03, 0x2e, 0xcb, 0x00, 0x8d, 0x58, 0x08, 0x89, 0x41, 0x40, 0x11, 0x43, 0x00, 0x43, 0x25, 0x2e, + 0xa1, 0x01, 0xd4, 0x6f, 0x8f, 0x52, 0x00, 0x43, 0x3a, 0x89, 0x00, 0x2e, 0x10, 0x43, 0x10, 0x43, 0x61, 0x0e, 0xfb, 0x2f, 0x03, + 0x2e, 0xa0, 0x01, 0x11, 0x1a, 0x02, 0x2f, 0x02, 0x25, 0x21, 0x2e, 0xa0, 0x01, 0xeb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x91, 0x52, + 0x10, 0x30, 0x02, 0x30, 0x95, 0x56, 0x52, 0x42, 0x4b, 0x0e, 0xfc, 0x2f, 0x8d, 0x54, 0x88, 0x82, 0x93, 0x56, 0x80, 0x42, 0x53, + 0x42, 0x40, 0x42, 0x42, 0x86, 0x83, 0x54, 0xc0, 0x2e, 0xc2, 0x42, 0x00, 0x2e, 0xa3, 0x52, 0x00, 0x51, 0x52, 0x40, 0x47, 0x40, + 0x1a, 0x25, 0x01, 0x2e, 0x97, 0x00, 0x8f, 0xbe, 0x72, 0x86, 0xfb, 0x7f, 0x0b, 0x30, 0x7c, 0xbf, 0xa5, 0x50, 0x10, 0x08, 0xdf, + 0xba, 0x70, 0x88, 0xf8, 0xbf, 0xcb, 0x42, 0xd3, 0x7f, 0x6c, 0xbb, 0xfc, 0xbb, 0xc5, 0x0a, 0x90, 0x7f, 0x1b, 0x7f, 0x0b, 0x43, + 0xc0, 0xb2, 0xe5, 0x7f, 0xb7, 0x7f, 0xa6, 0x7f, 0xc4, 0x7f, 0x90, 0x2e, 0x1c, 0xb7, 0x07, 0x2e, 0xd2, 0x00, 0xc0, 0xb2, 0x0b, + 0x2f, 0x97, 0x52, 0x01, 0x2e, 0xcd, 0x00, 0x82, 0x7f, 0x98, 0x2e, 0xbb, 0xcc, 0x0b, 0x30, 0x37, 0x2e, 0xd2, 0x00, 0x82, 0x6f, + 0x90, 0x6f, 0x1a, 0x25, 0x00, 0xb2, 0x8b, 0x7f, 0x14, 0x2f, 0xa6, 0xbd, 0x25, 0xbd, 0xb6, 0xb9, 0x2f, 0xb9, 0x80, 0xb2, 0xd4, + 0xb0, 0x0c, 0x2f, 0x99, 0x54, 0x9b, 0x56, 0x0b, 0x30, 0x0b, 0x2e, 0xb1, 0x00, 0xa1, 0x58, 0x9b, 0x42, 0xdb, 0x42, 0x6c, 0x09, + 0x2b, 0x2e, 0xb1, 0x00, 0x8b, 0x42, 0xcb, 0x42, 0x86, 0x7f, 0x73, 0x84, 0xa7, 0x56, 0xc3, 0x08, 0x39, 0x52, 0x05, 0x50, 0x72, + 0x7f, 0x63, 0x7f, 0x98, 0x2e, 0xc2, 0xc0, 0xe1, 0x6f, 0x62, 0x6f, 0xd1, 0x0a, 0x01, 0x2e, 0xcd, 0x00, 0xd5, 0x6f, 0xc4, 0x6f, + 0x72, 0x6f, 0x97, 0x52, 0x9d, 0x5c, 0x98, 0x2e, 0x06, 0xcd, 0x23, 0x6f, 0x90, 0x6f, 0x99, 0x52, 0xc0, 0xb2, 0x04, 0xbd, 0x54, + 0x40, 0xaf, 0xb9, 0x45, 0x40, 0xe1, 0x7f, 0x02, 0x30, 0x06, 0x2f, 0xc0, 0xb2, 0x02, 0x30, 0x03, 0x2f, 0x9b, 0x5c, 0x12, 0x30, + 0x94, 0x43, 0x85, 0x43, 0x03, 0xbf, 0x6f, 0xbb, 0x80, 0xb3, 0x20, 0x2f, 0x06, 0x6f, 0x26, 0x01, 0x16, 0x6f, 0x6e, 0x03, 0x45, + 0x42, 0xc0, 0x90, 0x29, 0x2e, 0xce, 0x00, 0x9b, 0x52, 0x14, 0x2f, 0x9b, 0x5c, 0x00, 0x2e, 0x93, 0x41, 0x86, 0x41, 0xe3, 0x04, + 0xae, 0x07, 0x80, 0xab, 0x04, 0x2f, 0x80, 0x91, 0x0a, 0x2f, 0x86, 0x6f, 0x73, 0x0f, 0x07, 0x2f, 0x83, 0x6f, 0xc0, 0xb2, 0x04, + 0x2f, 0x54, 0x42, 0x45, 0x42, 0x12, 0x30, 0x04, 0x2c, 0x11, 0x30, 0x02, 0x2c, 0x11, 0x30, 0x11, 0x30, 0x02, 0xbc, 0x0f, 0xb8, + 0xd2, 0x7f, 0x00, 0xb2, 0x0a, 0x2f, 0x01, 0x2e, 0xfc, 0x00, 0x05, 0x2e, 0xc7, 0x01, 0x10, 0x1a, 0x02, 0x2f, 0x21, 0x2e, 0xc7, + 0x01, 0x03, 0x2d, 0x02, 0x2c, 0x01, 0x30, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xd1, 0x6f, 0xa0, 0x6f, 0x98, 0x2e, + 0x95, 0xcf, 0xe2, 0x6f, 0x9f, 0x52, 0x01, 0x2e, 0xce, 0x00, 0x82, 0x40, 0x50, 0x42, 0x0c, 0x2c, 0x42, 0x42, 0x11, 0x30, 0x23, + 0x2e, 0xd2, 0x00, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xa0, 0x6f, 0x01, 0x30, 0x98, 0x2e, 0x95, 0xcf, 0x00, 0x2e, + 0xfb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x83, 0x86, 0x01, 0x30, 0x00, 0x30, 0x94, 0x40, 0x24, 0x18, 0x06, 0x00, 0x53, 0x0e, 0x4f, + 0x02, 0xf9, 0x2f, 0xb8, 0x2e, 0xa9, 0x52, 0x00, 0x2e, 0x60, 0x40, 0x41, 0x40, 0x0d, 0xbc, 0x98, 0xbc, 0xc0, 0x2e, 0x01, 0x0a, + 0x0f, 0xb8, 0xab, 0x52, 0x53, 0x3c, 0x52, 0x40, 0x40, 0x40, 0x4b, 0x00, 0x82, 0x16, 0x26, 0xb9, 0x01, 0xb8, 0x41, 0x40, 0x10, + 0x08, 0x97, 0xb8, 0x01, 0x08, 0xc0, 0x2e, 0x11, 0x30, 0x01, 0x08, 0x43, 0x86, 0x25, 0x40, 0x04, 0x40, 0xd8, 0xbe, 0x2c, 0x0b, + 0x22, 0x11, 0x54, 0x42, 0x03, 0x80, 0x4b, 0x0e, 0xf6, 0x2f, 0xb8, 0x2e, 0x9f, 0x50, 0x10, 0x50, 0xad, 0x52, 0x05, 0x2e, 0xd3, + 0x00, 0xfb, 0x7f, 0x00, 0x2e, 0x13, 0x40, 0x93, 0x42, 0x41, 0x0e, 0xfb, 0x2f, 0x98, 0x2e, 0xa5, 0xb7, 0x98, 0x2e, 0x87, 0xcf, + 0x01, 0x2e, 0xd9, 0x00, 0x00, 0xb2, 0xfb, 0x6f, 0x0b, 0x2f, 0x01, 0x2e, 0x69, 0xf7, 0xb1, 0x3f, 0x01, 0x08, 0x01, 0x30, 0xf0, + 0x5f, 0x23, 0x2e, 0xd9, 0x00, 0x21, 0x2e, 0x69, 0xf7, 0x80, 0x2e, 0x7a, 0xb7, 0xf0, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xc0, 0xf8, + 0x03, 0x2e, 0xfc, 0xf5, 0x15, 0x54, 0xaf, 0x56, 0x82, 0x08, 0x0b, 0x2e, 0x69, 0xf7, 0xcb, 0x0a, 0xb1, 0x58, 0x80, 0x90, 0xdd, + 0xbe, 0x4c, 0x08, 0x5f, 0xb9, 0x59, 0x22, 0x80, 0x90, 0x07, 0x2f, 0x03, 0x34, 0xc3, 0x08, 0xf2, 0x3a, 0x0a, 0x08, 0x02, 0x35, + 0xc0, 0x90, 0x4a, 0x0a, 0x48, 0x22, 0xc0, 0x2e, 0x23, 0x2e, 0xfc, 0xf5, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x56, 0xc7, 0x98, + 0x2e, 0x49, 0xc3, 0x10, 0x30, 0xfb, 0x6f, 0xf0, 0x5f, 0x21, 0x2e, 0xcc, 0x00, 0x21, 0x2e, 0xca, 0x00, 0xb8, 0x2e, 0x03, 0x2e, + 0xd3, 0x00, 0x16, 0xb8, 0x02, 0x34, 0x4a, 0x0c, 0x21, 0x2e, 0x2d, 0xf5, 0xc0, 0x2e, 0x23, 0x2e, 0xd3, 0x00, 0x03, 0xbc, 0x21, + 0x2e, 0xd5, 0x00, 0x03, 0x2e, 0xd5, 0x00, 0x40, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x30, 0x05, 0x2f, 0x05, 0x2e, + 0xd8, 0x00, 0x80, 0x90, 0x01, 0x2f, 0x23, 0x2e, 0x6f, 0xf5, 0xc0, 0x2e, 0x21, 0x2e, 0xd9, 0x00, 0x11, 0x30, 0x81, 0x08, 0x01, + 0x2e, 0x6a, 0xf7, 0x71, 0x3f, 0x23, 0xbd, 0x01, 0x08, 0x02, 0x0a, 0xc0, 0x2e, 0x21, 0x2e, 0x6a, 0xf7, 0x30, 0x25, 0x00, 0x30, + 0x21, 0x2e, 0x5a, 0xf5, 0x10, 0x50, 0x21, 0x2e, 0x7b, 0x00, 0x21, 0x2e, 0x7c, 0x00, 0xfb, 0x7f, 0x98, 0x2e, 0xc3, 0xb7, 0x40, + 0x30, 0x21, 0x2e, 0xd4, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0x03, 0x25, 0x80, 0x2e, 0xaf, 0xb7, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x01, 0x2e, 0x5d, 0xf7, 0x08, 0xbc, 0x80, 0xac, 0x0e, + 0xbb, 0x02, 0x2f, 0x00, 0x30, 0x41, 0x04, 0x82, 0x06, 0xc0, 0xa4, 0x00, 0x30, 0x11, 0x2f, 0x40, 0xa9, 0x03, 0x2f, 0x40, 0x91, + 0x0d, 0x2f, 0x00, 0xa7, 0x0b, 0x2f, 0x80, 0xb3, 0xb3, 0x58, 0x02, 0x2f, 0x90, 0xa1, 0x26, 0x13, 0x20, 0x23, 0x80, 0x90, 0x10, + 0x30, 0x01, 0x2f, 0xcc, 0x0e, 0x00, 0x2f, 0x00, 0x30, 0xb8, 0x2e, 0xb5, 0x50, 0x18, 0x08, 0x08, 0xbc, 0x88, 0xb6, 0x0d, 0x17, + 0xc6, 0xbd, 0x56, 0xbc, 0xb7, 0x58, 0xda, 0xba, 0x04, 0x01, 0x1d, 0x0a, 0x10, 0x50, 0x05, 0x30, 0x32, 0x25, 0x45, 0x03, 0xfb, + 0x7f, 0xf6, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x16, 0xb5, 0x9a, 0xbc, 0x06, 0xb8, 0x80, 0xa8, 0x41, 0x0a, 0x0e, 0x2f, + 0x80, 0x90, 0x02, 0x2f, 0x2d, 0x50, 0x48, 0x0f, 0x09, 0x2f, 0xbf, 0xa0, 0x04, 0x2f, 0xbf, 0x90, 0x06, 0x2f, 0xb7, 0x54, 0xca, + 0x0f, 0x03, 0x2f, 0x00, 0x2e, 0x02, 0x2c, 0xb7, 0x52, 0x2d, 0x52, 0xf2, 0x33, 0x98, 0x2e, 0xd9, 0xc0, 0xfb, 0x6f, 0xf1, 0x37, + 0xc0, 0x2e, 0x01, 0x08, 0xf0, 0x5f, 0xbf, 0x56, 0xb9, 0x54, 0xd0, 0x40, 0xc4, 0x40, 0x0b, 0x2e, 0xfd, 0xf3, 0xbf, 0x52, 0x90, + 0x42, 0x94, 0x42, 0x95, 0x42, 0x05, 0x30, 0xc1, 0x50, 0x0f, 0x88, 0x06, 0x40, 0x04, 0x41, 0x96, 0x42, 0xc5, 0x42, 0x48, 0xbe, + 0x73, 0x30, 0x0d, 0x2e, 0xd8, 0x00, 0x4f, 0xba, 0x84, 0x42, 0x03, 0x42, 0x81, 0xb3, 0x02, 0x2f, 0x2b, 0x2e, 0x6f, 0xf5, 0x06, + 0x2d, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x93, 0x08, 0x25, 0x2e, 0x77, 0xf7, 0xbb, 0x54, 0x25, 0x2e, 0xc2, 0xf5, 0x07, 0x2e, + 0xfd, 0xf3, 0x42, 0x30, 0xb4, 0x33, 0xda, 0x0a, 0x4c, 0x00, 0x27, 0x2e, 0xfd, 0xf3, 0x43, 0x40, 0xd4, 0x3f, 0xdc, 0x08, 0x43, + 0x42, 0x00, 0x2e, 0x00, 0x2e, 0x43, 0x40, 0x24, 0x30, 0xdc, 0x0a, 0x43, 0x42, 0x04, 0x80, 0x03, 0x2e, 0xfd, 0xf3, 0x4a, 0x0a, + 0x23, 0x2e, 0xfd, 0xf3, 0x61, 0x34, 0xc0, 0x2e, 0x01, 0x42, 0x00, 0x2e, 0x60, 0x50, 0x1a, 0x25, 0x7a, 0x86, 0xe0, 0x7f, 0xf3, + 0x7f, 0x03, 0x25, 0xc3, 0x52, 0x41, 0x84, 0xdb, 0x7f, 0x33, 0x30, 0x98, 0x2e, 0x16, 0xc2, 0x1a, 0x25, 0x7d, 0x82, 0xf0, 0x6f, + 0xe2, 0x6f, 0x32, 0x25, 0x16, 0x40, 0x94, 0x40, 0x26, 0x01, 0x85, 0x40, 0x8e, 0x17, 0xc4, 0x42, 0x6e, 0x03, 0x95, 0x42, 0x41, + 0x0e, 0xf4, 0x2f, 0xdb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0xb0, 0x51, 0xfb, 0x7f, 0x98, 0x2e, 0xe8, 0x0d, 0x5a, 0x25, 0x98, 0x2e, + 0x0f, 0x0e, 0xcb, 0x58, 0x32, 0x87, 0xc4, 0x7f, 0x65, 0x89, 0x6b, 0x8d, 0xc5, 0x5a, 0x65, 0x7f, 0xe1, 0x7f, 0x83, 0x7f, 0xa6, + 0x7f, 0x74, 0x7f, 0xd0, 0x7f, 0xb6, 0x7f, 0x94, 0x7f, 0x17, 0x30, 0xc7, 0x52, 0xc9, 0x54, 0x51, 0x7f, 0x00, 0x2e, 0x85, 0x6f, + 0x42, 0x7f, 0x00, 0x2e, 0x51, 0x41, 0x45, 0x81, 0x42, 0x41, 0x13, 0x40, 0x3b, 0x8a, 0x00, 0x40, 0x4b, 0x04, 0xd0, 0x06, 0xc0, + 0xac, 0x85, 0x7f, 0x02, 0x2f, 0x02, 0x30, 0x51, 0x04, 0xd3, 0x06, 0x41, 0x84, 0x05, 0x30, 0x5d, 0x02, 0xc9, 0x16, 0xdf, 0x08, + 0xd3, 0x00, 0x8d, 0x02, 0xaf, 0xbc, 0xb1, 0xb9, 0x59, 0x0a, 0x65, 0x6f, 0x11, 0x43, 0xa1, 0xb4, 0x52, 0x41, 0x53, 0x41, 0x01, + 0x43, 0x34, 0x7f, 0x65, 0x7f, 0x26, 0x31, 0xe5, 0x6f, 0xd4, 0x6f, 0x98, 0x2e, 0x37, 0xca, 0x32, 0x6f, 0x75, 0x6f, 0x83, 0x40, + 0x42, 0x41, 0x23, 0x7f, 0x12, 0x7f, 0xf6, 0x30, 0x40, 0x25, 0x51, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x14, 0x6f, 0x20, 0x05, 0x70, + 0x6f, 0x25, 0x6f, 0x69, 0x07, 0xa2, 0x6f, 0x31, 0x6f, 0x0b, 0x30, 0x04, 0x42, 0x9b, 0x42, 0x8b, 0x42, 0x55, 0x42, 0x32, 0x7f, + 0x40, 0xa9, 0xc3, 0x6f, 0x71, 0x7f, 0x02, 0x30, 0xd0, 0x40, 0xc3, 0x7f, 0x03, 0x2f, 0x40, 0x91, 0x15, 0x2f, 0x00, 0xa7, 0x13, + 0x2f, 0x00, 0xa4, 0x11, 0x2f, 0x84, 0xbd, 0x98, 0x2e, 0x79, 0xca, 0x55, 0x6f, 0xb7, 0x54, 0x54, 0x41, 0x82, 0x00, 0xf3, 0x3f, + 0x45, 0x41, 0xcb, 0x02, 0xf6, 0x30, 0x98, 0x2e, 0x37, 0xca, 0x35, 0x6f, 0xa4, 0x6f, 0x41, 0x43, 0x03, 0x2c, 0x00, 0x43, 0xa4, + 0x6f, 0x35, 0x6f, 0x17, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x93, 0x40, 0x42, 0x82, 0x00, 0x41, 0xc3, 0x00, 0x03, 0x43, 0x51, 0x7f, + 0x00, 0x2e, 0x94, 0x40, 0x41, 0x41, 0x4c, 0x02, 0xc4, 0x6f, 0xd1, 0x56, 0x63, 0x0e, 0x74, 0x6f, 0x51, 0x43, 0xa5, 0x7f, 0x8a, + 0x2f, 0x09, 0x2e, 0xd8, 0x00, 0x01, 0xb3, 0x21, 0x2f, 0xcb, 0x58, 0x90, 0x6f, 0x13, 0x41, 0xb6, 0x6f, 0xe4, 0x7f, 0x00, 0x2e, + 0x91, 0x41, 0x14, 0x40, 0x92, 0x41, 0x15, 0x40, 0x17, 0x2e, 0x6f, 0xf5, 0xb6, 0x7f, 0xd0, 0x7f, 0xcb, 0x7f, 0x98, 0x2e, 0x00, + 0x0c, 0x07, 0x15, 0xc2, 0x6f, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0xc3, 0xa3, 0xc1, 0x8f, 0xe4, 0x6f, 0xd0, 0x6f, 0xe6, 0x2f, + 0x14, 0x30, 0x05, 0x2e, 0x6f, 0xf5, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0x18, 0x2d, 0xcd, 0x56, 0x04, 0x32, 0xb5, 0x6f, 0x1c, + 0x01, 0x51, 0x41, 0x52, 0x41, 0xc3, 0x40, 0xb5, 0x7f, 0xe4, 0x7f, 0x98, 0x2e, 0x1f, 0x0c, 0xe4, 0x6f, 0x21, 0x87, 0x00, 0x43, + 0x04, 0x32, 0xcf, 0x54, 0x5a, 0x0e, 0xef, 0x2f, 0x15, 0x54, 0x09, 0x2e, 0x77, 0xf7, 0x22, 0x0b, 0x29, 0x2e, 0x77, 0xf7, 0xfb, + 0x6f, 0x50, 0x5e, 0xb8, 0x2e, 0x10, 0x50, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0xb2, 0xfb, 0x7f, 0x51, 0x2f, 0x01, 0xb2, 0x48, 0x2f, + 0x02, 0xb2, 0x42, 0x2f, 0x03, 0x90, 0x56, 0x2f, 0xd7, 0x52, 0x79, 0x80, 0x42, 0x40, 0x81, 0x84, 0x00, 0x40, 0x42, 0x42, 0x98, + 0x2e, 0x93, 0x0c, 0xd9, 0x54, 0xd7, 0x50, 0xa1, 0x40, 0x98, 0xbd, 0x82, 0x40, 0x3e, 0x82, 0xda, 0x0a, 0x44, 0x40, 0x8b, 0x16, + 0xe3, 0x00, 0x53, 0x42, 0x00, 0x2e, 0x43, 0x40, 0x9a, 0x02, 0x52, 0x42, 0x00, 0x2e, 0x41, 0x40, 0x15, 0x54, 0x4a, 0x0e, 0x3a, + 0x2f, 0x3a, 0x82, 0x00, 0x30, 0x41, 0x40, 0x21, 0x2e, 0x85, 0x0f, 0x40, 0xb2, 0x0a, 0x2f, 0x98, 0x2e, 0xb1, 0x0c, 0x98, 0x2e, + 0x45, 0x0e, 0x98, 0x2e, 0x5b, 0x0e, 0xfb, 0x6f, 0xf0, 0x5f, 0x00, 0x30, 0x80, 0x2e, 0xce, 0xb7, 0xdd, 0x52, 0xd3, 0x54, 0x42, + 0x42, 0x4f, 0x84, 0x73, 0x30, 0xdb, 0x52, 0x83, 0x42, 0x1b, 0x30, 0x6b, 0x42, 0x23, 0x30, 0x27, 0x2e, 0xd7, 0x00, 0x37, 0x2e, + 0xd4, 0x00, 0x21, 0x2e, 0xd6, 0x00, 0x7a, 0x84, 0x17, 0x2c, 0x42, 0x42, 0x30, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x12, 0x2d, 0x21, + 0x30, 0x00, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0x7b, 0xf7, 0x0b, 0x2d, 0x17, 0x30, 0x98, 0x2e, 0x51, 0x0c, 0xd5, 0x50, + 0x0c, 0x82, 0x72, 0x30, 0x2f, 0x2e, 0xd4, 0x00, 0x25, 0x2e, 0x7b, 0xf7, 0x40, 0x42, 0x00, 0x2e, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, + 0x2e, 0x70, 0x50, 0x0a, 0x25, 0x39, 0x86, 0xfb, 0x7f, 0xe1, 0x32, 0x62, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xb5, 0x56, 0xa5, 0x6f, + 0xab, 0x08, 0x91, 0x6f, 0x4b, 0x08, 0xdf, 0x56, 0xc4, 0x6f, 0x23, 0x09, 0x4d, 0xba, 0x93, 0xbc, 0x8c, 0x0b, 0xd1, 0x6f, 0x0b, + 0x09, 0xcb, 0x52, 0xe1, 0x5e, 0x56, 0x42, 0xaf, 0x09, 0x4d, 0xba, 0x23, 0xbd, 0x94, 0x0a, 0xe5, 0x6f, 0x68, 0xbb, 0xeb, 0x08, + 0xbd, 0xb9, 0x63, 0xbe, 0xfb, 0x6f, 0x52, 0x42, 0xe3, 0x0a, 0xc0, 0x2e, 0x43, 0x42, 0x90, 0x5f, 0xd1, 0x50, 0x03, 0x2e, 0x25, + 0xf3, 0x13, 0x40, 0x00, 0x40, 0x9b, 0xbc, 0x9b, 0xb4, 0x08, 0xbd, 0xb8, 0xb9, 0x98, 0xbc, 0xda, 0x0a, 0x08, 0xb6, 0x89, 0x16, + 0xc0, 0x2e, 0x19, 0x00, 0x62, 0x02, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x31, 0x30, 0x08, + 0x04, 0xfb, 0x6f, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd6, 0x00, 0x21, 0x2e, 0xd7, 0x00, 0xb8, 0x2e, 0x01, 0x2e, 0xd7, 0x00, + 0x03, 0x2e, 0xd6, 0x00, 0x48, 0x0e, 0x01, 0x2f, 0x80, 0x2e, 0x1f, 0x0e, 0xb8, 0x2e, 0xe3, 0x50, 0x21, 0x34, 0x01, 0x42, 0x82, + 0x30, 0xc1, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x01, 0x00, 0x22, 0x30, 0x01, 0x40, 0x4a, 0x0a, 0x01, 0x42, 0xb8, 0x2e, 0xe3, 0x54, + 0xf0, 0x3b, 0x83, 0x40, 0xd8, 0x08, 0xe5, 0x52, 0x83, 0x42, 0x00, 0x30, 0x83, 0x30, 0x50, 0x42, 0xc4, 0x32, 0x27, 0x2e, 0x64, + 0xf5, 0x94, 0x00, 0x50, 0x42, 0x40, 0x42, 0xd3, 0x3f, 0x84, 0x40, 0x7d, 0x82, 0xe3, 0x08, 0x40, 0x42, 0x83, 0x42, 0xb8, 0x2e, + 0xdd, 0x52, 0x00, 0x30, 0x40, 0x42, 0x7c, 0x86, 0xb9, 0x52, 0x09, 0x2e, 0x70, 0x0f, 0xbf, 0x54, 0xc4, 0x42, 0xd3, 0x86, 0x54, + 0x40, 0x55, 0x40, 0x94, 0x42, 0x85, 0x42, 0x21, 0x2e, 0xd7, 0x00, 0x42, 0x40, 0x25, 0x2e, 0xfd, 0xf3, 0xc0, 0x42, 0x7e, 0x82, + 0x05, 0x2e, 0x7d, 0x00, 0x80, 0xb2, 0x14, 0x2f, 0x05, 0x2e, 0x89, 0x00, 0x27, 0xbd, 0x2f, 0xb9, 0x80, 0x90, 0x02, 0x2f, 0x21, + 0x2e, 0x6f, 0xf5, 0x0c, 0x2d, 0x07, 0x2e, 0x71, 0x0f, 0x14, 0x30, 0x1c, 0x09, 0x05, 0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x47, 0xbe, + 0x93, 0x08, 0x94, 0x0a, 0x25, 0x2e, 0x77, 0xf7, 0xe7, 0x54, 0x50, 0x42, 0x4a, 0x0e, 0xfc, 0x2f, 0xb8, 0x2e, 0x50, 0x50, 0x02, + 0x30, 0x43, 0x86, 0xe5, 0x50, 0xfb, 0x7f, 0xe3, 0x7f, 0xd2, 0x7f, 0xc0, 0x7f, 0xb1, 0x7f, 0x00, 0x2e, 0x41, 0x40, 0x00, 0x40, + 0x48, 0x04, 0x98, 0x2e, 0x74, 0xc0, 0x1e, 0xaa, 0xd3, 0x6f, 0x14, 0x30, 0xb1, 0x6f, 0xe3, 0x22, 0xc0, 0x6f, 0x52, 0x40, 0xe4, + 0x6f, 0x4c, 0x0e, 0x12, 0x42, 0xd3, 0x7f, 0xeb, 0x2f, 0x03, 0x2e, 0x86, 0x0f, 0x40, 0x90, 0x11, 0x30, 0x03, 0x2f, 0x23, 0x2e, + 0x86, 0x0f, 0x02, 0x2c, 0x00, 0x30, 0xd0, 0x6f, 0xfb, 0x6f, 0xb0, 0x5f, 0xb8, 0x2e, 0x40, 0x50, 0xf1, 0x7f, 0x0a, 0x25, 0x3c, + 0x86, 0xeb, 0x7f, 0x41, 0x33, 0x22, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd3, 0x6f, 0xf4, 0x30, 0xdc, 0x09, 0x47, 0x58, 0xc2, 0x6f, + 0x94, 0x09, 0xeb, 0x58, 0x6a, 0xbb, 0xdc, 0x08, 0xb4, 0xb9, 0xb1, 0xbd, 0xe9, 0x5a, 0x95, 0x08, 0x21, 0xbd, 0xf6, 0xbf, 0x77, + 0x0b, 0x51, 0xbe, 0xf1, 0x6f, 0xeb, 0x6f, 0x52, 0x42, 0x54, 0x42, 0xc0, 0x2e, 0x43, 0x42, 0xc0, 0x5f, 0x50, 0x50, 0xf5, 0x50, + 0x31, 0x30, 0x11, 0x42, 0xfb, 0x7f, 0x7b, 0x30, 0x0b, 0x42, 0x11, 0x30, 0x02, 0x80, 0x23, 0x33, 0x01, 0x42, 0x03, 0x00, 0x07, + 0x2e, 0x80, 0x03, 0x05, 0x2e, 0xd3, 0x00, 0x23, 0x52, 0xe2, 0x7f, 0xd3, 0x7f, 0xc0, 0x7f, 0x98, 0x2e, 0xb6, 0x0e, 0xd1, 0x6f, + 0x08, 0x0a, 0x1a, 0x25, 0x7b, 0x86, 0xd0, 0x7f, 0x01, 0x33, 0x12, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd1, 0x6f, 0x08, 0x0a, 0x00, + 0xb2, 0x0d, 0x2f, 0xe3, 0x6f, 0x01, 0x2e, 0x80, 0x03, 0x51, 0x30, 0xc7, 0x86, 0x23, 0x2e, 0x21, 0xf2, 0x08, 0xbc, 0xc0, 0x42, + 0x98, 0x2e, 0xa5, 0xb7, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0xb0, 0x6f, 0x0b, 0xb8, 0x03, 0x2e, 0x1b, 0x00, 0x08, 0x1a, 0xb0, + 0x7f, 0x70, 0x30, 0x04, 0x2f, 0x21, 0x2e, 0x21, 0xf2, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x98, 0x2e, 0x6d, 0xc0, 0x98, 0x2e, + 0x5d, 0xc0, 0xed, 0x50, 0x98, 0x2e, 0x44, 0xcb, 0xef, 0x50, 0x98, 0x2e, 0x46, 0xc3, 0xf1, 0x50, 0x98, 0x2e, 0x53, 0xc7, 0x35, + 0x50, 0x98, 0x2e, 0x64, 0xcf, 0x10, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x20, 0x26, 0xc0, 0x6f, 0x02, 0x31, 0x12, 0x42, 0xab, 0x33, + 0x0b, 0x42, 0x37, 0x80, 0x01, 0x30, 0x01, 0x42, 0xf3, 0x37, 0xf7, 0x52, 0xfb, 0x50, 0x44, 0x40, 0xa2, 0x0a, 0x42, 0x42, 0x8b, + 0x31, 0x09, 0x2e, 0x5e, 0xf7, 0xf9, 0x54, 0xe3, 0x08, 0x83, 0x42, 0x1b, 0x42, 0x23, 0x33, 0x4b, 0x00, 0xbc, 0x84, 0x0b, 0x40, + 0x33, 0x30, 0x83, 0x42, 0x0b, 0x42, 0xe0, 0x7f, 0xd1, 0x7f, 0x98, 0x2e, 0x58, 0xb7, 0xd1, 0x6f, 0x80, 0x30, 0x40, 0x42, 0x03, + 0x30, 0xe0, 0x6f, 0xf3, 0x54, 0x04, 0x30, 0x00, 0x2e, 0x00, 0x2e, 0x01, 0x89, 0x62, 0x0e, 0xfa, 0x2f, 0x43, 0x42, 0x11, 0x30, + 0xfb, 0x6f, 0xc0, 0x2e, 0x01, 0x42, 0xb0, 0x5f, 0xc1, 0x4a, 0x00, 0x00, 0x6d, 0x57, 0x00, 0x00, 0x77, 0x8e, 0x00, 0x00, 0xe0, + 0xff, 0xff, 0xff, 0xd3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff, 0xff, 0xee, 0xe1, 0xff, 0xff, 0x7c, 0x13, 0x00, 0x00, 0x46, 0xe6, + 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, + 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, + 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, + 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, + 0x00, 0xc1}; + +#define BMI270_CONFIG_FILE_SIZE sizeof(bmi270_config_file) + +BMI270Sensor::BMI270Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) +{ + if (foundDevice.address.port == ScanI2C::I2CPort::WIRE1) { +#ifdef I2C_SDA1 + wire = &Wire1; +#else + wire = &Wire; +#endif + } else { + wire = &Wire; + } +} + +bool BMI270Sensor::writeRegister(uint8_t reg, uint8_t value) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->write(value); + return wire->endTransmission() == 0; +} + +bool BMI270Sensor::writeRegisters(uint8_t reg, const uint8_t *data, size_t len) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + for (size_t i = 0; i < len; i++) { + wire->write(data[i]); + } + return wire->endTransmission() == 0; +} + +uint8_t BMI270Sensor::readRegister(uint8_t reg) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->endTransmission(false); + wire->requestFrom(deviceAddress(), (uint8_t)1); + if (wire->available()) { + return wire->read(); + } + return 0; +} + +bool BMI270Sensor::readRegisters(uint8_t reg, uint8_t *data, size_t len) +{ + wire->beginTransmission(deviceAddress()); + wire->write(reg); + wire->endTransmission(false); + size_t bytesRead = wire->requestFrom(deviceAddress(), (uint8_t)len); + if (bytesRead != len) { + // Read any available bytes to keep the bus state clean, but report failure. + for (size_t i = 0; i < bytesRead && wire->available(); i++) { + data[i] = wire->read(); + } + return false; + } + + for (size_t i = 0; i < len && wire->available(); i++) { + data[i] = wire->read(); + } + return true; +} + +bool BMI270Sensor::uploadConfigFile() +{ + if (!writeRegister(BMI270_REG_INIT_CTRL, 0x00)) + return false; + + const size_t chunkSize = 32; + size_t bytesWritten = 0; + + while (bytesWritten < BMI270_CONFIG_FILE_SIZE) { + size_t remaining = BMI270_CONFIG_FILE_SIZE - bytesWritten; + size_t toWrite = (remaining < chunkSize) ? remaining : chunkSize; + + // Set address in word units before each chunk + uint16_t wordAddr = bytesWritten / 2; + if (!writeRegister(BMI270_REG_INIT_ADDR_0, (uint8_t)(wordAddr & 0x0F))) + return false; + if (!writeRegister(BMI270_REG_INIT_ADDR_1, (uint8_t)(wordAddr >> 4))) + return false; + + wire->beginTransmission(deviceAddress()); + wire->write(BMI270_REG_INIT_DATA); + for (size_t i = 0; i < toWrite; i++) { + wire->write(pgm_read_byte(&bmi270_config_file[bytesWritten + i])); + } + if (wire->endTransmission() != 0) + return false; + + bytesWritten += toWrite; + } + + if (!writeRegister(BMI270_REG_INIT_CTRL, 0x01)) + return false; + + delay(50); + + for (int i = 0; i < 10; i++) { + uint8_t status = readRegister(BMI270_REG_INTERNAL_STATUS); + if ((status & 0x0F) == BMI270_INIT_OK) + return true; + delay(20); + } + + LOG_WARN("BMI270 status=0x%02X", readRegister(BMI270_REG_INTERNAL_STATUS)); + return false; +} + +bool BMI270Sensor::init() +{ + delay(10); + writeRegister(BMI270_REG_CMD, BMI270_CMD_SOFTRESET); + delay(50); + + if (!writeRegister(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_ADV_POWER_SAVE_DISABLED)) + return false; + delay(2); + + if (!uploadConfigFile()) { + LOG_WARN("BMI270 config failed"); + return false; + } + + uint8_t accConf = BMI270_ACC_ODR_50HZ | BMI270_ACC_BWP_NORMAL | BMI270_ACC_FILTER_PERF; + if (!writeRegister(BMI270_REG_ACC_CONF, accConf) || !writeRegister(BMI270_REG_ACC_RANGE, BMI270_ACC_RANGE_2G) || + !writeRegister(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_ACC_EN)) + return false; + + delay(50); + initialized = true; + return true; +} + +int32_t BMI270Sensor::runOnce() +{ + if (!initialized) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Read accelerometer data (6 bytes) + uint8_t data[6]; + if (!readRegisters(BMI270_REG_ACC_X_LSB, data, 6)) { + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Convert to 16-bit signed values + int16_t x = (int16_t)((data[1] << 8) | data[0]); + int16_t y = (int16_t)((data[3] << 8) | data[2]); + int16_t z = (int16_t)((data[5] << 8) | data[4]); + + if (!hasBaseline) { + prevX = x; + prevY = y; + prevZ = z; + hasBaseline = true; + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } + + // Calculate change in acceleration + int16_t deltaX = abs(x - prevX); + int16_t deltaY = abs(y - prevY); + int16_t deltaZ = abs(z - prevZ); + + // Update baseline with low-pass filter + prevX = (prevX * 9 + x) / 10; + prevY = (prevY * 9 + y) / 10; + prevZ = (prevZ * 9 + z) / 10; + + // Check for significant motion (~0.2g at 2g range) + const int16_t threshold = 3200; + if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) { + wakeScreen(); + return 500; + } + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +#endif diff --git a/src/motion/BMI270Sensor.h b/src/motion/BMI270Sensor.h new file mode 100644 index 000000000..7d6cdeaa9 --- /dev/null +++ b/src/motion/BMI270Sensor.h @@ -0,0 +1,36 @@ +#pragma once +#ifndef _BMI270_SENSOR_H_ +#define _BMI270_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMI270) + +class BMI270Sensor : public MotionSensor +{ + private: + bool initialized = false; + TwoWire *wire = nullptr; + + // Previous readings for motion detection + int16_t prevX = 0, prevY = 0, prevZ = 0; + bool hasBaseline = false; + + // BMI270 register access + bool writeRegister(uint8_t reg, uint8_t value); + bool writeRegisters(uint8_t reg, const uint8_t *data, size_t len); + uint8_t readRegister(uint8_t reg); + bool readRegisters(uint8_t reg, uint8_t *data, size_t len); + + // Config file upload (BMI270 requires 8KB config blob) + bool uploadConfigFile(); + + public: + explicit BMI270Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/BMX160Sensor.h b/src/motion/BMX160Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/ICM20948Sensor.h b/src/motion/ICM20948Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/LIS3DHSensor.h b/src/motion/LIS3DHSensor.h old mode 100755 new mode 100644 diff --git a/src/motion/LSM6DS3Sensor.cpp b/src/motion/LSM6DS3Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/LSM6DS3Sensor.h b/src/motion/LSM6DS3Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/MPU6050Sensor.cpp b/src/motion/MPU6050Sensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/MPU6050Sensor.h b/src/motion/MPU6050Sensor.h old mode 100755 new mode 100644 diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h old mode 100755 new mode 100644 diff --git a/src/motion/STK8XXXSensor.cpp b/src/motion/STK8XXXSensor.cpp old mode 100755 new mode 100644 diff --git a/src/motion/STK8XXXSensor.h b/src/motion/STK8XXXSensor.h old mode 100755 new mode 100644 diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 3e039a86b..aba06c210 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -300,7 +300,9 @@ struct PubSubConfig { if (config.tls_enabled) { serverPort = 8883; } - std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort); + auto [parsedServerAddr, parsedServerPort] = parseHostAndPort(serverAddr.c_str(), serverPort); + serverAddr = std::move(parsedServerAddr); + serverPort = parsedServerPort; } // Defaults @@ -320,8 +322,8 @@ bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &cli pubSub.setClient(client); pubSub.setServer(config.serverAddr.c_str(), config.serverPort); - LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(), - config.serverPort, config.mqttUsername, config.mqttPassword); + LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: ***", config.serverAddr.c_str(), + config.serverPort, config.mqttUsername); // Generate node ID from nodenum for client identification std::string nodeId = nodeDB->getNodeId(); @@ -441,7 +443,8 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) moduleConfig.mqtt.map_report_settings.publish_interval_secs, default_map_publish_interval_secs); } - String host = parseHostAndPort(moduleConfig.mqtt.address).first; + auto [host, parsedPort] = parseHostAndPort(moduleConfig.mqtt.address); + (void)parsedPort; isConfiguredForDefaultServer = isDefaultServer(host); IPAddress ip; isMqttServerAddressPrivate = ip.fromString(host.c_str()) && isPrivateIpAddress(ip); @@ -456,7 +459,9 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) enabled = true; runASAP = true; reconnectCount = 0; +#if !IS_RUNNING_TESTS publishNodeInfo(); +#endif } // preflightSleepObserver.observe(&preflightSleep); } else { @@ -646,22 +651,34 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC if (config.enabled && !config.proxy_to_client_enabled) { #if HAS_NETWORKING - std::unique_ptr clientConnection; if (config.tls_enabled) { -#if MQTT_SUPPORTS_TLS - MQTTClientTLS *tlsClient = new MQTTClientTLS; - clientConnection.reset(tlsClient); - tlsClient->setInsecure(); -#else +#if !MQTT_SUPPORTS_TLS LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node"); return false; #endif - } else { - clientConnection.reset(new MQTTClient); } - std::unique_ptr pubSub(new PubSubClient); + // Perform a lightweight TCP connectivity check without using connectPubSub(), + // which mutates the module's isConnected state. This only checks if the server + // is reachable โ€” it does not establish an MQTT session. + // Settings are always saved regardless of the result. if (isConnectedToNetwork()) { - return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection); + MQTTClient testClient; + if (!testClient.connect(parsed.serverAddr.c_str(), parsed.serverPort)) { + const char *warning = "Could not reach the MQTT server. Settings will be saved, but please verify the server " + "address and credentials."; + LOG_WARN(warning); +#if !IS_RUNNING_TESTS + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + if (cn) { + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + strncpy(cn->message, warning, sizeof(cn->message) - 1); + cn->message[sizeof(cn->message) - 1] = '\0'; + service->sendClientNotification(cn); + } +#endif + } + testClient.stop(); } #else const char *warning = "Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network"; diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 7aee45f81..30398a675 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -202,6 +202,8 @@ #define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L #elif defined(HELTEC_WIRELESS_TRACKER_V2) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 +#elif defined(M5STACK_CARDPUTER_ADV) +#define HW_VENDOR meshtastic_HardwareModel_M5STACK_CARDPUTER_ADV #else #define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 171911e5e..25cb30e96 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -231,7 +231,9 @@ void cpuDeepSleep(uint32_t msecToWake) #if SOC_RTCIO_HOLD_SUPPORTED && SOC_PM_SUPPORT_EXT_WAKEUP uint64_t gpioMask = (1ULL << (config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); #endif - +#ifdef ALT_BUTTON_WAKE + gpioMask |= (1ULL << BUTTON_PIN_ALT); +#endif #ifdef BUTTON_NEED_PULLUP gpio_pullup_en((gpio_num_t)BUTTON_PIN); #endif diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp index ea5773d30..19c8881ca 100644 --- a/src/platform/extra_variants/t_lora_pager/variant.cpp +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -23,5 +23,6 @@ void lateInitVariant() cfg.i2s.bits = BIT_LENGTH_16BITS; cfg.i2s.rate = RATE_44K; board.begin(cfg); + board.setVolume(75); // 75% volume } #endif \ No newline at end of file diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp index d119ffddb..8c937d71f 100644 --- a/src/platform/nrf52/AsyncUDP.cpp +++ b/src/platform/nrf52/AsyncUDP.cpp @@ -33,7 +33,10 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p if (!udp.beginPacket(ip, port)) return false; udp.write(data, len); - return udp.endPacket(); + isSending = true; + bool ok = udp.endPacket(); + isSending = false; + return ok; } void AsyncUDP::close() @@ -70,7 +73,7 @@ const uint8_t *AsyncUDPPacket::data() int32_t AsyncUDP::runOnce() { - if (_onPacket && udp.parsePacket() > 0) { + if (_onPacket && !isSending && udp.parsePacket() > 0) { AsyncUDPPacket packet(udp); _onPacket(packet); } diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h index ecbcb12fb..eb6083bc4 100644 --- a/src/platform/nrf52/AsyncUDP.h +++ b/src/platform/nrf52/AsyncUDP.h @@ -31,6 +31,7 @@ class AsyncUDP : public Print, private concurrency::OSThread private: EthernetUDP udp; uint16_t localPort; + volatile bool isSending = false; std::function _onPacket; virtual int32_t runOnce() override; }; diff --git a/src/platform/nrf52/Nrf52SaadcLock.cpp b/src/platform/nrf52/Nrf52SaadcLock.cpp new file mode 100644 index 000000000..21f4f4dfd --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.cpp @@ -0,0 +1,13 @@ +#include "Nrf52SaadcLock.h" +#include "concurrency/Lock.h" +#include "configuration.h" + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +static Lock nrf52SaadcLockInstance; +Lock *nrf52SaadcLock = &nrf52SaadcLockInstance; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/Nrf52SaadcLock.h b/src/platform/nrf52/Nrf52SaadcLock.h new file mode 100644 index 000000000..77024eea3 --- /dev/null +++ b/src/platform/nrf52/Nrf52SaadcLock.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef ARCH_NRF52 + +namespace concurrency +{ +class Lock; +/** Shared mutex for SAADC configuration and reads (VDD + battery analog path). */ +extern Lock *nrf52SaadcLock; +} // namespace concurrency + +#endif diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index f73a5b896..eafd799fc 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -157,7 +157,7 @@ #endif -#ifdef PIN_LED1 +#if defined(PIN_LED1) && !defined(LED_POWER) #define LED_POWER PIN_LED1 // LED1 on nrf52840-DK #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c705521ad..5cf3a4465 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -17,6 +17,7 @@ #include #include // #include +#include "HardwareRNG.h" #include "NodeDB.h" #include "PowerMon.h" #include "error.h" @@ -25,6 +26,8 @@ #include "power.h" #include +#include "Nrf52SaadcLock.h" +#include "concurrency/LockGuard.h" #include #ifdef BQ25703A_ADDR @@ -51,6 +54,10 @@ uint16_t getVDDVoltage(); void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} +// Optional variant hook called each nrf52Loop(); e.g. for low-VDD System OFF. +void variant_nrf52LoopHook(void) __attribute__((weak)); +void variant_nrf52LoopHook(void) {} + static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; @@ -74,11 +81,18 @@ bool powerHAL_isVBUSConnected() bool powerHAL_isPowerLevelSafe() { - static bool powerLevelSafe = true; - uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV - uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000; +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_MV + uint16_t threshold = SAFE_VDD_VOLTAGE_THRESHOLD_MV; +#else + uint16_t threshold = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD * 1000.0f + 0.5f); // convert V to mV +#endif +#ifdef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV + uint16_t hysteresis = SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV; +#else + uint16_t hysteresis = (uint16_t)(SAFE_VDD_VOLTAGE_THRESHOLD_HYST * 1000.0f + 0.5f); +#endif if (powerLevelSafe) { if (getVDDVoltage() < threshold) { @@ -125,11 +139,12 @@ void powerHAL_platformInit() // get VDD voltage (in millivolts) uint16_t getVDDVoltage() { - // we use the same values as regular battery read so there is no conflict on SAADC + concurrency::LockGuard guard(concurrency::nrf52SaadcLock); + + // Match battery read resolution; SAADC is shared with AnalogBatteryLevel in Power.cpp. analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); // VDD range on NRF52840 is 1.8-3.3V so we need to remap analog reference to 3.6V - // let's hope battery reading runs in same task and we don't have race condition analogReference(AR_INTERNAL); uint16_t vddADCRead = analogReadVDD(); @@ -326,6 +341,8 @@ void nrf52Loop() checkSDEvents(); reportLittleFSCorruptionOnce(); + + variant_nrf52LoopHook(); // Optional variant hook called each nrf52Loop(); } #ifdef USE_SEMIHOSTING @@ -382,15 +399,14 @@ void nrf52Setup() #endif // Init random seed - union seedParts { - uint32_t seed32; - uint8_t seed8[4]; - } seed; - nRFCrypto.begin(); - nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8)); - LOG_DEBUG("Set random seed %u", seed.seed32); - randomSeed(seed.seed32); - nRFCrypto.end(); + uint32_t seed = 0; + if (!HardwareRNG::seed(seed)) { + LOG_WARN("Hardware RNG seed unavailable, using PRNG fallback"); + // Use a hardware timer value as a fallback seed for better entropy + seed = micros(); + } + LOG_DEBUG("Set random seed %u", seed); + randomSeed(seed); // Set up nrfx watchdog. Do not enable the watchdog yet (we do that // the first time through the main loop), so that other threads can diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index ad3e2de5e..708cccf37 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -1,4 +1,5 @@ #include "CryptoEngine.h" +#include "HardwareRNG.h" #include "PortduinoGPIO.h" #include "SPIChip.h" #include "mesh/RF95Interface.h" @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -233,7 +235,9 @@ void portduinoSetup() 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); + uint32_t seed = TCPPort; + HardwareRNG::seed(seed); + randomSeed(seed); return; } @@ -278,6 +282,24 @@ void portduinoSetup() std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat" << std::endl; + // check for custom data fields + int i = 0; + while (access(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str(), R_OK) == 0) { + std::ifstream customFieldFile(("/proc/device-tree/hat/custom_" + std::to_string(i)).c_str()); + if (customFieldFile.is_open()) { + std::string customFieldName; + std::string customFieldValue; + getline(customFieldFile, customFieldName, ' '); + getline(customFieldFile, customFieldValue, ' '); + customFieldFile.close(); + + printf("autoconf: Found hat+ custom field %s: %s\n", customFieldName.c_str(), customFieldValue.c_str()); + portduino_config.hat_plus_custom_fields[customFieldName] = customFieldValue; + } + + i++; + } + // potential TODO: Validate that this is a real UUID std::ifstream hatUUID("/proc/device-tree/hat/uuid"); char uuid[38] = {0}; @@ -494,17 +516,24 @@ void portduinoSetup() #endif printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]); // Rather important to set this, if not running simulated. - randomSeed(time(NULL)); + uint32_t seed = static_cast(time(NULL)); + HardwareRNG::seed(seed); + randomSeed(seed); std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); + + std::set used_pins; + for (const auto *i : portduino_config.all_pins) { - if (i->enabled && i->pin > max_GPIO) + if (i->enabled && i->pin > max_GPIO) { max_GPIO = i->pin; + } } for (auto i : portduino_config.extra_pins) { - if (i.enabled && i.pin > max_GPIO) + 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. @@ -518,12 +547,18 @@ void portduinoSetup() continue; } 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); + if (used_pins.find(i->pin) != used_pins.end()) { + printf("Pin %d is in use for multiple purposes\n", i->pin); + } else { + 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); + } + used_pins.insert(i->pin); } } } + printf("Initializing extra pins\n"); for (auto i : portduino_config.extra_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. @@ -531,9 +566,14 @@ void portduinoSetup() continue; } 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); + if (used_pins.find(i.pin) != used_pins.end()) { + printf("Pin %d is in use for multiple purposes\n", i.pin); + } else { + 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); + } + used_pins.insert(i.pin); } } } @@ -557,6 +597,25 @@ void portduinoSetup() // disable bias once finished pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT); + } else if (portduino_config.hat_plus_custom_fields.find("io_slot1") != portduino_config.hat_plus_custom_fields.end()) { + printf("Hat+ io_slot1 is %s\n", portduino_config.hat_plus_custom_fields["io_slot1"].c_str()); + if (portduino_config.hat_plus_custom_fields["io_slot1"] != "RAK13302") { + std::cout << "Hat+ io_slot1 is not RAK13302, skipping PA curve" << std::endl; + portduino_config.num_pa_points = 1; + portduino_config.tx_gain_lora[0] = 0; + } + } + + for (auto i : portduino_config.extra_pins) { + // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora + // Those GPIO are handled in our usermode driver instead. + if (i.config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { + continue; + } + if (i.enabled && i.default_high) { + pinMode(i.pin, OUTPUT); + digitalWrite(i.pin, HIGH); + } } // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware @@ -577,7 +636,9 @@ void portduinoSetup() } } else if (portduino_config.JSONFilename != "") { try { - JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + if (portduino_config.JSONFileRotate == 0) { + JSONFile.open(portduino_config.JSONFilename, std::ios::out | std::ios::app); + } } catch (std::ofstream::failure &e) { std::cout << "*** JSONFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); @@ -634,6 +695,7 @@ bool loadConfig(const char *configPath) } portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFileRotate = yamlConfig["Logging"]["JSONFileRotate"].as(0); portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; @@ -713,6 +775,17 @@ bool loadConfig(const char *configPath) } } + if (yamlConfig["Lora"]["Enable_Pins"]) { + for (auto extra_pin : yamlConfig["Lora"]["Enable_Pins"]) { + portduino_config.extra_pins.push_back(pinMapping()); + portduino_config.extra_pins.back().config_section = "Lora"; + portduino_config.extra_pins.back().config_name = "Enable_Pins"; + portduino_config.extra_pins.back().enabled = true; + portduino_config.extra_pins.back().default_high = true; + readGPIOFromYaml(extra_pin, portduino_config.extra_pins.back()); + } + } + 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); diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 62787c5cb..1bb82fa05 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -52,6 +52,7 @@ struct pinMapping { int gpiochip; int line; bool enabled = false; + bool default_high = false; }; extern std::ofstream traceFile; @@ -164,6 +165,7 @@ extern struct portduino_config_struct { bool ascii_logs_explicit = false; std::string JSONFilename; + int JSONFileRotate = 0; meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; // Webserver @@ -215,6 +217,8 @@ extern struct portduino_config_struct { int maxtophone = 100; int MaxNodes = 200; + std::unordered_map hat_plus_custom_fields; + pinMapping *all_pins[21] = {&lora_cs_pin, &lora_irq_pin, &lora_busy_pin, @@ -489,6 +493,9 @@ extern struct portduino_config_struct { out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; if (JSONFilename != "") { out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFileRotate != 0) + out << YAML::Key << "JSONFileRotate" << YAML::Value << JSONFileRotate; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp index 6c73e385a..e59b0a9cd 100644 --- a/src/platform/rp2xx0/main-rp2xx0.cpp +++ b/src/platform/rp2xx0/main-rp2xx0.cpp @@ -1,3 +1,4 @@ +#include "HardwareRNG.h" #include "configuration.h" #include "hardware/xosc.h" #include @@ -98,10 +99,12 @@ void getMacAddr(uint8_t *dmac) void rp2040Setup() { - /* Sets a random seed to make sure we get different random numbers on each boot. - Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. - */ - randomSeed(rp2040.hwrand32()); + /* Sets a random seed to make sure we get different random numbers on each boot. */ + uint32_t seed = 0; + if (!HardwareRNG::seed(seed)) { + seed = rp2040.hwrand32(); + } + randomSeed(seed); #ifdef RP2040_SLOW_CLOCK uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY); diff --git a/src/platform/stm32wl/hardfault_handler.s b/src/platform/stm32wl/hardfault_handler.s new file mode 100644 index 000000000..ab76096fa --- /dev/null +++ b/src/platform/stm32wl/hardfault_handler.s @@ -0,0 +1,11 @@ +.globl HardFault_Handler +.syntax unified +.thumb + +.type HardFault_Handler, %function +HardFault_Handler: +tst lr, #4 +ite eq +mrseq r0, msp +mrsne r0, psp +b HardFault_Handler_C \ No newline at end of file diff --git a/src/platform/stm32wl/main-stm32wl.cpp b/src/platform/stm32wl/main-stm32wl.cpp index e841f8f29..93080f840 100644 --- a/src/platform/stm32wl/main-stm32wl.cpp +++ b/src/platform/stm32wl/main-stm32wl.cpp @@ -1,5 +1,6 @@ #include "RTC.h" #include "configuration.h" +#include #include #include @@ -53,4 +54,99 @@ extern "C" void __wrap__tzset_unlocked_r(struct _reent *reent_ptr) { return; } -#endif \ No newline at end of file +#endif + +// Taken from https://interrupt.memfault.com/blog/cortex-m-hardfault-debug +typedef struct __attribute__((packed)) ContextStateFrame { + uint32_t r0; + uint32_t r1; + uint32_t r2; + uint32_t r3; + uint32_t r12; + uint32_t lr; + uint32_t return_address; + uint32_t xpsr; +} sContextStateFrame; + +// NOTE: If you are using CMSIS, the registers can also be +// accessed through CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk +#define HALT_IF_DEBUGGING() \ + do { \ + if ((*(volatile uint32_t *)0xE000EDF0) & (1 << 0)) { \ + __asm("bkpt 1"); \ + } \ + } while (0) + +static char hardfault_message_buffer[256]; + +// printf directly using srcwrapper's debug UART function. +static void debug_printf(const char *format, ...) +{ + va_list args; + va_start(args, format); + int length = vsnprintf(hardfault_message_buffer, sizeof(hardfault_message_buffer), format, args); + va_end(args); + + if (length < 0) + return; + uart_debug_write((uint8_t *)hardfault_message_buffer, min((unsigned int)length, sizeof(hardfault_message_buffer) - 1)); +} + +// N picked by guessing +#define DOT_TIME 1200000 +static void dot() +{ + digitalWrite(LED_POWER, LED_STATE_ON); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } + digitalWrite(LED_POWER, LED_STATE_OFF); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } +} + +static void dash() +{ + digitalWrite(LED_POWER, LED_STATE_ON); + for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */ + } + digitalWrite(LED_POWER, LED_STATE_OFF); + for (volatile int i = 0; i < DOT_TIME; i++) { /* busy wait */ + } +} + +static void space() +{ + for (volatile int i = 0; i < (DOT_TIME * 3); i++) { /* busy wait */ + } +} + +// Disable optimizations for this function so "frame" argument +// does not get optimized away +extern "C" __attribute__((optimize("O0"))) void HardFault_Handler_C(sContextStateFrame *frame) +{ + debug_printf("HardFault!\r\n"); + debug_printf("r0: %08x\r\n", frame->r0); + debug_printf("r1: %08x\r\n", frame->r1); + debug_printf("r2: %08x\r\n", frame->r2); + debug_printf("r3: %08x\r\n", frame->r3); + debug_printf("r12: %08x\r\n", frame->r12); + debug_printf("lr: %08x\r\n", frame->lr); + debug_printf("pc[return address]: %08x\r\n", frame->return_address); + debug_printf("xpsr: %08x\r\n", frame->xpsr); + + HALT_IF_DEBUGGING(); + + // blink SOS forever + while (1) { + dot(); + dot(); + dot(); + dash(); + dash(); + dash(); + dot(); + dot(); + dot(); + space(); + } +} \ No newline at end of file diff --git a/src/sleep.cpp b/src/sleep.cpp index 8470e9273..9c044eaf7 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -163,13 +163,6 @@ void initDeepSleep() if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { LOG_DEBUG("Disable any holds on RTC IO pads"); for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { -#if defined(USE_GC1109_PA) - // Skip GC1109 FEM power pins - they are held HIGH during deep sleep to keep - // the LNA active for RX wake. Released later in SX126xInterface::init() after - // GPIO registers are set HIGH first, avoiding a power glitch. - if (i == LORA_PA_POWER || i == LORA_PA_EN) - continue; -#endif if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) rtc_gpio_hold_dis((gpio_num_t)i); @@ -436,8 +429,13 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL); #endif -#ifdef INPUTDRIVER_ENCODER_BTN - gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_ENCODER_BTN, GPIO_INTR_LOW_LEVEL); +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) || defined(INPUTDRIVER_ENCODER_BTN) +#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN) +#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_TWO_WAY_ROCKER_BTN +#else +#define INPUTDRIVER_WAKE_BTN_PIN INPUTDRIVER_ENCODER_BTN +#endif + gpio_wakeup_enable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN, GPIO_INTR_LOW_LEVEL); #endif #if defined(WAKE_ON_TOUCH) gpio_wakeup_enable((gpio_num_t)SCREEN_TOUCH_INT, GPIO_INTR_LOW_LEVEL); @@ -478,8 +476,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r // Disable wake-on-button interrupt. Re-attach normal button-interrupts gpio_wakeup_disable(pin); #endif -#if defined(INPUTDRIVER_ENCODER_BTN) - gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_ENCODER_BTN); +#ifdef INPUTDRIVER_WAKE_BTN_PIN + gpio_wakeup_disable((gpio_num_t)INPUTDRIVER_WAKE_BTN_PIN); +#undef INPUTDRIVER_WAKE_BTN_PIN #endif #if defined(WAKE_ON_TOUCH) gpio_wakeup_disable((gpio_num_t)SCREEN_TOUCH_INT); @@ -546,7 +545,8 @@ void enableModemSleep() bool shouldLoraWake(uint32_t msecToWake) { - return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER); + return msecToWake < portMAX_DELAY && (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE); } void enableLoraInterrupt() @@ -567,15 +567,8 @@ void enableLoraInterrupt() gpio_pullup_en((gpio_num_t)LORA_CS); #endif -#if defined(USE_GC1109_PA) - // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. - // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), - // then latch with RTC hold so the state survives deep sleep. - digitalWrite(LORA_PA_POWER, HIGH); - rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); - digitalWrite(LORA_PA_EN, HIGH); - rtc_gpio_hold_en((gpio_num_t)LORA_PA_EN); - gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); +#if HAS_LORA_FEM + loraFEMInterface.setRxModeEnableWhenMCUSleep(); #endif LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); diff --git a/test/test_admin_radio/test_main.cpp b/test/test_admin_radio/test_main.cpp new file mode 100644 index 000000000..9906bb94c --- /dev/null +++ b/test/test_admin_radio/test_main.cpp @@ -0,0 +1,814 @@ +/** + * Tests for the radio configuration validation and clamping functions + * introduced in the radio_interface_cherrypick branch. + * + * Targets: + * 1. getRegion() + * 2. RadioInterface::validateConfigRegion() + * 3. RadioInterface::validateConfigLora() + * 4. RadioInterface::clampConfigLora() + * 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF) + * 6. Channel spacing calculation (placeholder for future protobuf changes) + */ + +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "RadioInterface.h" +#include "TestUtil.h" +#include "modules/AdminModule.h" +#include + +#include "meshtastic/config.pb.h" + +class MockMeshService : public MeshService +{ + public: + void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } +}; + +static MockMeshService *mockMeshService; + +// ----------------------------------------------------------------------- +// getRegion() tests +// ----------------------------------------------------------------------- +extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code); + +static void test_getRegion_returnsCorrectRegion_US() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_US, r->code); + TEST_ASSERT_EQUAL_STRING("US", r->name); +} + +static void test_getRegion_returnsCorrectRegion_EU868() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_EU_868, r->code); + TEST_ASSERT_EQUAL_STRING("EU_868", r->name); +} + +static void test_getRegion_returnsCorrectRegion_LORA24() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_LORA_24, r->code); + TEST_ASSERT_TRUE(r->wideLora); +} + +static void test_getRegion_unsetCodeReturnsUnsetEntry() +{ + const RegionInfo *r = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); + TEST_ASSERT_EQUAL_STRING("UNSET", r->name); +} + +static void test_getRegion_unknownCodeFallsToUnset() +{ + // A code not in the table should iterate to the UNSET sentinel + const RegionInfo *r = getRegion((meshtastic_Config_LoRaConfig_RegionCode)255); + TEST_ASSERT_NOT_NULL(r); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); +} + +// ----------------------------------------------------------------------- +// validateConfigRegion() tests +// ----------------------------------------------------------------------- + +static void test_validateConfigRegion_validRegionReturnsTrue() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + + // Ensure owner is not licensed (should not matter for non-licensed-only regions) + devicestate.owner.is_licensed = false; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); +} + +static void test_validateConfigRegion_unsetRegionReturnsTrue() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + + devicestate.owner.is_licensed = false; + + // UNSET region has licensedOnly=false, so should pass + TEST_ASSERT_TRUE(RadioInterface::validateConfigRegion(cfg)); +} + +// ----------------------------------------------------------------------- +// Shadow tables for testing (preset lists โ†’ profiles โ†’ regions โ†’ lookup) +// ----------------------------------------------------------------------- + +// A minimal preset list with only one entry +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_SINGLE[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, + MODEM_PRESET_END, +}; + +// A preset list that includes all turbo variants only +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_TURBO_ONLY[] = { + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + MODEM_PRESET_END, +}; + +// A restricted list simulating a hypothetical tight-regulation region +static const meshtastic_Config_LoRaConfig_ModemPreset TEST_PRESETS_RESTRICTED[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + MODEM_PRESET_END, +}; + +// Mirrors PROFILE_STD but with non-zero spacing/padding for testing +static const RegionProfile TEST_PROFILE_SPACED = { + TEST_PRESETS_SINGLE, + /* spacing */ 0.025f, + /* padding */ 0.010f, + /* audioPermitted */ true, + /* licensedOnly */ false, + /* textThrottle */ 0, + /* positionThrottle */ 0, + /* telemetryThrottle */ 0, + /* overrideSlot */ 0, +}; + +// A licensed-only profile for testing access control +static const RegionProfile TEST_PROFILE_LICENSED = { + TEST_PRESETS_RESTRICTED, + /* spacing */ 0.0f, + /* padding */ 0.0f, + /* audioPermitted */ false, + /* licensedOnly */ true, + /* textThrottle */ 5, + /* positionThrottle */ 10, + /* telemetryThrottle */ 10, + /* overrideSlot */ 3, +}; + +// Turbo-only profile +static const RegionProfile TEST_PROFILE_TURBO = { + TEST_PRESETS_TURBO_ONLY, + /* spacing */ 0.0f, + /* padding */ 0.0f, + /* audioPermitted */ true, + /* licensedOnly */ false, + /* textThrottle */ 0, + /* positionThrottle */ 0, + /* telemetryThrottle */ 0, + /* overrideSlot */ 0, +}; + +static const RegionInfo testRegions[] = { + // A wide US-like region with spacing + padding + {meshtastic_Config_LoRaConfig_RegionCode_US, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_US_SPACED"}, + + // A narrow band simulating tight EU regulation + {meshtastic_Config_LoRaConfig_RegionCode_EU_868, 869.4f, 869.65f, 10, 14, false, false, &TEST_PROFILE_LICENSED, + "TEST_EU_LICENSED"}, + + // A wide-LoRa region with turbo-only presets + {meshtastic_Config_LoRaConfig_RegionCode_LORA_24, 2400.0f, 2483.5f, 100, 10, false, true, &TEST_PROFILE_TURBO, + "TEST_LORA24_TURBO"}, + + // Sentinel โ€” must be last + {meshtastic_Config_LoRaConfig_RegionCode_UNSET, 902.0f, 928.0f, 100, 30, false, false, &TEST_PROFILE_SPACED, "TEST_UNSET"}, +}; + +static const RegionInfo *getTestRegion(meshtastic_Config_LoRaConfig_RegionCode code) +{ + const RegionInfo *r = testRegions; + while (r->code != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (r->code == code) + return r; + r++; + } + return r; // Returns the UNSET sentinel +} + +// ----------------------------------------------------------------------- +// Shadow table tests +// ----------------------------------------------------------------------- + +static void test_shadowTable_spacedProfileHasNonZeroSpacing() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL_STRING("TEST_US_SPACED", r->name); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.025f, r->profile->spacing); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.010f, r->profile->padding); +} + +static void test_shadowTable_licensedProfileFlagsCorrect() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_TRUE(r->profile->licensedOnly); + TEST_ASSERT_FALSE(r->profile->audioPermitted); + TEST_ASSERT_EQUAL(3, r->profile->overrideSlot); +} + +static void test_shadowTable_presetCountMatchesExpected() +{ + const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(1, spaced->getNumPresets()); + + const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(2, licensed->getNumPresets()); + + const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(2, turbo->getNumPresets()); +} + +static void test_shadowTable_defaultPresetIsFirstInList() +{ + const RegionInfo *spaced = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, spaced->getDefaultPreset()); + + const RegionInfo *licensed = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, licensed->getDefaultPreset()); + + const RegionInfo *turbo = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, turbo->getDefaultPreset()); +} + +static void test_shadowTable_channelSpacingWithPadding() +{ + // Verify channel count when spacing + padding are non-zero + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); + float channelSpacing = r->profile->spacing + (r->profile->padding * 2) + (bw / 1000.0f); + + // spacing=0.025, padding=0.010*2=0.020, bw=250kHz=0.250 + // channelSpacing = 0.025 + 0.020 + 0.250 = 0.295 MHz + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.295f, channelSpacing); + + uint32_t numChannels = (uint32_t)(((r->freqEnd - r->freqStart + r->profile->spacing) / channelSpacing) + 0.5f); + // (928 - 902 + 0.025) / 0.295 = 88.2 โ†’ 88 + TEST_ASSERT_EQUAL_UINT32(88, numChannels); +} + +static void test_shadowTable_turboOnlyOnWideLora() +{ + const RegionInfo *r = getTestRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_TRUE(r->wideLora); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, r->getDefaultPreset()); + + // Verify wide-LoRa bandwidth for SHORT_TURBO + float bw = modemPresetToBwKHz(r->getDefaultPreset(), r->wideLora); + TEST_ASSERT_FLOAT_WITHIN(0.1f, 1625.0f, bw); // 1625 kHz in wide mode +} + +static void test_shadowTable_unknownCodeFallsToSentinel() +{ + const RegionInfo *r = getTestRegion((meshtastic_Config_LoRaConfig_RegionCode)200); + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_RegionCode_UNSET, r->code); + TEST_ASSERT_EQUAL_STRING("TEST_UNSET", r->name); +} + +// ----------------------------------------------------------------------- +// validateConfigLora() tests +// ----------------------------------------------------------------------- + +static void test_validateConfigLora_validPresetForUS() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_allStdPresetsValidForUS() +{ + meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + }; + + for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = stdPresets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for US"); + } +} + +static void test_validateConfigLora_turboPresetsInvalidForEU868() +{ + // EU_868 has PRESETS_EU_868 which excludes SHORT_TURBO and LONG_TURBO + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for EU_868"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_TURBO should be invalid for EU_868"); +} + +static void test_validateConfigLora_validPresetsForEU868() +{ + meshtastic_Config_LoRaConfig_ModemPreset eu868Presets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, + }; + + for (size_t i = 0; i < sizeof(eu868Presets) / sizeof(eu868Presets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + cfg.modem_preset = eu868Presets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for EU_868"); + } +} + +static void test_validateConfigLora_customBandwidthTooWideForEU868() +{ + // EU_868 spans 869.4 - 869.65 = 0.25 MHz = 250 kHz + // A 500 kHz custom BW should be rejected + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 500; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_customBandwidthFitsUS() +{ + // US spans 902 - 928 = 26 MHz, so 250 kHz BW fits easily + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = false; + cfg.bandwidth = 250; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_customBandwidthFitsEU868() +{ + // EU_868 spans 250 kHz, 125 kHz BW should fit + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 125; + cfg.spread_factor = 12; + cfg.coding_rate = 8; + + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_bogusPresetRejected() +{ + // A fabricated preset value not in any list should be rejected + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); +} + +static void test_validateConfigLora_unsetRegionOnlyAcceptsLongFast() +{ + // UNSET uses PROFILE_UNDEF which has only LONG_FAST + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + cfg.use_preset = true; + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "LONG_FAST should be valid for UNSET"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "MEDIUM_FAST should be invalid for UNSET"); + + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + TEST_ASSERT_FALSE_MESSAGE(RadioInterface::validateConfigLora(cfg), "SHORT_TURBO should be invalid for UNSET"); +} + +static void test_validateConfigLora_allPresetsValidForLORA24() +{ + // LORA_24 uses PROFILE_STD (9 presets) with wideLora=true + meshtastic_Config_LoRaConfig_ModemPreset stdPresets[] = { + meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO, + }; + + for (size_t i = 0; i < sizeof(stdPresets) / sizeof(stdPresets[0]); i++) { + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.use_preset = true; + cfg.modem_preset = stdPresets[i]; + TEST_ASSERT_TRUE_MESSAGE(RadioInterface::validateConfigLora(cfg), "Expected valid preset for LORA_24"); + } +} + +// ----------------------------------------------------------------------- +// clampConfigLora() tests +// ----------------------------------------------------------------------- + +static void test_clampConfigLora_invalidPresetClampedToDefault() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; // not in EU_868 preset list + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), cfg.modem_preset); +} + +static void test_clampConfigLora_validPresetUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_customBwTooWideClampedToDefaultBw() +{ + // EU_868 span is 250kHz. A 500kHz custom BW should be clamped to default preset BW. + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.use_preset = false; + cfg.bandwidth = 500; + cfg.spread_factor = 11; + cfg.coding_rate = 5; + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + float expectedBw = modemPresetToBwKHz(eu868->getDefaultPreset(), eu868->wideLora); + TEST_ASSERT_FLOAT_WITHIN(0.01f, expectedBw, (float)cfg.bandwidth); +} + +static void test_clampConfigLora_customBwValidLeftUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = false; + cfg.bandwidth = 125; + cfg.spread_factor = 12; + cfg.coding_rate = 8; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL_UINT16(125, cfg.bandwidth); +} + +static void test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast() +{ + // UNSET uses PROFILE_UNDEF with only LONG_FAST; any other preset should clamp to it + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault() +{ + // LORA_24 uses PROFILE_STD; a bogus preset should clamp to LONG_FAST (first in PRESETS_STD) + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; + cfg.use_preset = true; + cfg.modem_preset = (meshtastic_Config_LoRaConfig_ModemPreset)99; + + RadioInterface::clampConfigLora(cfg); + + const RegionInfo *lora24 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_LORA_24); + TEST_ASSERT_EQUAL(lora24->getDefaultPreset(), cfg.modem_preset); +} + +// ----------------------------------------------------------------------- +// RegionInfo preset list integrity tests +// ----------------------------------------------------------------------- + +static void test_presetsStd_hasNineEntries() +{ + // PROFILE_STD should have exactly 9 presets + const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + TEST_ASSERT_EQUAL(9, us->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_STD.presets, us->getAvailablePresets()); +} + +static void test_presetsEU868_hasSevenEntries() +{ + const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(7, eu->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_EU868.presets, eu->getAvailablePresets()); +} + +static void test_presetsUndef_hasOneEntry() +{ + const RegionInfo *unset = getRegion(meshtastic_Config_LoRaConfig_RegionCode_UNSET); + TEST_ASSERT_EQUAL(1, unset->getNumPresets()); + TEST_ASSERT_EQUAL_PTR(PROFILE_UNDEF.presets, unset->getAvailablePresets()); +} + +static void test_defaultPresetIsInAvailablePresets() +{ + // For every region, the defaultPreset must appear in its own availablePresets list + const RegionInfo *r = regions; + while (true) { + bool found = false; + for (size_t i = 0; i < r->getNumPresets(); i++) { + if (r->getAvailablePresets()[i] == r->getDefaultPreset()) { + found = true; + break; + } + } + char msg[80]; + snprintf(msg, sizeof(msg), "Region %s defaultPreset not in availablePresets", r->name); + TEST_ASSERT_TRUE_MESSAGE(found, msg); + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; // UNSET is the sentinel, stop after it + r++; + } +} + +static void test_regionFieldsAreSane() +{ + // Basic sanity check: all regions have freqEnd > freqStart and a non-null name + const RegionInfo *r = regions; + while (true) { + char msg[80]; + snprintf(msg, sizeof(msg), "Region %s: freqEnd must be > freqStart", r->name); + TEST_ASSERT_TRUE_MESSAGE(r->freqEnd > r->freqStart, msg); + TEST_ASSERT_NOT_NULL(r->name); + TEST_ASSERT_TRUE_MESSAGE(r->getNumPresets() > 0, "numPresets must be > 0"); + TEST_ASSERT_NOT_NULL(r->getAvailablePresets()); + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; + r++; + } +} + +static void test_onlyLORA24HasWideLora() +{ + // Verify that LORA_24 is the only region with wideLora=true + const RegionInfo *r = regions; + while (true) { + char msg[80]; + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { + snprintf(msg, sizeof(msg), "Region %s should have wideLora=true", r->name); + TEST_ASSERT_TRUE_MESSAGE(r->wideLora, msg); + } else { + snprintf(msg, sizeof(msg), "Region %s should have wideLora=false", r->name); + TEST_ASSERT_FALSE_MESSAGE(r->wideLora, msg); + } + + if (r->code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + break; + r++; + } +} + +// ----------------------------------------------------------------------- +// Channel spacing calculation (placeholder for future protobuf updates) +// ----------------------------------------------------------------------- + +static void test_channelSpacingCalculation_US_LONG_FAST() +{ + // Current formula: channelSpacing = spacing + (padding * 2) + (bw / 1000) + // US: spacing=0, padding=0 + // LONG_FAST on non-wide region: bw=250 kHz + // channelSpacing = 0 + 0 + 0.250 = 0.250 MHz + // numChannels = round((928 - 902 + 0) / 0.250) = round(104) = 104 + const RegionInfo *us = getRegion(meshtastic_Config_LoRaConfig_RegionCode_US); + float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, us->wideLora); + float channelSpacing = us->profile->spacing + (us->profile->padding * 2) + (bw / 1000.0f); + uint32_t numChannels = (uint32_t)(((us->freqEnd - us->freqStart + us->profile->spacing) / channelSpacing) + 0.5f); + + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); + TEST_ASSERT_EQUAL_UINT32(104, numChannels); +} + +static void test_channelSpacingCalculation_EU868_LONG_FAST() +{ + // EU_868: freqStart=869.4, freqEnd=869.65, spacing=0, padding=0 + // LONG_FAST: bw=250 kHz => channelSpacing = 0.250 MHz + // numChannels = round((0.25 + 0) / 0.250) = 1 + const RegionInfo *eu = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + float bw = modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, eu->wideLora); + float channelSpacing = eu->profile->spacing + (eu->profile->padding * 2) + (bw / 1000.0f); + uint32_t numChannels = (uint32_t)(((eu->freqEnd - eu->freqStart + eu->profile->spacing) / channelSpacing) + 0.5f); + + TEST_ASSERT_FLOAT_WITHIN(0.001f, 0.250f, channelSpacing); + TEST_ASSERT_EQUAL_UINT32(1, numChannels); +} + +// Placeholder: when protobuf region definitions include non-zero padding/spacing, +// add tests here to verify the channel count and frequency calculations. +static void test_channelSpacingCalculation_placeholder() +{ + // TODO: Once protobuf RegionInfo entries have non-zero padding or spacing values, + // verify: + // - Channel count matches expected value for each (region, preset) pair + // - First channel frequency = freqStart + (bw/2000) + padding + // - Nth channel frequency = first + (n * channelSpacing) + // - overrideSlot, when non-zero, forces the channel_num + TEST_PASS_MESSAGE("Placeholder for future channel spacing tests with updated protobuf region fields"); +} + +// ----------------------------------------------------------------------- +// handleSetConfig fromOthers dispatch tests +// ----------------------------------------------------------------------- + +class AdminModuleTestShim : public AdminModule +{ + public: + using AdminModule::handleSetConfig; +}; + +static AdminModuleTestShim *testAdmin; + +static meshtastic_Config makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode region, bool usePreset, + meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + meshtastic_Config c = meshtastic_Config_init_zero; + c.which_payload_variant = meshtastic_Config_lora_tag; + c.payload_variant.lora.region = region; + c.payload_variant.lora.use_preset = usePreset; + c.payload_variant.lora.modem_preset = preset; + return c; +} + +static void test_handleSetConfig_fromOthers_invalidPresetRejected() +{ + // Set up a known-good baseline in the global config + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with an invalid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + + testAdmin->handleSetConfig(c, true); // fromOthers = true + + // fromOthers=true: invalid preset should be rejected, old preset preserved + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, config.lora.modem_preset); +} + +static void test_handleSetConfig_fromLocal_invalidPresetClamped() +{ + // Set up a known-good baseline + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with an invalid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO); + + testAdmin->handleSetConfig(c, false); // fromOthers = false (local client) + + // fromOthers=false: invalid preset should be clamped to the region's default + const RegionInfo *eu868 = getRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_868); + TEST_ASSERT_EQUAL(eu868->getDefaultPreset(), config.lora.modem_preset); +} + +static void test_handleSetConfig_fromOthers_validPresetAccepted() +{ + // Set up baseline + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + initRegion(); + + // Build an admin set_config with a valid preset for EU_868 + meshtastic_Config c = makeLoraSetConfig(meshtastic_Config_LoRaConfig_RegionCode_EU_868, true, + meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST); + + testAdmin->handleSetConfig(c, true); // fromOthers = true + + // Valid preset should be accepted regardless of fromOthers + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, config.lora.modem_preset); +} + +// ----------------------------------------------------------------------- +// Test runner +// ----------------------------------------------------------------------- + +void setUp(void) +{ + mockMeshService = new MockMeshService(); + service = mockMeshService; + testAdmin = new AdminModuleTestShim(); +} +void tearDown(void) +{ + service = nullptr; + delete mockMeshService; + mockMeshService = nullptr; + delete testAdmin; + testAdmin = nullptr; +} + +void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + + UNITY_BEGIN(); + + // getRegion() + RUN_TEST(test_getRegion_returnsCorrectRegion_US); + RUN_TEST(test_getRegion_returnsCorrectRegion_EU868); + RUN_TEST(test_getRegion_returnsCorrectRegion_LORA24); + RUN_TEST(test_getRegion_unsetCodeReturnsUnsetEntry); + RUN_TEST(test_getRegion_unknownCodeFallsToUnset); + + // validateConfigRegion() + RUN_TEST(test_validateConfigRegion_validRegionReturnsTrue); + RUN_TEST(test_validateConfigRegion_unsetRegionReturnsTrue); + + // Shadow table tests + RUN_TEST(test_shadowTable_spacedProfileHasNonZeroSpacing); + RUN_TEST(test_shadowTable_licensedProfileFlagsCorrect); + RUN_TEST(test_shadowTable_presetCountMatchesExpected); + RUN_TEST(test_shadowTable_defaultPresetIsFirstInList); + RUN_TEST(test_shadowTable_channelSpacingWithPadding); + RUN_TEST(test_shadowTable_turboOnlyOnWideLora); + RUN_TEST(test_shadowTable_unknownCodeFallsToSentinel); + + // validateConfigLora() + RUN_TEST(test_validateConfigLora_validPresetForUS); + RUN_TEST(test_validateConfigLora_allStdPresetsValidForUS); + RUN_TEST(test_validateConfigLora_turboPresetsInvalidForEU868); + RUN_TEST(test_validateConfigLora_validPresetsForEU868); + RUN_TEST(test_validateConfigLora_customBandwidthTooWideForEU868); + RUN_TEST(test_validateConfigLora_customBandwidthFitsUS); + RUN_TEST(test_validateConfigLora_customBandwidthFitsEU868); + RUN_TEST(test_validateConfigLora_bogusPresetRejected); + RUN_TEST(test_validateConfigLora_unsetRegionOnlyAcceptsLongFast); + RUN_TEST(test_validateConfigLora_allPresetsValidForLORA24); + + // clampConfigLora() + RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); + RUN_TEST(test_clampConfigLora_validPresetUnchanged); + RUN_TEST(test_clampConfigLora_customBwTooWideClampedToDefaultBw); + RUN_TEST(test_clampConfigLora_customBwValidLeftUnchanged); + RUN_TEST(test_clampConfigLora_bogusPresetOnUnsetClampedToLongFast); + RUN_TEST(test_clampConfigLora_invalidPresetOnLORA24ClampedToDefault); + + // RegionInfo preset list integrity + RUN_TEST(test_presetsStd_hasNineEntries); + RUN_TEST(test_presetsEU868_hasSevenEntries); + RUN_TEST(test_presetsUndef_hasOneEntry); + RUN_TEST(test_defaultPresetIsInAvailablePresets); + RUN_TEST(test_regionFieldsAreSane); + RUN_TEST(test_onlyLORA24HasWideLora); + + // Channel spacing (current + placeholder) + RUN_TEST(test_channelSpacingCalculation_US_LONG_FAST); + RUN_TEST(test_channelSpacingCalculation_EU868_LONG_FAST); + RUN_TEST(test_channelSpacingCalculation_placeholder); + + // handleSetConfig fromOthers dispatch + RUN_TEST(test_handleSetConfig_fromOthers_invalidPresetRejected); + RUN_TEST(test_handleSetConfig_fromLocal_invalidPresetClamped); + RUN_TEST(test_handleSetConfig_fromOthers_validPresetAccepted); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_default/test_main.cpp b/test/test_default/test_main.cpp new file mode 100644 index 000000000..9da367897 --- /dev/null +++ b/test/test_default/test_main.cpp @@ -0,0 +1,146 @@ +// Unit tests for Default::getConfiguredOrDefaultMsScaled +#include "Default.h" +#include "MeshRadio.h" +#include "TestUtil.h" +#include "meshUtils.h" +#include + +// Helper to compute expected ms using same logic as Default::congestionScalingCoefficient +static uint32_t computeExpectedMs(uint32_t defaultSeconds, uint32_t numOnlineNodes) +{ + uint32_t baseMs = Default::getConfiguredOrDefaultMs(0, defaultSeconds); + + // Routers (including ROUTER_LATE) don't scale + if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + return baseMs; + } + + // Sensors and trackers don't scale + if ((config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) || + (config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER)) { + return baseMs; + } + + if (numOnlineNodes <= 40) { + return baseMs; + } + + float bwKHz = + config.lora.use_preset ? modemPresetToBwKHz(config.lora.modem_preset, false) : bwCodeToKHz(config.lora.bandwidth); + + uint8_t sf = config.lora.spread_factor; + if (sf < 7) + sf = 7; + else if (sf > 12) + sf = 12; + + float throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 100.0f); +#if USERPREFS_EVENT_MODE + throttlingFactor = static_cast(pow_of_2(sf)) / (bwKHz * 25.0f); +#endif + + int nodesOverForty = (numOnlineNodes - 40); + float coeff = 1.0f + (nodesOverForty * throttlingFactor); + return static_cast(baseMs * coeff + 0.5f); +} + +void test_router_no_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + // set some sane lora config so bootstrap paths are deterministic + config.lora.use_preset = false; + config.lora.spread_factor = 9; + config.lora.bandwidth = 250; + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 100); + uint32_t expected = computeExpectedMs(60, 100); + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_below_threshold() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = false; + config.lora.spread_factor = 9; + config.lora.bandwidth = 250; + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 40); + uint32_t expected = computeExpectedMs(60, 40); + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_default_preset_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = false; + config.lora.spread_factor = 9; // SF9 + config.lora.bandwidth = 250; // 250 kHz + + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 50); + uint32_t expected = computeExpectedMs(60, 50); // nodesOverForty = 10 + TEST_ASSERT_EQUAL_UINT32(expected, res); +} + +void test_client_medium_fast_preset_scaling() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + // nodesOverForty = 30 -> test with nodes=70 + uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 70); + uint32_t expected = computeExpectedMs(60, 70); + // Allow ยฑ1 ms tolerance for floating-point rounding + TEST_ASSERT_INT_WITHIN(1, expected, res); +} + +void test_router_uses_router_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); +} + +void test_router_late_uses_router_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry); + TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position); +} + +void test_client_uses_public_channel_minimums() +{ + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + + uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs); + uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs); + + TEST_ASSERT_EQUAL_UINT32(30 * 60, telemetry); + TEST_ASSERT_EQUAL_UINT32(60 * 60, position); +} + +void setup() +{ + // Small delay to match other test mains + delay(10); + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_router_no_scaling); + RUN_TEST(test_client_below_threshold); + RUN_TEST(test_client_default_preset_scaling); + RUN_TEST(test_client_medium_fast_preset_scaling); + RUN_TEST(test_router_uses_router_minimums); + RUN_TEST(test_router_late_uses_router_minimums); + RUN_TEST(test_client_uses_public_channel_minimums); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index afc8a399a..edf9a3983 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -289,13 +289,23 @@ class MQTTUnitTest : public MQTT mqtt = unitTest = new MQTTUnitTest(); mqtt->start(); + auto clearStartupOutput = []() { + pubsub->published_.clear(); + if (mockMeshService != nullptr) { + mockMeshService->messages_.clear(); + mockMeshService->notifications_.clear(); + } + }; + if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) { loopUntil([] { return true; }); // Loop once + clearStartupOutput(); return; } // Wait for MQTT to subscribe to all topics. TEST_ASSERT_TRUE(loopUntil( [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); })); + clearStartupOutput(); } PubSubClient &getPubSub() { return pubSub; } }; @@ -808,16 +818,13 @@ void test_configEmptyIsValid(void) TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// Empty 'enabled' configuration is valid. +// Empty 'enabled' configuration is valid. A lightweight TCP check may be performed +// but does not affect the result. void test_configEnabledEmptyIsValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str()); - TEST_ASSERT_EQUAL(1883, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // Configuration with the default server is valid. @@ -836,38 +843,32 @@ void test_configWithDefaultServerAndInvalidPort(void) TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); } -// isValidConfig connects to a custom host and port. +// Custom host and port is valid. TCP reachability is checked but does not block saving. void test_configCustomHostAndPort(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"}; - MockPubSubServer client; - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); - TEST_ASSERT_TRUE(client.connected_); - TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str()); - TEST_ASSERT_EQUAL(1234, client.port_); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } -// isValidConfig returns false if a connection cannot be established. -void test_configWithConnectionFailure(void) +// An unreachable server is still a valid config โ€” settings always save. +// A warning notification is sent in non-test builds, but isValidConfig returns true. +void test_configWithUnreachableServerIsStillValid(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"}; - MockPubSubServer client; - client.refuseConnection_ = true; - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); } // isValidConfig returns true when tls_enabled is supported, or false otherwise. void test_configWithTLSEnabled(void) { meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true}; - MockPubSubServer client; #if MQTT_SUPPORTS_TLS - TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_TRUE(MQTT::isValidConfig(config)); #else - TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client)); + TEST_ASSERT_FALSE(MQTT::isValidConfig(config)); #endif } @@ -917,7 +918,7 @@ void setup() RUN_TEST(test_configWithDefaultServer); RUN_TEST(test_configWithDefaultServerAndInvalidPort); RUN_TEST(test_configCustomHostAndPort); - RUN_TEST(test_configWithConnectionFailure); + RUN_TEST(test_configWithUnreachableServerIsStillValid); RUN_TEST(test_configWithTLSEnabled); exit(UNITY_END()); } @@ -930,4 +931,4 @@ void setup() UNITY_END(); } #endif -void loop() {} \ No newline at end of file +void loop() {} diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index fbe2b1b13..a7d3d32d2 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -1,10 +1,36 @@ #include "MeshRadio.h" +#include "MeshService.h" #include "RadioInterface.h" #include "TestUtil.h" #include #include "meshtastic/config.pb.h" +class MockMeshService : public MeshService +{ + public: + void sendClientNotification(meshtastic_ClientNotification *n) override { releaseClientNotificationToPool(n); } +}; + +static MockMeshService *mockMeshService; + +// Test shim to expose protected radio parameters set by applyModemConfig() +class TestableRadioInterface : public RadioInterface +{ + public: + TestableRadioInterface() : RadioInterface() {} + uint8_t getCr() const { return cr; } + uint8_t getSf() const { return sf; } + float getBw() const { return bw; } + + // Override reconfigure to call the base which invokes applyModemConfig() + bool reconfigure() override { return RadioInterface::reconfigure(); } + + // Stubs for pure virtual methods required by RadioInterface + uint32_t getPacketTime(uint32_t, bool) override { return 0; } + ErrorCode send(meshtastic_MeshPacket *p) override { return ERRNO_OK; } +}; + static void test_bwCodeToKHz_specialMappings() { TEST_ASSERT_FLOAT_WITHIN(0.0001f, 31.25f, bwCodeToKHz(31)); @@ -21,7 +47,7 @@ static void test_bwCodeToKHz_passthrough() TEST_ASSERT_FLOAT_WITHIN(0.0001f, 250.0f, bwCodeToKHz(250)); } -static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() +static void test_validateConfigLora_noopWhenUsePresetFalse() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = false; @@ -30,55 +56,152 @@ static void test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse() cfg.bandwidth = 123; cfg.spread_factor = 8; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); + RadioInterface::validateConfigLora(cfg); TEST_ASSERT_EQUAL_UINT16(123, cfg.bandwidth); TEST_ASSERT_EQUAL_UINT32(8, cfg.spread_factor); TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); } -static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion() +static void test_validateConfigLora_validPreset_nonWideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } -static void test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion() +static void test_validateConfigLora_validPreset_wideRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_LORA_24; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL_UINT16(800, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(9, cfg.spread_factor); + TEST_ASSERT_TRUE(RadioInterface::validateConfigLora(cfg)); } -static void test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan() +static void test_validateConfigLora_rejectsInvalidPresetForRegion() { meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; cfg.use_preset = true; cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; - RadioInterface::bootstrapLoRaConfigFromPreset(cfg); - - TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); - TEST_ASSERT_EQUAL_UINT16(250, cfg.bandwidth); - TEST_ASSERT_EQUAL_UINT32(11, cfg.spread_factor); + TEST_ASSERT_FALSE(RadioInterface::validateConfigLora(cfg)); } -void setUp(void) {} -void tearDown(void) {} +static void test_clampConfigLora_invalidPresetClampedToDefault() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST, cfg.modem_preset); +} + +static void test_clampConfigLora_validPresetUnchanged() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.use_preset = true; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL(meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST, cfg.modem_preset); +} + +// ----------------------------------------------------------------------- +// applyModemConfig() coding rate tests (via reconfigure) +// ----------------------------------------------------------------------- + +static TestableRadioInterface *testRadio; + +// After fresh flash: coding_rate=0, use_preset=true, modem_preset=LONG_FAST +// CR should come from the preset (5 for LONG_FAST), not from the zero default. +static void test_applyModemConfig_freshFlashCodingRateNotZero() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + // coding_rate is 0 (default after init_zero, same as fresh flash) + + testRadio->reconfigure(); + + // LONG_FAST preset has cr=5; must never be 0 + TEST_ASSERT_EQUAL_UINT8(5, testRadio->getCr()); + TEST_ASSERT_EQUAL_UINT8(11, testRadio->getSf()); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 250.0f, testRadio->getBw()); +} + +// When coding_rate matches the preset exactly, should still use the preset value +static void test_applyModemConfig_codingRateMatchesPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; + config.lora.coding_rate = 8; // LONG_SLOW default is cr=8 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); +} + +// Custom CR higher than preset should be used +static void test_applyModemConfig_customCodingRateHigherThanPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + config.lora.coding_rate = 7; // LONG_FAST preset has cr=5, 7 > 5 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(7, testRadio->getCr()); +} + +// Custom CR lower than preset: preset wins (higher is more robust) +static void test_applyModemConfig_customCodingRateLowerThanPreset() +{ + config.lora = meshtastic_Config_LoRaConfig_init_zero; + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + config.lora.use_preset = true; + config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; + config.lora.coding_rate = 5; // LONG_SLOW preset has cr=8, 5 < 8 + + testRadio->reconfigure(); + + TEST_ASSERT_EQUAL_UINT8(8, testRadio->getCr()); +} + +void setUp(void) +{ + mockMeshService = new MockMeshService(); + service = mockMeshService; + + // RadioInterface computes slotTimeMsec during construction and expects myRegion to be valid. + config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US; + initRegion(); + + testRadio = new TestableRadioInterface(); +} +void tearDown(void) +{ + delete testRadio; + testRadio = nullptr; + service = nullptr; + delete mockMeshService; + mockMeshService = nullptr; +} void setup() { @@ -90,10 +213,16 @@ void setup() UNITY_BEGIN(); RUN_TEST(test_bwCodeToKHz_specialMappings); RUN_TEST(test_bwCodeToKHz_passthrough); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_noopWhenUsePresetFalse); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_nonWideRegion); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_setsDerivedFields_wideRegion); - RUN_TEST(test_bootstrapLoRaConfigFromPreset_fallsBackIfBandwidthExceedsRegionSpan); + RUN_TEST(test_validateConfigLora_noopWhenUsePresetFalse); + RUN_TEST(test_validateConfigLora_validPreset_nonWideRegion); + RUN_TEST(test_validateConfigLora_validPreset_wideRegion); + RUN_TEST(test_validateConfigLora_rejectsInvalidPresetForRegion); + RUN_TEST(test_clampConfigLora_invalidPresetClampedToDefault); + RUN_TEST(test_clampConfigLora_validPresetUnchanged); + RUN_TEST(test_applyModemConfig_freshFlashCodingRateNotZero); + RUN_TEST(test_applyModemConfig_codingRateMatchesPreset); + RUN_TEST(test_applyModemConfig_customCodingRateHigherThanPreset); + RUN_TEST(test_applyModemConfig_customCodingRateLowerThanPreset); exit(UNITY_END()); } diff --git a/test/test_traffic_management/test_main.cpp b/test/test_traffic_management/test_main.cpp new file mode 100644 index 000000000..ec54f2312 --- /dev/null +++ b/test/test_traffic_management/test_main.cpp @@ -0,0 +1,1160 @@ +#include "TestUtil.h" +#include + +#if defined(ARCH_PORTDUINO) +#define TM_TEST_ENTRY extern "C" +#else +#define TM_TEST_ENTRY +#endif + +#if HAS_TRAFFIC_MANAGEMENT + +#include "mesh/CryptoEngine.h" +#include "mesh/MeshService.h" +#include "mesh/NodeDB.h" +#include "mesh/Router.h" +#include "modules/TrafficManagementModule.h" +#include +#include +#include +#include +#include + +namespace +{ + +constexpr NodeNum kLocalNode = 0x11111111; +constexpr NodeNum kRemoteNode = 0x22222222; +constexpr NodeNum kTargetNode = 0x33333333; + +class MockNodeDB : public NodeDB +{ + public: + meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override + { + if (hasCachedNode && n == cachedNodeNum) + return &cachedNode; + return NodeDB::getMeshNode(n); + } + + void clearCachedNode() + { + hasCachedNode = false; + cachedNodeNum = 0; + cachedNode = meshtastic_NodeInfoLite_init_zero; + } + + void setCachedNode(NodeNum n) + { + clearCachedNode(); + hasCachedNode = true; + cachedNodeNum = n; + cachedNode.num = n; + cachedNode.has_user = true; + } + + private: + bool hasCachedNode = false; + NodeNum cachedNodeNum = 0; + meshtastic_NodeInfoLite cachedNode = meshtastic_NodeInfoLite_init_zero; +}; + +class MockRadioInterface : public RadioInterface +{ + public: + ErrorCode send(meshtastic_MeshPacket *p) override + { + packetPool.release(p); + return ERRNO_OK; + } + + uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override + { + (void)totalPacketLen; + (void)received; + return 0; + } +}; + +class MockRouter : public Router +{ + public: + ~MockRouter() + { + // Router allocates a global crypt lock in its constructor. + // Clean it up here so each test can build a fresh mock router. + delete cryptLock; + cryptLock = nullptr; + } + + ErrorCode send(meshtastic_MeshPacket *p) override + { + sentPackets.push_back(*p); + packetPool.release(p); + return ERRNO_OK; + } + + std::vector sentPackets; +}; + +class TrafficManagementModuleTestShim : public TrafficManagementModule +{ + public: + using TrafficManagementModule::alterReceived; + using TrafficManagementModule::handleReceived; + using TrafficManagementModule::resetEpoch; + using TrafficManagementModule::runOnce; + + bool ignoreRequestFlag() const { return ignoreRequest; } +}; + +MockNodeDB *mockNodeDB = nullptr; + +static void resetTrafficConfig() +{ + moduleConfig = meshtastic_LocalModuleConfig_init_zero; + moduleConfig.has_traffic_management = true; + moduleConfig.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero; + moduleConfig.traffic_management.enabled = true; + + config = meshtastic_LocalConfig_init_zero; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + + myNodeInfo.my_node_num = kLocalNode; + + router = nullptr; + service = nullptr; + + mockNodeDB->resetNodes(); + mockNodeDB->clearCachedNode(); + nodeDB = mockNodeDB; +} + +static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = from; + packet.to = to; + packet.id = 0x1001; + packet.channel = 0; + packet.hop_start = 3; + packet.hop_limit = 3; + packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.decoded.portnum = port; + packet.decoded.has_bitfield = true; + packet.decoded.bitfield = 0; + return packet; +} + +static meshtastic_MeshPacket makeUnknownPacket(NodeNum from, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = from; + packet.to = to; + packet.id = 0x2001; + packet.channel = 0; + packet.hop_start = 3; + packet.hop_limit = 3; + packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; + packet.encrypted.size = 0; + return packet; +} + +static meshtastic_MeshPacket makePositionPacket(NodeNum from, int32_t lat, int32_t lon, NodeNum to = NODENUM_BROADCAST) +{ + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, from, to); + meshtastic_Position pos = meshtastic_Position_init_zero; + pos.has_latitude_i = true; + pos.has_longitude_i = true; + pos.latitude_i = lat; + pos.longitude_i = lon; + + packet.decoded.payload.size = + pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_Position_msg, &pos); + return packet; +} + +static meshtastic_MeshPacket makeNodeInfoPacket(NodeNum from, const char *longName, const char *shortName) +{ + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, from, NODENUM_BROADCAST); + + meshtastic_User user = meshtastic_User_init_zero; + snprintf(user.id, sizeof(user.id), "!%08x", from); + strncpy(user.long_name, longName, sizeof(user.long_name) - 1); + strncpy(user.short_name, shortName, sizeof(user.short_name) - 1); + + packet.decoded.payload.size = + pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes), &meshtastic_User_msg, &user); + return packet; +} + +/** + * Verify the module is a no-op when traffic management is disabled. + * Important so config toggles cannot accidentally change routing behavior. + */ +static void test_tm_moduleDisabled_doesNothing(void) +{ + moduleConfig.has_traffic_management = false; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage result = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.packets_inspected); + TEST_ASSERT_EQUAL_UINT32(0, stats.unknown_packet_drops); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +/** + * Verify unknown-packet dropping uses N+1 threshold semantics. + * Important to catch off-by-one regressions in drop decisions. + */ +static void test_tm_unknownPackets_dropOnNPlusOne(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 2; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); + TEST_ASSERT_EQUAL_UINT32(3, stats.packets_inspected); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify duplicate position broadcasts inside the dedup window are dropped. + * Important because this is the primary airtime-saving behavior. + */ +static void test_tm_positionDedup_dropsDuplicateWithinWindow(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_GREATER_THAN_UINT32(0, first.decoded.payload.size); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify changed coordinates are forwarded even with dedup enabled. + * Important so real movement updates are never suppressed as duplicates. + */ +static void test_tm_positionDedup_allowsMovedPosition(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket moved = makePositionPacket(kRemoteNode, 384221234, -1210845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(moved); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify rate limiting drops only after exceeding the configured threshold. + * Important to protect threshold semantics from off-by-one regressions. + */ +static void test_tm_rateLimit_dropsOnlyAfterThreshold(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 3; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + ProcessMessage r3 = module.handleReceived(packet); + ProcessMessage r4 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r4)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); +} + +/** + * Verify routing/admin traffic is exempt from rate limiting. + * Important because throttling control traffic can destabilize the mesh. + */ +static void test_tm_rateLimit_skipsRoutingAndAdminPorts(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket routingPacket = makeDecodedPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode); + meshtastic_MeshPacket adminPacket = makeDecodedPacket(meshtastic_PortNum_ADMIN_APP, kRemoteNode); + + for (int i = 0; i < 4; i++) { + ProcessMessage rr = module.handleReceived(routingPacket); + ProcessMessage ar = module.handleReceived(adminPacket); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(rr)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(ar)); + } + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify packets sourced from this node bypass dedup and rate limiting. + * Important so local transmissions are not accidentally self-throttled. + */ +static void test_tm_fromUs_bypassesPositionAndRateFilters(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket positionPacket = makePositionPacket(kLocalNode, 374221234, -1220845678); + meshtastic_MeshPacket textPacket = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode); + + ProcessMessage p1 = module.handleReceived(positionPacket); + ProcessMessage p2 = module.handleReceived(positionPacket); + ProcessMessage t1 = module.handleReceived(textPacket); + ProcessMessage t2 = module.handleReceived(textPacket); + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify locally addressed packets are never dropped by transit shaping. + * Important so dedup/rate limiting do not suppress end-user delivery. + */ +static void test_tm_localDestination_bypassesTransitFilters(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket position1 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); + meshtastic_MeshPacket position2 = makePositionPacket(kRemoteNode, 374221234, -1220845678, kLocalNode); + meshtastic_MeshPacket text1 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); + meshtastic_MeshPacket text2 = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode); + + ProcessMessage p1 = module.handleReceived(position1); + ProcessMessage p2 = module.handleReceived(position2); + ProcessMessage t1 = module.handleReceived(text1); + ProcessMessage t2 = module.handleReceived(text2); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(p2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(t2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); + TEST_ASSERT_EQUAL_UINT32(0, stats.rate_limit_drops); +} + +/** + * Verify router role clamps NodeInfo response hops to router-safe maximum. + * Important so large config values cannot widen response scope unexpectedly. + */ +static void test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; + mockNodeDB->setCachedNode(kTargetNode); + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 5; + request.hop_limit = 1; // 4 hops away; router clamp should cap max at 3 + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +/** + * Verify NodeInfo direct-response success path and reply packet fields. + * Important because this path consumes the request and generates a spoofed cached reply. + */ +static void test_tm_nodeinfo_directResponse_respondsFromCache(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.config_ok_to_mqtt = true; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.id = 0x13572468; + request.hop_start = 3; + request.hop_limit = 3; // direct request (0 hops away) + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); + + const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); + TEST_ASSERT_EQUAL_INT(meshtastic_PortNum_NODEINFO_APP, reply.decoded.portnum); + TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); + TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); + TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); + TEST_ASSERT_FALSE(reply.decoded.want_response); + TEST_ASSERT_EQUAL_UINT8(0, reply.hop_limit); + TEST_ASSERT_EQUAL_UINT8(0, reply.hop_start); + TEST_ASSERT_EQUAL_UINT8(mockNodeDB->getLastByteOfNodeNum(kRemoteNode), reply.next_hop); + TEST_ASSERT_TRUE(reply.decoded.has_bitfield); + TEST_ASSERT_EQUAL_UINT8(BITFIELD_OK_TO_MQTT_MASK, reply.decoded.bitfield); +} + +/** + * Verify cached direct replies still preserve requester NodeInfo learning. + * Important so consuming the request does not skip NodeDB refresh for observers. + */ +static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "requester-long", "rq"); + request.to = kTargetNode; + request.decoded.want_response = true; + request.id = 0x01020304; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_NOT_NULL(requestor); + TEST_ASSERT_TRUE(requestor->has_user); + TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name); + TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name); + TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel); +} + +/** + * Verify client role only answers direct (0-hop) NodeInfo requests. + * Important so clients do not answer relayed requests outside intended scope. + */ +static void test_tm_nodeinfo_clientClamp_skipsWhenNotDirect(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 2; + request.hop_limit = 1; // 1 hop away; clients are clamped to max 0 + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); +} + +#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) +/** + * Verify non-PSRAM builds require NodeDB for direct NodeInfo responses. + * Important because fallback should only happen through node-wide data when + * the dedicated PSRAM cache does not exist. + */ +static void test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->clearCachedNode(); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); +} +#endif + +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) +/** + * Verify PSRAM NodeInfo cache can answer requests without NodeDB and that + * shouldRespondToNodeInfo() uses cached bitfield metadata. + */ +static void test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + config.lora.config_ok_to_mqtt = true; + mockNodeDB->clearCachedNode(); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket observed = makeNodeInfoPacket(kTargetNode, "target-long", "tg"); + observed.decoded.has_bitfield = true; + observed.decoded.bitfield = BITFIELD_WANT_RESPONSE_MASK; + observed.channel = 2; + observed.rx_time = 123456; + + ProcessMessage observedResult = module.handleReceived(observed); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(observedResult)); + + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.id = 0x24681357; + request.channel = 1; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(result)); + TEST_ASSERT_TRUE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(1, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(1, static_cast(mockRouter.sentPackets.size())); + + const meshtastic_MeshPacket &reply = mockRouter.sentPackets.front(); + TEST_ASSERT_TRUE(reply.decoded.has_bitfield); + TEST_ASSERT_EQUAL_UINT8(static_cast(BITFIELD_WANT_RESPONSE_MASK | BITFIELD_OK_TO_MQTT_MASK), reply.decoded.bitfield); + TEST_ASSERT_EQUAL_UINT32(kTargetNode, reply.from); + TEST_ASSERT_EQUAL_UINT32(kRemoteNode, reply.to); + TEST_ASSERT_EQUAL_UINT8(request.channel, reply.channel); + TEST_ASSERT_EQUAL_UINT32(request.id, reply.decoded.request_id); +} + +/** + * Verify PSRAM cache misses do not fall back to NodeDB. + * Important so the dedicated PSRAM index stays logically separate from + * NodeInfoModule/NodeDB when PSRAM is available. + */ +static void test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb(void) +{ + moduleConfig.traffic_management.nodeinfo_direct_response = true; + moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10; + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + mockNodeDB->setCachedNode(kTargetNode); + + MockRouter mockRouter; + mockRouter.addInterface(std::unique_ptr(new MockRadioInterface())); + MeshService mockService; + router = &mockRouter; + service = &mockService; + + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket request = makeDecodedPacket(meshtastic_PortNum_NODEINFO_APP, kRemoteNode, kTargetNode); + request.decoded.want_response = true; + request.hop_start = 3; + request.hop_limit = 3; + + ProcessMessage result = module.handleReceived(request); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.ignoreRequestFlag()); + TEST_ASSERT_EQUAL_UINT32(0, stats.nodeinfo_cache_hits); + TEST_ASSERT_EQUAL_UINT32(0, static_cast(mockRouter.sentPackets.size())); +} +#endif + +/** + * Verify relayed telemetry broadcasts are hop-exhausted when enabled. + * Important to prevent further mesh propagation while still allowing one relay step. + */ +static void test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 3; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); + TEST_ASSERT_EQUAL_UINT8(3, packet.hop_start); + TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify hop exhaustion skips unicast and local-origin packets. + * Important to avoid mutating traffic that should retain normal forwarding behavior. + */ +static void test_tm_alterReceived_skipsLocalAndUnicast(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket unicast = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, kTargetNode); + unicast.hop_start = 5; + unicast.hop_limit = 3; + module.alterReceived(unicast); + TEST_ASSERT_EQUAL_UINT8(3, unicast.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(unicast)); + + meshtastic_MeshPacket fromUs = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kLocalNode, NODENUM_BROADCAST); + fromUs.hop_start = 5; + fromUs.hop_limit = 3; + module.alterReceived(fromUs); + TEST_ASSERT_EQUAL_UINT8(3, fromUs.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(fromUs)); + + meshtastic_TrafficManagementStats stats = module.getStats(); + TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); +} + +/** + * Verify position dedup window expires and later duplicates are allowed. + * Important so periodic identical reports can resume after cooldown. + */ +static void test_tm_positionDedup_allowsDuplicateAfterIntervalExpires(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 1; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket third = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(third); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify interval=0 disables position deduplication. + * Important because this is an explicit configuration escape hatch. + */ +static void test_tm_positionDedup_intervalZero_neverDrops(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 0; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify precision values above 32 fall back to default precision. + * Important so invalid config uses the documented default behavior. + */ +static void test_tm_positionDedup_precisionAbove32_usesDefaultPrecision(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 99; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 384221234, -1210845678); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify precision=32 does not collapse all positions to one fingerprint. + * Important to prevent false duplicate drops at the full-precision boundary. + */ +static void test_tm_positionDedup_precision32_allowsDistinctPositions(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 32; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify invalid precision=0 is treated as full precision. + * Important so invalid config does not collapse all positions into one fingerprint. + */ +static void test_tm_positionDedup_precisionZero_allowsDistinctPositions(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 0; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket second = makePositionPacket(kRemoteNode, 374221235, -1220845677); + + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(second); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(0, stats.position_dedup_drops); +} + +/** + * Verify epoch reset invalidates stale position identity for dedup. + * Important so reset paths cannot leak prior packet identity into new windows. + */ +static void test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket afterReset = makePositionPacket(kRemoteNode, 374221234, -1220845678); + meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 374221234, -1220845678); + + ProcessMessage r1 = module.handleReceived(first); + module.resetEpoch(millis()); + ProcessMessage r2 = module.handleReceived(afterReset); + ProcessMessage r3 = module.handleReceived(duplicate); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify non-position cache state does not make the first fingerprint-0 position look duplicated. + * Important so unified cache entries from other features cannot leak into dedup decisions. + */ +static void test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero(void) +{ + moduleConfig.traffic_management.position_dedup_enabled = true; + moduleConfig.traffic_management.position_precision_bits = 16; + moduleConfig.traffic_management.position_min_interval_secs = 300; + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 10; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + meshtastic_MeshPacket first = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); + meshtastic_MeshPacket duplicate = makePositionPacket(kRemoteNode, 0x12300000, 0x45600000); + + ProcessMessage seeded = module.handleReceived(text); + ProcessMessage r1 = module.handleReceived(first); + ProcessMessage r2 = module.handleReceived(duplicate); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(seeded)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_UINT32(1, stats.position_dedup_drops); +} + +/** + * Verify rate-limit counters reset after the window expires. + * Important so temporary bursts do not cause persistent throttling. + */ +static void test_tm_rateLimit_resetsAfterWindowExpires(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 1; + moduleConfig.traffic_management.rate_limit_max_packets = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); +} + +/** + * Verify rate-limit thresholds above 255 effectively clamp to 255. + * Important because counters are uint8_t and must not overflow behavior. + */ +static void test_tm_rateLimit_thresholdAbove255_clamps(void) +{ + moduleConfig.traffic_management.rate_limit_enabled = true; + moduleConfig.traffic_management.rate_limit_window_secs = 60; + moduleConfig.traffic_management.rate_limit_max_packets = 300; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + + for (int i = 0; i < 255; i++) { + ProcessMessage result = module.handleReceived(packet); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + } + ProcessMessage dropped = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); + TEST_ASSERT_EQUAL_UINT32(1, stats.rate_limit_drops); +} + +/** + * Verify unknown-packet tracking resets after its active window expires. + * Important so old unknown traffic does not trigger delayed drops. + */ +static void test_tm_unknownPackets_resetAfterWindowExpires(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 1; + moduleConfig.traffic_management.rate_limit_window_secs = 1; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + ProcessMessage r1 = module.handleReceived(packet); + ProcessMessage r2 = module.handleReceived(packet); + testDelay(1200); + ProcessMessage r3 = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r1)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(r2)); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(r3)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); +} + +/** + * Verify unknown threshold values above 255 clamp to the counter ceiling. + * Important to align config semantics with saturating counter storage. + */ +static void test_tm_unknownPackets_thresholdAbove255_clamps(void) +{ + moduleConfig.traffic_management.drop_unknown_enabled = true; + moduleConfig.traffic_management.unknown_packet_threshold = 300; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode); + + for (int i = 0; i < 255; i++) { + ProcessMessage result = module.handleReceived(packet); + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + } + ProcessMessage dropped = module.handleReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::STOP), static_cast(dropped)); + TEST_ASSERT_EQUAL_UINT32(1, stats.unknown_packet_drops); +} + +/** + * Verify relayed position broadcasts can also be hop-exhausted. + * Important because telemetry and position use separate exhaust flags. + */ +static void test_tm_alterReceived_exhaustsRelayedPositionBroadcast(void) +{ + moduleConfig.traffic_management.exhaust_hop_position = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makePositionPacket(kRemoteNode, 374221234, -1220845678, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 2; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(0, packet.hop_limit); + TEST_ASSERT_EQUAL_UINT8(4, packet.hop_start); + TEST_ASSERT_TRUE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify hop exhaustion ignores undecoded/encrypted packets. + * Important so we never mutate packets that were not decoded by this module. + */ +static void test_tm_alterReceived_skipsUndecodedPackets(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + meshtastic_MeshPacket packet = makeUnknownPacket(kRemoteNode, NODENUM_BROADCAST); + packet.hop_start = 5; + packet.hop_limit = 3; + + module.alterReceived(packet); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_UINT8(5, packet.hop_start); + TEST_ASSERT_EQUAL_UINT8(3, packet.hop_limit); + TEST_ASSERT_FALSE(module.shouldExhaustHops(packet)); + TEST_ASSERT_EQUAL_UINT32(0, stats.hop_exhausted_packets); +} + +/** + * Verify exhaustRequested is per-packet and resets on next handleReceived(). + * Important so a prior packet cannot leak hop-exhaust state into later packets. + */ +static void test_tm_alterReceived_resetExhaustFlagOnNextPacket(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket telemetry = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + telemetry.hop_start = 5; + telemetry.hop_limit = 3; + module.alterReceived(telemetry); + TEST_ASSERT_TRUE(module.shouldExhaustHops(telemetry)); + + meshtastic_MeshPacket text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode); + ProcessMessage result = module.handleReceived(text); + meshtastic_TrafficManagementStats stats = module.getStats(); + + TEST_ASSERT_EQUAL_INT(static_cast(ProcessMessage::CONTINUE), static_cast(result)); + TEST_ASSERT_FALSE(module.shouldExhaustHops(telemetry)); + TEST_ASSERT_EQUAL_UINT32(1, stats.hop_exhausted_packets); +} + +/** + * Verify exhaust requests are packet-scoped (from + id). + * Important so stale state from one packet cannot influence unrelated packets + * that pass through duplicate/rebroadcast paths before handleReceived(). + */ +static void test_tm_alterReceived_exhaustFlag_isPacketScoped(void) +{ + moduleConfig.traffic_management.exhaust_hop_telemetry = true; + TrafficManagementModuleTestShim module; + + meshtastic_MeshPacket exhausted = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kRemoteNode, NODENUM_BROADCAST); + exhausted.id = 0x1010; + exhausted.hop_start = 5; + exhausted.hop_limit = 3; + module.alterReceived(exhausted); + + meshtastic_MeshPacket unrelated = makeDecodedPacket(meshtastic_PortNum_TELEMETRY_APP, kTargetNode, NODENUM_BROADCAST); + unrelated.id = 0x2020; + unrelated.hop_start = 4; + unrelated.hop_limit = 0; + + TEST_ASSERT_TRUE(module.shouldExhaustHops(exhausted)); + TEST_ASSERT_FALSE(module.shouldExhaustHops(unrelated)); +} + +/** + * Verify runOnce() returns sleep-forever interval when module is disabled. + * Important to ensure the maintenance thread is effectively inert when off. + */ +static void test_tm_runOnce_disabledReturnsMaxInterval(void) +{ + moduleConfig.traffic_management.enabled = false; + TrafficManagementModuleTestShim module; + + int32_t interval = module.runOnce(); + + TEST_ASSERT_EQUAL_INT32(INT32_MAX, interval); +} + +/** + * Verify runOnce() returns the maintenance cadence when enabled. + * Important so periodic cache housekeeping continues at expected interval. + */ +static void test_tm_runOnce_enabledReturnsMaintenanceInterval(void) +{ + TrafficManagementModuleTestShim module; + + int32_t interval = module.runOnce(); + + TEST_ASSERT_EQUAL_INT32(60 * 1000, interval); +} + +} // namespace + +void setUp(void) +{ + resetTrafficConfig(); +} +void tearDown(void) {} + +TM_TEST_ENTRY void setup() +{ + delay(10); + delay(2000); + + initializeTestEnvironment(); + mockNodeDB = new MockNodeDB(); + nodeDB = mockNodeDB; + + UNITY_BEGIN(); + RUN_TEST(test_tm_moduleDisabled_doesNothing); + RUN_TEST(test_tm_unknownPackets_dropOnNPlusOne); + RUN_TEST(test_tm_positionDedup_dropsDuplicateWithinWindow); + RUN_TEST(test_tm_positionDedup_allowsMovedPosition); + RUN_TEST(test_tm_rateLimit_dropsOnlyAfterThreshold); + RUN_TEST(test_tm_rateLimit_skipsRoutingAndAdminPorts); + RUN_TEST(test_tm_fromUs_bypassesPositionAndRateFilters); + RUN_TEST(test_tm_localDestination_bypassesTransitFilters); + RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops); + RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache); + RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo); + RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect); +#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) + RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips); +#endif +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + RUN_TEST(test_tm_nodeinfo_directResponse_psramCacheRespondsAndPreservesBitfield); + RUN_TEST(test_tm_nodeinfo_directResponse_psramMissDoesNotFallbackToNodeDb); +#endif + RUN_TEST(test_tm_alterReceived_exhaustsRelayedTelemetryBroadcast); + RUN_TEST(test_tm_alterReceived_skipsLocalAndUnicast); + RUN_TEST(test_tm_positionDedup_allowsDuplicateAfterIntervalExpires); + RUN_TEST(test_tm_positionDedup_intervalZero_neverDrops); + RUN_TEST(test_tm_positionDedup_precisionAbove32_usesDefaultPrecision); + RUN_TEST(test_tm_positionDedup_precision32_allowsDistinctPositions); + RUN_TEST(test_tm_positionDedup_precisionZero_allowsDistinctPositions); + RUN_TEST(test_tm_positionDedup_epochReset_doesNotDropFirstPacketAfterReset); + RUN_TEST(test_tm_positionDedup_priorRateState_doesNotDropFirstFingerprintZero); + RUN_TEST(test_tm_rateLimit_resetsAfterWindowExpires); + RUN_TEST(test_tm_rateLimit_thresholdAbove255_clamps); + RUN_TEST(test_tm_unknownPackets_resetAfterWindowExpires); + RUN_TEST(test_tm_unknownPackets_thresholdAbove255_clamps); + RUN_TEST(test_tm_alterReceived_exhaustsRelayedPositionBroadcast); + RUN_TEST(test_tm_alterReceived_skipsUndecodedPackets); + RUN_TEST(test_tm_alterReceived_resetExhaustFlagOnNextPacket); + RUN_TEST(test_tm_alterReceived_exhaustFlag_isPacketScoped); + RUN_TEST(test_tm_runOnce_disabledReturnsMaxInterval); + RUN_TEST(test_tm_runOnce_enabledReturnsMaintenanceInterval); + exit(UNITY_END()); +} + +TM_TEST_ENTRY void loop() {} + +#else + +void setUp(void) {} +void tearDown(void) {} + +TM_TEST_ENTRY void setup() +{ + initializeTestEnvironment(); + UNITY_BEGIN(); + exit(UNITY_END()); +} + +TM_TEST_ENTRY void loop() {} + +#endif diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp index 992668d97..3bd84b55c 100644 --- a/test/test_transmit_history/test_main.cpp +++ b/test/test_transmit_history/test_main.cpp @@ -1,5 +1,6 @@ #include "TestUtil.h" #include "TransmitHistory.h" +#include "gps/RTC.h" #include #include @@ -161,44 +162,141 @@ static void test_save_and_load_round_trip() // After loadFromDisk, millis should be seeded (non-zero) for stored entries uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); if (restoredNodeInfo > 0) { - // If epoch was stored, millis should be seeded from load + // If epoch was stored (set seconds ago), epoch-conversion gives elapsed โ‰ˆ 0 s, + // so getLastSentToMeshMillis() should return a non-zero value. TEST_ASSERT_NOT_EQUAL(0, restoredMillis); } } // --- Boot without RTC scenario --- -static void test_load_seeds_millis_even_without_rtc() +// Crash-reboot protection: a send that happened moments before the reboot must still +// throttle after reload. This works because getLastSentToMeshMillis() reconstructs +// a millis()-relative timestamp from the stored epoch, and Throttle uses unsigned +// subtraction so the age survives wraparound even when uptime is near zero. +static void test_boot_after_recent_send_still_throttles() { - // This tests the critical crash-reboot scenario: - // After loadFromDisk(), even if getTime() returns 0 (no RTC), - // lastMillis should be seeded so throttle blocks immediate re-broadcast. - transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); transmitHistory->saveToDisk(); - // Simulate reboot: destroy and recreate + // Simulate reboot delete transmitHistory; transmitHistory = nullptr; transmitHistory = TransmitHistory::getInstance(); transmitHistory->loadFromDisk(); - // The key insight: after load, getLastSentToMeshMillis should return non-zero - // because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry. - // This ensures throttle works even without RTC. + // Epoch was set seconds ago; reconstructed age is still within the 10-min window. uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); - uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); - if (epoch > 0) { - // Data was persisted โ€” millis must be seeded - TEST_ASSERT_NOT_EQUAL(0, result); - - // And it should cause throttle to block (treating as "just sent") - bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); - TEST_ASSERT_TRUE(withinInterval); + if (epoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; } - // If epoch == 0, RTC wasn't available โ€” no data was saved, so nothing to restore. - // This is expected on platforms without RTC during the very first boot. + + TEST_ASSERT_NOT_EQUAL(0, result); + bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +// Regression test for issue #9901: +// A device powered off for longer than the throttle window must broadcast NodeInfo +// on its next boot โ€” it must not be silenced because loadFromDisk() once treated +// every loaded entry as "just sent" by seeding lastMillis to millis() at boot. +static void test_boot_after_long_gap_allows_nodeinfo() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 30 minutes ago (outside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (30 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_FALSE_MESSAGE(throttled, "NodeInfo must not be throttled after a 30-min gap (#9901)"); +} + +// Complementary: a rapid reboot must still throttle (crash-loop protection), even +// though the reconstructed lastMs may wrap because current uptime is small. +static void test_boot_within_throttle_window_still_throttles() +{ + if (getRTCQuality() <= RTCQualityNone) { + TEST_IGNORE_MESSAGE("No RTC available; skipping epoch-dependent test"); + return; + } + + uint32_t now = getTime(); + + // Simulate: last NodeInfo sent 5 minutes ago (inside the 10-min throttle window) + transmitHistory->setLastSentAtEpoch(meshtastic_PortNum_NODEINFO_APP, now - (5 * 60)); + transmitHistory->saveToDisk(); + + // Simulate reboot + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + uint32_t restoredEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (restoredEpoch == 0) { + TEST_IGNORE_MESSAGE("Epoch not persisted; skipping"); + return; + } + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "NodeInfo must still be throttled when last send was within the 10-min window"); +} + +static void test_boot_without_time_source_still_throttles_recent_restart() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(31); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + bool throttled = (restoredMs != 0) && Throttle::isWithinTimespanMs(restoredMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE_MESSAGE(throttled, "Recent no-RTC reboots should still suppress duplicate NodeInfo"); +} + +static void test_boot_without_time_source_expires_boot_relative_history() +{ + setBootRelativeTimeForUnitTest(32); + transmitHistory->setLastSentAtBootRelative(meshtastic_PortNum_NODEINFO_APP, 32); + transmitHistory->saveToDisk(); + + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + + setBootRelativeTimeForUnitTest(400); + transmitHistory->loadFromDisk(); + + uint32_t restoredMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, restoredMs, "Boot-relative history should only suppress near-term restarts"); } void setup() @@ -222,7 +320,15 @@ void setup() // Persistence RUN_TEST(test_save_and_load_round_trip); - RUN_TEST(test_load_seeds_millis_even_without_rtc); + RUN_TEST(test_boot_after_recent_send_still_throttles); + + // Issue #9901 regression tests + RUN_TEST(test_boot_after_long_gap_allows_nodeinfo); + RUN_TEST(test_boot_within_throttle_window_still_throttles); + + // No-RTC regression tests + RUN_TEST(test_boot_without_time_source_still_throttles_recent_restart); + RUN_TEST(test_boot_without_time_source_expires_boot_relative_history); exit(UNITY_END()); } diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 9e916aae2..b81f09362 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -18,6 +18,7 @@ // "USERPREFS_CHANNEL_2_UPLINK_ENABLED": "false", // "USERPREFS_CONFIG_GPS_MODE": "meshtastic_Config_PositionConfig_GpsMode_ENABLED", // "USERPREFS_CONFIG_LORA_IGNORE_MQTT": "true", + // "USERPREFS_LORA_TX_DISABLED": "1", // If set, forces config.lora.tx_enabled=false during lora bootstrap // "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", // "USERPREFS_CONFIG_OWNER_LONG_NAME": "My Long Name", // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini index 068d71011..c2e89b333 100644 --- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini +++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini @@ -13,7 +13,3 @@ board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 460800 -lib_deps = - ${esp32_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index bbbcd3cbe..b9d6f5c50 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -5,7 +5,10 @@ custom_esp32_kind = custom_mtjson_part = platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.12.0 + platformio/espressif32@6.13.0 +platform_packages = + # renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs + platformio/tool-mklittlefs@1.203.210628 extra_scripts = ${env.extra_scripts} @@ -24,14 +27,19 @@ board_build.filesystem = littlefs # Remove -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL for low level BLE logging. # See library directory for BLE logging possible values: .pio/libdeps/tbeam/NimBLE-Arduino/src/log_common/log_common.h # This overrides the BLE logging default of LOG_LEVEL_INFO (1) from: .pio/libdeps/tbeam/NimBLE-Arduino/src/esp_nimble_cfg.h -build_unflags = -fno-lto +build_unflags = + -fno-lto + # Keep explicit std unflags on ESP32; base-level unflags are not sufficient + # to prevent framework-injected C++11 fallback on this platform. + -std=c++11 + -std=gnu++11 build_flags = ${arduino_base.build_flags} -flto -Wall -Wextra -Isrc/platform/esp32 - -std=c++11 + -std=gnu++17 -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL @@ -59,13 +67,13 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip + https://github.com/meshtastic/esp32_https_server/archive/0c71f380390ad483ff134ad938e07f6cf1226c5b.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@1.4.3 # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini index 20ce38fae..f40f1d064 100644 --- a/variants/esp32/esp32.ini +++ b/variants/esp32/esp32.ini @@ -4,25 +4,29 @@ extends = esp32_common custom_esp32_kind = esp32 +build_src_filter = + ${esp32_common.build_src_filter} + - + - + build_flags = ${esp32_common.build_flags} -DMESHTASTIC_EXCLUDE_AUDIO=1 -; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra -; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets + -DMESHTASTIC_EXCLUDE_ACCELEROMETER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} ${networking_extra.lib_deps} + ${radiolib_base.lib_deps} ${environmental_base.lib_deps} ${environmental_extra_no_bsec.lib_deps} - ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master - https://github.com/meshtastic/esp32_https_server/archive/b0f3960b3e8444563280656d88e22b5899481884.zip # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino - h2zero/NimBLE-Arduino@^1.4.3 - # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master - https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip - # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib - https://github.com/lewisxhe/XPowersLib/archive/v0.3.3.zip + h2zero/NimBLE-Arduino@1.4.3 + # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib + lewisxhe/XPowersLib@0.3.3 # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto - rweather/Crypto@0.4.0 \ No newline at end of file + rweather/Crypto@0.4.0 diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index 7393c291f..e107bd893 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -19,7 +19,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 lib_ignore = diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 16ecb99cc..b27ebf28e 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 Explorer custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1_EXPLORER diff --git a/variants/esp32/nano-g1/platformio.ini b/variants/esp32/nano-g1/platformio.ini index 724e008c7..b2e392dbd 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Nano G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D NANO_G1 diff --git a/variants/esp32/rak11200/platformio.ini b/variants/esp32/rak11200/platformio.ini index 63821a092..b48d638fb 100644 --- a/variants/esp32/rak11200/platformio.ini +++ b/variants/esp32/rak11200/platformio.ini @@ -16,4 +16,8 @@ build_flags = ${esp32_base.build_flags} -D RAK_11200 -I variants/esp32/rak11200 + -DMESHTASTIC_EXCLUDE_WEBSERVER=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_RANGETEST=1 + -DMESHTASTIC_EXCLUDE_MQTT=1 upload_speed = 115200 diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index fad003b20..5a7f33485 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Station G1 custom_meshtastic_tags = B&Q extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D STATION_G1 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index dbaccee8f..c9e6cce1f 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -10,14 +10,12 @@ custom_meshtastic_images = tbeam.svg custom_meshtastic_tags = LilyGo extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam board_check = true build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue -ULED_BUILTIN upload_speed = 921600 @@ -32,5 +30,5 @@ lib_deps = ${env:tbeam.lib_deps} # renovate: datasource=github-tags depName=meshtastic-st7796 packageName=meshtastic/st7796 https://github.com/meshtastic/st7796/archive/1.0.5.zip - # renovate: datasource=custom.pio depName=lewisxhe-SensorLib packageName=lewisxhe/library/SensorLib + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index e2763fdec..1809ba56c 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_tags = LilyGo board_level = extra extends = esp32_base -board = ttgo-t-beam +board = ttgo-tbeam build_flags = ${esp32_base.build_flags} -D TBEAM_V07 diff --git a/variants/esp32/tlora_v1/platformio.ini b/variants/esp32/tlora_v1/platformio.ini index c45cc2ce9..5f72d634e 100644 --- a/variants/esp32/tlora_v1/platformio.ini +++ b/variants/esp32/tlora_v1/platformio.ini @@ -13,4 +13,5 @@ build_flags = ${esp32_base.build_flags} -D TLORA_V1 -I variants/esp32/tlora_v1 + -ULED_BUILTIN upload_speed = 115200 diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini index aa62fb4fc..5eb1c9977 100644 --- a/variants/esp32c3/heltec_hru_3601/platformio.ini +++ b/variants/esp32c3/heltec_hru_3601/platformio.ini @@ -5,6 +5,3 @@ build_flags = ${esp32c3_base.build_flags} -D HELTEC_HRU_3601 -I variants/esp32c3/heltec_hru_3601 -lib_deps = ${esp32c3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32c6/esp32c6.ini b/variants/esp32c6/esp32c6.ini index 9ee8591be..cdd9f9868 100644 --- a/variants/esp32c6/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -3,12 +3,14 @@ extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip +platform_packages = + # HACK: This release was automatically removed upstream + framework-arduinoespressif32 @ https://github.com/vidplace7/platform-espressif32/releases/download/meshtastic-esp32c6/framework-arduinoespressif32-all-release_v5.1-124d64e.zip build_flags = ${arduino_base.build_flags} -Wall -Wextra -Isrc/platform/esp32 - -std=c++11 -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING -DSERIAL_BUFFER_SIZE=4096 -DLIBPAX_ARDUINO diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index 4c04d0c26..bf605ca61 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -23,8 +23,6 @@ build_unflags = -D HAS_WIFI lib_deps = ${esp32c6_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@2.3.7 build_flags = diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h index 8768fd70e..c8e56426f 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h @@ -1,6 +1,3 @@ -// Status -#define LED_POWER 1 - #define PIN_BUTTON1 47 // ๅŠŸ่ƒฝ้”ฎ #define PIN_BUTTON2 4 // ็”ตๆบ้”ฎ #define ALT_BUTTON_PIN PIN_BUTTON2 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp index 917341560..51a91bef0 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp @@ -9,11 +9,11 @@ void earlyInitVariant() io.pinMode(PCA_PIN_EINK_EN, OUTPUT); io.pinMode(PCA_PIN_POWER_EN, OUTPUT); io.pinMode(PCA_LED_POWER, OUTPUT); - io.pinMode(PCA_LED_USER, OUTPUT); + io.pinMode(PCA_LED_NOTIFICATION, OUTPUT); io.pinMode(PCA_LED_ENABLE, OUTPUT); io.digitalWrite(PCA_PIN_POWER_EN, HIGH); - io.digitalWrite(PCA_LED_USER, LOW); + io.digitalWrite(PCA_LED_NOTIFICATION, LOW); io.digitalWrite(PCA_LED_ENABLE, LOW); } diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 223f60264..2d02c7f27 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -8,9 +8,9 @@ // LED // Both of these are on the GPIO expander -#define PCA_LED_USER 1 // the Blue LED -#define PCA_LED_ENABLE 2 // the power supply to the LEDs, in an OR arrangement with VBUS power -#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. +#define PCA_LED_NOTIFICATION 1 // the Blue LED +#define PCA_LED_ENABLE 2 // the power supply to the LEDs, in an OR arrangement with VBUS power +#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. #define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING // USB_CHECK @@ -18,7 +18,7 @@ #define BATTERY_PIN 8 #define ADC_CHANNEL ADC1_GPIO8_CHANNEL -#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_MULTIPLIER 2.0 // 2.0 + 10% for correction of display undervoltage. #define PIN_BUZZER 9 @@ -86,6 +86,7 @@ #define BUTTON_PIN PIN_BUTTON1 #define BUTTON_PIN_ALT PIN_BUTTON2 +#define ALT_BUTTON_WAKE #define SERIAL_PRINT_PORT 0 #endif diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index 9230e08c5..90e4910f4 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -11,9 +11,7 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 + zinggjm/GxEPD2@1.6.8 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini index c53831c9f..60b030d42 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini @@ -8,10 +8,6 @@ board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/dreamcatcher/platformio.ini b/variants/esp32s3/dreamcatcher/platformio.ini index c830346e0..64d1b993d 100644 --- a/variants/esp32s3/dreamcatcher/platformio.ini +++ b/variants/esp32s3/dreamcatcher/platformio.ini @@ -12,8 +12,8 @@ build_flags = -D ARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index e0f6f0760..1b91a02bb 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -41,8 +41,8 @@ build_flags = ${esp32s3_base.build_flags} -Os lib_deps = ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 @@ -84,6 +84,7 @@ custom_meshtastic_images = crowpanel_2_4.svg, crowpanel_2_8.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_small_esp32s3_base build_flags = @@ -119,6 +120,7 @@ custom_meshtastic_images = crowpanel_3_5.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_small_esp32s3_base board_level = pr @@ -158,6 +160,7 @@ custom_meshtastic_images = crowpanel_5_0.svg, crowpanel_7_0.svg custom_meshtastic_tags = Elecrow custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = crowpanel_large_esp32s3_base build_flags = diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index aad29247b..64f50f80e 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -23,6 +23,4 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 + zinggjm/GxEPD2@1.6.8 diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini index 013280db1..9a5384ccd 100644 --- a/variants/esp32s3/heltec_sensor_hub/platformio.ini +++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini @@ -8,7 +8,3 @@ build_flags = -I variants/esp32s3/heltec_sensor_hub -D HELTEC_SENSOR_HUB -ULED_BUILTIN - -lib_deps = ${esp32s3_base.lib_deps} - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 72c53ded0..0336bf983 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -6,7 +6,9 @@ board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 + -D HAS_LORA_FEM=1 -I variants/esp32s3/heltec_v4 + -ULED_BUILTIN [env:heltec-v4] @@ -20,6 +22,7 @@ custom_meshtastic_images = heltec_v4.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = heltec_v4_base build_flags = @@ -66,7 +69,10 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1560 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LV_LVGL_H_INCLUDE_SIMPLE -D LV_CONF_INCLUDE_SIMPLE -D LV_COMP_CONF_INCLUDE_SIMPLE @@ -81,9 +87,9 @@ build_flags = -D USE_PACKET_API -D LGFX_DRIVER=LGFX_HELTEC_V4_TFT -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\" - -D VIEW_320x240 - -D MAP_FULL_REDRAW - -D DISPLAY_SIZE=320x240 ; landscape mode + -D VIEW_240x320 + -D DISPLAY_SET_RESOLUTION + -D DISPLAY_SIZE=240x320 ; portrait mode -D LGFX_PIN_SCK=17 -D LGFX_PIN_MOSI=33 -D LGFX_PIN_DC=16 diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 1c1168d94..72f55d09f 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -29,9 +29,17 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +// Enable Traffic Management Module for Heltec V4 +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif + // ---- GC1109 RF FRONT END CONFIGURATION ---- -// The Heltec V4 uses a GC1109 FEM chip with integrated PA and LNA -// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna // Measured net TX gain (non-linear due to PA compression): // +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) // +10dB at 16-17dBm input @@ -47,15 +55,31 @@ // CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 -#define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) - // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) -// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 and KCT8103L LDO power enable +#define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) + #if HAS_TFT #define USE_TFTDISPLAY 1 #endif @@ -73,4 +97,4 @@ // Seems to be missing on this new board #define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU #define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS -#define GPS_THREAD_INTERVAL 50 \ No newline at end of file +#define GPS_THREAD_INTERVAL 50 diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 1c4c69afe..bd1b73d2b 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = heltec-vision-master-e213.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_vision_master_e213 diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index 5affd24de..9fdb023de 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = heltec-vision-master-e290.svg custom_meshtastic_tags = Heltec custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_vision_master_e290 diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index 44e8b2f2d..be1ea192d 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -20,5 +20,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip + https://github.com/meshtastic/st7789/archive/a787beea5c6c8f864ba6787eb432bbefc575e6ad.zip upload_speed = 921600 diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index ce4bed30e..acd619c48 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_display_name = Heltec Wireless Paper custom_meshtastic_images = heltec-wireless-paper.svg custom_meshtastic_tags = Heltec custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = heltec_wifi_lora_32_V3 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h index 9fb825002..d30e2fcb7 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/pins_arduino.h @@ -13,8 +13,8 @@ static const uint8_t TX = 43; static const uint8_t RX = 44; -static const uint8_t SDA = 5; -static const uint8_t SCL = 6; +static const uint8_t SDA = 6; +static const uint8_t SCL = 17; static const uint8_t SS = 8; static const uint8_t MOSI = 10; diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index a5277ba19..ebf0118bb 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -17,6 +17,7 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker_v2 -D HELTEC_WIRELESS_TRACKER_V2 + -D HAS_LORA_FEM=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index 0ca2dfc03..7c797a503 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -73,29 +73,22 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 -// ---- GC1109 RF FRONT END CONFIGURATION ---- -// The Heltec Wireless Tracker V2 uses a GC1109 FEM chip with integrated PA and LNA -// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna -// Measured net TX gain (non-linear due to PA compression): -// +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) -// +10dB at 16-17dBm input -// +9dB at 18-19dBm input -// +7dB at 21dBm input (e.g., 21dBm in -> 28dBm out max) -// Control logic (from GC1109 datasheet): +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 // Shutdown: CSD=0, CTX=X, CPS=X -// Receive LNA: CSD=1, CTX=0, CPS=X (17dB gain, 2dB NF) -// Transmit bypass: CSD=1, CTX=1, CPS=0 (~1dB loss, no PA) -// Transmit PA: CSD=1, CTX=1, CPS=1 (full PA enabled) // Pin mapping: -// CTX (pin 6) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) // CSD (pin 4) -> GPIO4: Chip enable (HIGH=on, LOW=shutdown) -// CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) +// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 -#define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) -// GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) -// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp -// Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 \ No newline at end of file +#define USE_KCT8103L_PA +#define LORA_PA_POWER 7 // VFEM_Ctrl - KCT8103L LDO power enable +#define LORA_KCT8103L_PA_CSD 4 // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) \ No newline at end of file diff --git a/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h b/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h new file mode 100644 index 000000000..12581cde0 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/pins_arduino.h @@ -0,0 +1,20 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define USB_VID 0x303a // USB JTAG/serial debug unit ID +#define USB_PID 0x1001 // USB JTAG/serial debug unit ID + +static const uint8_t SS = 5; +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; +static const uint8_t ADC = 10; +static const uint8_t TXD2 = 13; +static const uint8_t MOSI = 14; +static const uint8_t RXD2 = 15; +static const uint8_t MISO = 39; +static const uint8_t SCK = 40; + +#endif diff --git a/variants/esp32s3/m5stack_cardputer_adv/platformio.ini b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini new file mode 100644 index 000000000..7da039cd4 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/platformio.ini @@ -0,0 +1,25 @@ +; M5stack Cardputer Advanced +[env:m5stack-cardputer-adv] +extends = esp32s3_base +board = m5stack-stamps3 +board_check = true +board_build.partitions = default_8MB.csv +upload_protocol = esptool +build_flags = + ${esp32s3_base.build_flags} + -D M5STACK_CARDPUTER_ADV + -D ARDUINO_USB_CDC_ON_BOOT=1 + -I variants/esp32s3/m5stack_cardputer_adv +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/m5stack_cardputer_adv> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main + https://github.com/meshtastic/st7789/archive/a787beea5c6c8f864ba6787eb432bbefc575e6ad.zip + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.cpp b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp new file mode 100644 index 000000000..2bbe8e2e3 --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.cpp @@ -0,0 +1,40 @@ +#include "AudioBoard.h" +#include "configuration.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// M5stack Cardputer ADV specific init + +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); + + // extra ES8311 init + auto es8311_write_reg = [](uint8_t reg, uint8_t val) { + Wire.beginTransmission(0x18); // ES8311 i2c address + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); + }; + es8311_write_reg(0x00, 0x80); // reset, power on + es8311_write_reg(0x01, 0xB5); // MCLK = BCLK + es8311_write_reg(0x02, 0x18); // CLOCK_MANAGER/ MULT_PRE=3 + es8311_write_reg(0x0D, 0x01); // analog power up + es8311_write_reg(0x12, 0x00); // DAC power up + es8311_write_reg(0x13, 0x10); // enable HP drive + es8311_write_reg(0x32, 0xBF); // DAC volume (0dB) + es8311_write_reg(0x37, 0x08); // EQ bypass +} diff --git a/variants/esp32s3/m5stack_cardputer_adv/variant.h b/variants/esp32s3/m5stack_cardputer_adv/variant.h new file mode 100644 index 000000000..5fdb1436e --- /dev/null +++ b/variants/esp32s3/m5stack_cardputer_adv/variant.h @@ -0,0 +1,90 @@ +#define USE_ST7789 + +#define ST7789_NSS 37 +#define ST7789_RS 34 // DC +#define ST7789_SDA 35 // MOSI +#define ST7789_SCK 36 +#define ST7789_RESET 33 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +// #define VTFT_CTRL 38 +#define VTFT_LEDA 38 +// #define ST7789_BL (32+6) +#define ST7789_SPI_HOST SPI2_HOST +// #define TFT_BL (32+6) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 135 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define HAS_PHYSICAL_KEYBOARD 1 + +// Backlight is controlled to power rail on this board, this also powers the neopixel +// #define PIN_POWER_EN 38 + +#define BUTTON_PIN 0 + +#define I2C_SDA 8 +#define I2C_SCL 9 + +#define I2C_SDA1 2 +#define I2C_SCL1 1 + +#undef LORA_SCK +#undef LORA_MISO +#undef LORA_MOSI +#undef LORA_CS + +#define LORA_SCK 40 +#define LORA_MISO 39 +#define LORA_MOSI 14 +#define LORA_CS 5 // NSS + +#define USE_SX1262 +#define LORA_DIO0 -1 +#define LORA_RESET 3 +#define LORA_RST 3 +#define LORA_DIO1 4 +#define LORA_DIO2 6 +#define LORA_DIO3 RADIOLIB_NC + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 15 +#define GPS_TX_PIN 13 +#define HAS_GPS 1 +#define GPS_BAUDRATE 115200 + +// audio codec ES8311 +#define HAS_I2S +#define DAC_I2S_BCK 41 +#define DAC_I2S_WS 43 +#define DAC_I2S_DOUT 42 +#define DAC_I2S_DIN 46 +#define DAC_I2S_MCLK 45 // dummy + +// TCA8418 keyboard +#define I2C_NO_RESCAN +#define KB_INT 11 + +#define HAS_NEOPIXEL +#define NEOPIXEL_COUNT 1 +#define NEOPIXEL_DATA 21 +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) + +#define BATTERY_PIN 10 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO10_CHANNEL +#define ADC_MULTIPLIER 2 * 1.02 // 100k + 100k, and add 2% to kick the voltage over the max voltage to show charging. + +// BMI270 6-axis IMU on internal I2C bus +#define HAS_BMI270 diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index ecf5498b3..a153ba9fb 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -12,6 +12,7 @@ build_flags = ${esp32s3_base.build_flags} -D CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=1 + -D MESHTASTIC_EXCLUDE_SCREEN=1 -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 @@ -31,7 +32,10 @@ build_flags = ${esp32s3_base.build_flags} -D HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER - -D RAM_SIZE=1024 + -D MAP_TILES_GREY ; required for 2MB PSRAM + -D RAM_SIZE=1432 + -D STBI_ARENA_SIZE=450000 + -D LV_CACHE_DEF_SIZE=0 -D LGFX_DRIVER_TEMPLATE -D LGFX_DRIVER=LGFX_GENERIC -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" diff --git a/variants/esp32s3/mini-epaper-s3/nicheGraphics.h b/variants/esp32s3/mini-epaper-s3/nicheGraphics.h new file mode 100644 index 000000000..0cbd6a192 --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/nicheGraphics.h @@ -0,0 +1,131 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applet.h" +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" +#include "graphics/niche/InkHUD/SystemApplet.h" + +// Shared NicheGraphics components +#include "graphics/niche/Drivers/EInk/GDEW0102T4.h" +#include "graphics/niche/Inputs/TwoButtonExtended.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // Power-enable the E-Ink panel on this board before any SPI traffic. + pinMode(PIN_EINK_EN, OUTPUT); + digitalWrite(PIN_EINK_EN, HIGH); + delay(10); + + // Display uses HSPI on this board + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + Drivers::GDEW0102T4 *displayDriver = new Drivers::GDEW0102T4; + displayDriver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + // Tuned fast-refresh values reg30 reg50 reg82 lutW2 lutB2 = 11 F2 04 11 0D + displayDriver->setFastConfig({0x11, 0xF2, 0x04, 0x11, 0x0D}); + Drivers::EInk *driver = displayDriver; + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + inkhud->setDriver(driver); + // Slightly stricter FAST/FULL + inkhud->setDisplayResilience(5, 1.5); + inkhud->twoWayRocker = true; + + // Fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontMedium = FREESANS_6PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Small display defaults + inkhud->persistence->settings.rotation = 0; + inkhud->persistence->settings.userTiles.maxCount = 1; + inkhud->persistence->settings.userTiles.count = 1; + inkhud->persistence->settings.joystick.enabled = true; + inkhud->persistence->settings.joystick.aligned = true; + inkhud->persistence->settings.optionalMenuItems.nextTile = false; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, false, false); // - + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false); // Activated, not autoshown + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, true); // Activated, Autoshown + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1), false, false); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, false, false); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // Start running InkHUD + inkhud->begin(); + + // Enforce two-way rocker behavior regardless of persisted settings. + inkhud->persistence->settings.joystick.enabled = true; + inkhud->persistence->settings.joystick.aligned = true; + inkhud->persistence->settings.optionalMenuItems.nextTile = false; + + // Inputs + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); + + // Center press (boot button) + buttons->setWiring(0, INPUTDRIVER_TWO_WAY_ROCKER_BTN, true); + // Match baseUI encoder long-press feel. + buttons->setTiming(0, 75, 300); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + // LEFT rocker pin is IO4; RIGHT rocker pin is IO3. + buttons->setTwoWayRockerWiring(INPUTDRIVER_TWO_WAY_ROCKER_LEFT, INPUTDRIVER_TWO_WAY_ROCKER_RIGHT, true); + buttons->setJoystickDebounce(50); + + // Two-way rocker behavior: + // - when a system applet is handling input (menu, tips, etc): LEFT=up, RIGHT=down + // - otherwise: LEFT=previous applet, RIGHT=next applet + buttons->setTwoWayRockerPressHandlers( + [inkhud]() { + bool systemHandlingInput = false; + for (const InkHUD::SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + systemHandlingInput = true; + break; + } + } + + if (systemHandlingInput) + inkhud->navUp(); + else + inkhud->prevApplet(); + }, + [inkhud]() { + bool systemHandlingInput = false; + for (const InkHUD::SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + systemHandlingInput = true; + break; + } + } + + if (systemHandlingInput) + inkhud->navDown(); + else + inkhud->nextApplet(); + }); + + buttons->start(); +} + +#endif diff --git a/variants/esp32s3/mini-epaper-s3/pins_arduino.h b/variants/esp32s3/mini-epaper-s3/pins_arduino.h index a4b3c4bf7..afb2428a0 100644 --- a/variants/esp32s3/mini-epaper-s3/pins_arduino.h +++ b/variants/esp32s3/mini-epaper-s3/pins_arduino.h @@ -3,24 +3,23 @@ #include -#define USB_VID 0x303a +#define USB_VID 0x303A #define USB_PID 0x1001 // The default Wire will be mapped to PMU and RTC static const uint8_t SDA = 18; static const uint8_t SCL = 9; -// Default SPI will be mapped to Radio +// Default SPI (LoRa bus) static const uint8_t SS = -1; static const uint8_t MOSI = 17; static const uint8_t MISO = 6; static const uint8_t SCK = 8; +// SD card SPI bus #define SPI_MOSI (39) #define SPI_SCK (41) #define SPI_MISO (38) #define SPI_CS (40) -#define SDCARD_CS SPI_CS - #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/mini-epaper-s3/platformio.ini b/variants/esp32s3/mini-epaper-s3/platformio.ini index f49be707f..3e4802319 100644 --- a/variants/esp32s3/mini-epaper-s3/platformio.ini +++ b/variants/esp32s3/mini-epaper-s3/platformio.ini @@ -1,5 +1,5 @@ [env:mini-epaper-s3] -;custom_meshtastic_hw_model = +custom_meshtastic_hw_model = 125 custom_meshtastic_hw_model_slug = MINI_EPAPER_S3 custom_meshtastic_architecture = esp32-s3 custom_meshtastic_actively_supported = true @@ -8,6 +8,7 @@ custom_meshtastic_display_name = LILYGO Mini ePaper S3 E-Ink custom_meshtastic_images = mini-epaper-s3.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = no +custom_meshtastic_has_mui = false extends = esp32s3_base board = mini-epaper-s3 @@ -17,11 +18,15 @@ upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/mini-epaper-s3 - -DMINI_EPAPER_S3 - -DUSE_EINK - -DEINK_DISPLAY_MODEL=GxEPD2_102 - -DEINK_WIDTH=128 - -DEINK_HEIGHT=80 + -D MINI_EPAPER_S3 + -D USE_EINK + -D EINK_DISPLAY_MODEL=GxEPD2_102 + -D EINK_WIDTH=128 + -D EINK_HEIGHT=80 + -D USE_EINK_DYNAMICDISPLAY + -D EINK_LIMIT_FASTREFRESH=3 + -D EINK_BACKGROUND_USES_FAST + -D EINK_HASQUIRK_GHOSTING lib_deps = ${esp32s3_base.lib_deps} @@ -29,3 +34,22 @@ lib_deps = https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 + +[env:mini-epaper-s3-inkhud] +extends = esp32s3_base, inkhud +board = mini-epaper-s3 +board_check = true +upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + ${inkhud.build_src_filter} +build_flags = + ${esp32s3_base.build_flags} + ${inkhud.build_flags} + -I variants/esp32s3/mini-epaper-s3 + -D MINI_EPAPER_S3 +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32s3/mini-epaper-s3/variant.h b/variants/esp32s3/mini-epaper-s3/variant.h index b464c9b4a..0b640f9cf 100644 --- a/variants/esp32s3/mini-epaper-s3/variant.h +++ b/variants/esp32s3/mini-epaper-s3/variant.h @@ -1,46 +1,46 @@ -// Display (E-Ink) +#pragma once -#define PIN_EINK_CS 13 -#define PIN_EINK_BUSY 10 -#define PIN_EINK_RES 11 -#define PIN_EINK_SCLK 14 -#define PIN_EINK_MOSI 15 -#define PIN_EINK_DC 12 -#define PIN_EINK_EN 42 +#define GPS_DEFAULT_NOT_PRESENT 1 -#define SPI_INTERFACES_COUNT 2 -#define PIN_SPI1_MISO -1 -#define PIN_SPI1_MOSI PIN_EINK_MOSI -#define PIN_SPI1_SCK PIN_EINK_SCLK -#define DISPLAY_FORCE_SMALL_FONTS +// SD card (TF) +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS 40 +#define SD_SPI_FREQUENCY 25000000U +// Built-in RTC (I2C) +#define PCF8563_RTC 0x51 +#define HAS_RTC 1 #define I2C_SDA SDA #define I2C_SCL SCL +// Battery voltage monitoring #define BATTERY_PIN 2 // A battery voltage measurement pin, voltage divider connected here to // measure battery voltage ratio of voltage divider = 2.0 (assumption) #define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. #define ADC_CHANNEL ADC1_GPIO2_CHANNEL -#define HAS_GPS 0 -#undef GPS_RX_PIN -#undef GPS_TX_PIN +// Display (E-Ink) +#define PIN_EINK_EN 42 +#define PIN_EINK_CS 13 +#define PIN_EINK_BUSY 10 +#define PIN_EINK_DC 12 +#define PIN_EINK_RES 11 +#define PIN_EINK_SCLK 14 +#define PIN_EINK_MOSI 15 +#define DISPLAY_FORCE_SMALL_FONTS -#define BUTTON_PIN 3 -#define BUTTON_NEED_PULLUP -#define ALT_BUTTON_PIN 4 -#define ALT_BUTTON_ACTIVE_LOW true -#define ALT_BUTTON_ACTIVE_PULLUP true -#define PIN_BUTTON3 0 - -// #define HAS_SDCARD 1 -// #define SDCARD_USE_SOFT_SPI - -// PCF85063 RTC Module -#define PCF85063_RTC 0x51 -#define HAS_RTC 1 +// Two-Way Rocker input (left/right + boot as press) +#define INPUTDRIVER_TWO_WAY_ROCKER +#define INPUTDRIVER_ENCODER_TYPE 2 +#define INPUTDRIVER_TWO_WAY_ROCKER_RIGHT 3 +#define INPUTDRIVER_TWO_WAY_ROCKER_LEFT 4 +#define INPUTDRIVER_TWO_WAY_ROCKER_BTN 0 +#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 +// LoRa (SX1262) #define USE_SX1262 + #define LORA_DIO1 5 #define LORA_SCK 8 #define LORA_MISO 6 diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index 82147f222..6f218a126 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -6,6 +6,7 @@ custom_meshtastic_actively_supported = true custom_meshtastic_support_level = 3 custom_meshtastic_display_name = Pi Computer S3 custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = bpi_picow_esp32_s3 diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 113c2f527..87e5b63ff 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = rak_3312.svg custom_meshtastic_tags = RAK custom_meshtastic_requires_dfu = false custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false extends = esp32s3_base board = wiscore_rak3312 diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index 8423bb4df..7847410ae 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -1,6 +1,27 @@ ; rak_wismeshtap2 rak3112 +[ft5x06] +build_flags = + -D LGFX_TOUCH=FT5x06 + -D LGFX_TOUCH_I2C_FREQ=100000 + -D LGFX_TOUCH_I2C_PORT=0 + -D LGFX_TOUCH_I2C_ADDR=0x38 + -D LGFX_TOUCH_I2C_SDA=9 + -D LGFX_TOUCH_I2C_SCL=40 + -D LGFX_TOUCH_RST=-1 + -D LGFX_TOUCH_INT=39 + +[env:rak_wismesh_tap_v2] +custom_meshtastic_hw_model = 116 +custom_meshtastic_hw_model_slug = WISMESH_TAP_V2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Tap V2 +custom_meshtastic_images = rak-wismesh-tap-v2.svg +custom_meshtastic_tags = RAK +custom_meshtastic_partition_scheme = 8MB +custom_meshtastic_has_mui = true -[rak_wismeshtap_s3] extends = esp32s3_base board = wiscore_rak3312 board_check = true @@ -18,23 +39,12 @@ lib_deps = # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 -[ft5x06] -extends = mesh_tab_base -build_flags = - -D LGFX_TOUCH=FT5x06 - -D LGFX_TOUCH_I2C_FREQ=100000 - -D LGFX_TOUCH_I2C_PORT=0 - -D LGFX_TOUCH_I2C_ADDR=0x38 - -D LGFX_TOUCH_I2C_SDA=9 - -D LGFX_TOUCH_I2C_SCL=40 - -D LGFX_TOUCH_RST=-1 - -D LGFX_TOUCH_INT=39 - [env:rak_wismesh_tap_v2-tft] -extends = rak_wismeshtap_s3 +extends = env:rak_wismesh_tap_v2 build_flags = - ${rak_wismeshtap_s3.build_flags} + ${env:rak_wismesh_tap_v2.build_flags} + -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D CONFIG_ARDUHAL_ESP_LOG -D CONFIG_ARDUHAL_LOG_COLORS=1 -D CONFIG_DISABLE_HAL_LOCKS=1 @@ -69,9 +79,9 @@ build_flags = -D VIEW_320x240 -D USE_PACKET_API ${ft5x06.build_flags} - -D LGFX_SCREEN_WIDTH=240 - -D LGFX_SCREEN_HEIGHT=320 - -D DISPLAY_SIZE=320x240 ; landscape mode + -D LGFX_SCREEN_WIDTH=240 ; native panel width (portrait) + -D LGFX_SCREEN_HEIGHT=320 ; native panel height (portrait) + -D DISPLAY_SIZE=320x240 ; UI runs in landscape mode -D LGFX_PANEL=ST7789 -D LGFX_ROTATION=1 -D LGFX_TOUCH_X_MIN=0 @@ -83,7 +93,7 @@ build_flags = -D MAP_FULL_REDRAW=1 lib_deps = - ${rak_wismeshtap_s3.lib_deps} + ${env:rak_wismesh_tap_v2.lib_deps} ${device-ui_base.lib_deps} diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 70a10e0d4..bb52f801b 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -9,7 +9,7 @@ custom_meshtastic_display_name = Seeed SenseCAP Indicator custom_meshtastic_images = seeed-sensecap-indicator.svg custom_meshtastic_tags = Seeed custom_meshtastic_partition_scheme = 8MB - = true +custom_meshtastic_has_mui = true extends = esp32s3_base platform_packages = @@ -37,8 +37,8 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} ; TODO switch back to official LovyanGFX https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index 11bf48521..cbdbf8eb8 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -50,7 +50,6 @@ L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-S #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 #define HAS_GPS 1 -#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX PIN_GPS_TX #define PIN_SERIAL1_TX PIN_GPS_RX diff --git a/variants/esp32s3/station-g2/pins_arduino.h b/variants/esp32s3/station-g2/pins_arduino.h old mode 100755 new mode 100644 diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini old mode 100755 new mode 100644 diff --git a/variants/esp32s3/station-g2/variant.h b/variants/esp32s3/station-g2/variant.h old mode 100755 new mode 100644 index 8f0b4b220..2d65a042c --- a/variants/esp32s3/station-g2/variant.h +++ b/variants/esp32s3/station-g2/variant.h @@ -40,6 +40,14 @@ Board Information: https://wiki.uniteng.com/en/meshtastic/station-g2 #define SX126X_MAX_POWER 19 #endif +// Enable Traffic Management Module for Station G2 +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif + /* #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO4_CHANNEL diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 9abf895db..b14f2fe3c 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Beam 1W custom_meshtastic_images = tbeam-1w.svg custom_meshtastic_tags = LilyGo +custom_meshtastic_has_mui = false extends = esp32s3_base board = t-beam-1w diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 29960f280..93ef8babf 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -9,6 +9,7 @@ custom_meshtastic_images = tdeck_pro.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = false extends = esp32s3_base board = t-deck-pro @@ -33,7 +34,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index c216ec595..1b3599464 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = t-deck.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = t-deck @@ -29,8 +30,8 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 8c79ca097..352396818 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -27,7 +27,7 @@ lib_deps = ${esp32s3_base.lib_deps} lewisxhe/SensorLib@0.3.4 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t5s3_epaper/nicheGraphics.h b/variants/esp32s3/t5s3_epaper/nicheGraphics.h new file mode 100644 index 000000000..699a82de0 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/nicheGraphics.h @@ -0,0 +1,123 @@ +/* + +Most of the Meshtastic firmware uses preprocessor macros throughout the code to support different hardware variants. +NicheGraphics attempts a different approach: + +Per-device config takes place in this setupNicheGraphics() method +(And a small amount in platformio.ini) + +This file sets up InkHUD for Heltec VM-E290. +Different NicheGraphics UIs and different hardware variants will each have their own setup procedure. + +*/ + +#pragma once + +#include "configuration.h" +#include "mesh/MeshModule.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +// #include "graphics/niche/InkHUD/InkHUD.h" +#include "graphics/niche/InkHUD/WindowManager.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" +#include "graphics/niche/Inputs/TwoButton.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // Display is connected to HSPI + SPIClass *hspi = new SPIClass(HSPI); + hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); + + // E-Ink Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::DEPG0290BNS800; + driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle? + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + + // Setup backlight + // Note: AUX button behavior configured further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + // Optional arguments for defaults: + // - is activated? + // - is autoshown? + // - is foreground on a specific tile (index)? + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + // inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet); + // inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet); + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // A shared NicheGraphics component + + // Setup the main user button (0) + buttons->setWiring(0, BUTTON_PIN); + buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); }); + buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); }); + + // Setup the aux button (1) + // Bonus feature of VME290 + buttons->setWiring(1, BUTTON_PIN_SECONDARY); + buttons->setHandlerShortPress(1, []() { InkHUD::InkHUD::getInstance()->nextTile(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/pins_arduino.h b/variants/esp32s3/t5s3_epaper/pins_arduino.h new file mode 100644 index 000000000..4978cff2a --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/pins_arduino.h @@ -0,0 +1,43 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#if defined(T5_S3_EPAPER_PRO_V1) +// The default Wire will be mapped to RTC, Touch, BQ25896, and BQ27220 +static const uint8_t SDA = 6; +static const uint8_t SCL = 5; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 8; +static const uint8_t SCK = 18; + +#define SPI_MOSI (17) +#define SPI_SCK (18) +#define SPI_MISO (8) +#define SPI_CS (16) + +#else // T5_S3_EPAPER_PRO_V2 +// The default Wire will be mapped to RTC, Touch, PCA9535, BQ25896, and BQ27220 +static const uint8_t SDA = 39; +static const uint8_t SCL = 40; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 46; +static const uint8_t MOSI = 13; +static const uint8_t MISO = 21; +static const uint8_t SCK = 14; + +#define SPI_MOSI (13) +#define SPI_SCK (14) +#define SPI_MISO (21) +#define SPI_CS (12) + +#endif + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini new file mode 100644 index 000000000..bad36706c --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -0,0 +1,61 @@ +[t5s3_epaper_base] +extends = esp32s3_base +board = t5-epaper-s3 +board_build.partition = default_16MB.csv +board_check = true +upload_protocol = esptool +build_flags = -fno-strict-aliasing + ${esp32_base.build_flags} + -I variants/esp32s3/t5s3_epaper + -D T5_S3_EPAPER_PRO + -D USE_EINK + -D USE_EINK_PARALLELDISPLAY + -D PRIVATE_HW + -D TOUCH_THRESHOLD_X=60 + -D TOUCH_THRESHOLD_Y=40 + -D TIME_LONG_PRESS=500 +; -D EINK_LIMIT_GHOSTING_PX=5000 + -D EPD_FULLSLOW_PERIOD=100 + -D FAST_EPD_PARTIAL_UPDATE_BUG ; use rect area update instead of partial + +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/t5s3_epaper> +lib_deps = + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip + +[env:t5s3_epaper_inkhud] +extends = t5s3_epaper_base, inkhud +board_level = extra # inkhud port is incomplete +board_check = false +build_flags = + ${t5s3_epaper_base.build_flags} + ${inkhud.build_flags} + -D SDCARD_USE_SPI1 + -D T5_S3_EPAPER_PRO_V2 +build_src_filter = + ${t5s3_epaper_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${t5s3_epaper_base.lib_deps} + + +[env:t5s3-epaper-v1] ; H752 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V1 + -D GPS_DEFAULT_NOT_PRESENT=1 + +[env:t5s3-epaper-v2] ; H752-01 +extends = t5s3_epaper_base +build_flags = + ${t5s3_epaper_base.build_flags} + -D T5_S3_EPAPER_PRO_V2 + -D SDCARD_USE_SPI1 + -D GPS_POWER_TOGGLE diff --git a/variants/esp32s3/t5s3_epaper/variant.cpp b/variants/esp32s3/t5s3_epaper/variant.cpp new file mode 100644 index 000000000..e10d7c347 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.cpp @@ -0,0 +1,47 @@ +#include "configuration.h" + +#ifdef T5_S3_EPAPER_PRO + +#include "TouchDrvGT911.hpp" +#include "Wire.h" +#include "input/TouchScreenImpl1.h" + +TouchDrvGT911 touch; + +bool readTouch(int16_t *x, int16_t *y) +{ + if (!digitalRead(GT911_PIN_INT)) { + int16_t raw_x; + int16_t raw_y; + if (touch.getPoint(&raw_x, &raw_y)) { + // rotate 90ยฐ for landscape + *x = raw_y; + *y = EPD_WIDTH - 1 - raw_x; + LOG_DEBUG("touched(%d/%d)", *x, *y); + return true; + } + } + return false; +} + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(BOARD_BL_EN, OUTPUT); +} + +// T5-S3-ePaper Pro specific (late-) init +void lateInitVariant(void) +{ + touch.setPins(GT911_PIN_RST, GT911_PIN_INT); + if (touch.begin(Wire, GT911_SLAVE_ADDRESS_L, GT911_PIN_SDA, GT911_PIN_SCL)) { + touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch); + touchScreenImpl1->init(); + } else { + LOG_ERROR("Failed to find touch controller!"); + } +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/t5s3_epaper/variant.h b/variants/esp32s3/t5s3_epaper/variant.h new file mode 100644 index 000000000..c2c001373 --- /dev/null +++ b/variants/esp32s3/t5s3_epaper/variant.h @@ -0,0 +1,92 @@ + +// Display (E-Ink) ED047TC1 - 8bit parallel +#define EPD_WIDTH 960 +#define EPD_HEIGHT 540 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BOARD_BL_EN 40 +#else +#define BOARD_BL_EN 11 +#endif + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define HAS_TOUCHSCREEN 1 +#define GT911_PIN_SDA SDA +#define GT911_PIN_SCL SCL +#if defined(T5_S3_EPAPER_PRO_V1) +#define GT911_PIN_INT 15 +#define GT911_PIN_RST 41 +#else +#define GT911_PIN_INT 3 +#define GT911_PIN_RST 9 +#endif + +#define PCF85063_RTC 0x51 +#define HAS_RTC 1 +#define PCF85063_INT 2 + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GPS +#if !defined(T5_S3_EPAPER_PRO_V1) +#define GPS_RX_PIN 44 +#define GPS_TX_PIN 43 +#endif + +#if defined(T5_S3_EPAPER_PRO_V1) +#define BUTTON_PIN 48 +#define PIN_BUTTON2 0 +#define ALT_BUTTON_PIN PIN_BUTTON2 +#else +#define BUTTON_PIN 0 +#endif + +// SD card +#define HAS_SDCARD +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// battery charger BQ25896 +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// battery quality management BQ27220 +#define HAS_BQ27220 1 +#define BQ27220_I2C_SDA SDA +#define BQ27220_I2C_SCL SCL +#define BQ27220_DESIGN_CAPACITY 1500 + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK SCK +#define LORA_MISO MISO +#define LORA_MOSI MOSI +#define LORA_CS 46 + +#define LORA_DIO0 -1 +#if defined(T5_S3_EPAPER_PRO_V1) +#define LORA_RESET 43 +#define LORA_DIO1 3 // SX1262 IRQ +#define LORA_DIO2 44 // SX1262 BUSY +#define LORA_DIO3 +#else +#define LORA_RESET 1 +#define LORA_DIO1 10 // SX1262 IRQ +#define LORA_DIO2 47 // SX1262 BUSY +#define LORA_DIO3 +#endif + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 2.4 diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index b5c9fd579..832f9d7d7 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -10,6 +10,7 @@ custom_meshtastic_images = lilygo-tlora-pager.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true extends = esp32s3_base board = t-deck-pro ; same as T-Deck Pro @@ -26,7 +27,6 @@ build_flags = ${esp32s3_base.build_flags} -D T_LORA_PAGER -D BOARD_HAS_PSRAM -D HAS_SDCARD - -D SDCARD_USE_SPI1 -D ENABLE_ROTARY_PULLUP -D ENABLE_BUTTON_PULLUP -D ROTARY_BUXTRONICS @@ -34,8 +34,8 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 - # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio - earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix + https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.1.0 # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index f9d1ea7db..8f764bbe6 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 E-Ink custom_meshtastic_images = tlora-t3s3-epaper.svg custom_meshtastic_tags = LilyGo custom_meshtastic_requires_dfu = true +custom_meshtastic_has_ink_hud = true extends = esp32s3_base board = tlora-t3s3-v1 diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index 1d4af52f3..3c342e2ac 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -40,8 +40,6 @@ lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@1.2.19 # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip - # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.4 [env:unphone-tft] board_level = extra diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 17ec59442..86b1fe60a 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -34,8 +34,8 @@ lib_deps = adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip - # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 - adafruit/Adafruit BME680 Library@^2.0.5 + # renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680 + https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip build_flags = ${arduino_base.build_flags} @@ -55,9 +55,9 @@ build_flags = -li2c -luv -std=gnu17 - -std=c++17 - + -std=gnu++17 lib_ignore = Adafruit NeoPixel Adafruit ST7735 and ST7789 Library + Adafruit SSD1306 SD diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index cba4aaedd..c23d17b8d 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -8,3 +8,11 @@ // RAK12002 RTC Module #define RV3028_RTC (uint8_t)0b1010010 + +// Enable Traffic Management Module for native/portduino +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048 +#endif diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index e202715b0..fd159a6d2 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index 2a6cea73e..c169de8f7 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = ThinkNode M1 custom_meshtastic_images = thinknode_m1.svg custom_meshtastic_tags = Elecrow +custom_meshtastic_has_ink_hud = true extends = nrf52840_base board = ThinkNode-M1 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp index 04f86e2d4..216b62dcb 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.cpp @@ -32,8 +32,8 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(PIN_LED1, OUTPUT); - ledOff(PIN_LED1); + // pinMode(PIN_LED1, OUTPUT); + // ledOff(PIN_LED1); } void variant_shutdown() diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index e00e56785..4ae462758 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -42,15 +42,17 @@ extern "C" { #define NUM_ANALOG_OUTPUTS (0) // LED -#define PIN_LED1 (32 + 6) // red -#define LED_POWER (32 + 4) +// #define PIN_LED1 (32 + 6) +#define LED_POWER (32 + 4) // red #define LED_NOTIFICATION (0 + 13) // green +#define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING + // USB_CHECK #define EXT_PWR_DETECT (32 + 3) #define ADC_V (0 + 8) -#define LED_BLUE PIN_LED1 -#define LED_STATE_ON 0 // State when LED is lit // LED็ฏไบฎๆ—ถ็š„็Šถๆ€ +// #define LED_BLUE PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit // LED็ฏไบฎๆ—ถ็š„็Šถๆ€ #define PIN_BUZZER (0 + 6) /* * Buttons diff --git a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini index 9a2b3a467..f96e6038c 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M4/platformio.ini @@ -12,4 +12,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M4> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index afc8e4b2a..39b5dfbd4 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = nrfutil ;upload_port = /dev/ttyACM1 diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index 3410859b1..ebea1ce97 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -15,6 +15,6 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index 5e5b4b665..c529caa0b 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 debug_tool = jlink diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md index 4ffe625cc..5d3d90c72 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md @@ -53,7 +53,7 @@ Making your own node based on this design is straightforward. There are various The E80 from CDEbyte is the most obtainable module at present, and has been selected as the default option. -Naturally, CDEbyte have chosen to ignore the generic Semtech impelementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. +Naturally, CDEbyte have chosen to ignore the generic Semtech implementation of the RF switching logic and have supplied confusing and contradictory documentation, which is explained below. tl;dr: The E80 is chosen as the default. **If you wish to use another module, the table in `rfswitch.h` must be adjusted accordingly.** diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h index 71508c037..ac7ef57c4 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/rfswitch.h @@ -12,9 +12,15 @@ static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11 RADIOLIB_NC}; static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 DIO7 - {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, - {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, - {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, - {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, END_OF_MODE_TABLE, + // clang-format off + // mode DIO5 DIO6 DIO7 + {LR11x0::MODE_STBY, {LOW, LOW, LOW}}, + {LR11x0::MODE_RX, {LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH, LOW}}, + {LR11x0::MODE_TX_HP, {HIGH, LOW, LOW}}, + {LR11x0::MODE_TX_HF, {LOW, LOW, LOW}}, + {LR11x0::MODE_GNSS, {LOW, LOW, HIGH}}, + {LR11x0::MODE_WIFI, {LOW, LOW, LOW}}, + END_OF_MODE_TABLE, + // clang-format on }; diff --git a/variants/nrf52840/diy/xiao_ble/README.md b/variants/nrf52840/diy/xiao_ble/README.md index fe6dcba2d..efc1236a8 100644 --- a/variants/nrf52840/diy/xiao_ble/README.md +++ b/variants/nrf52840/diy/xiao_ble/README.md @@ -116,7 +116,7 @@ _(none)_ 1. Double press the XIAO nrf52840's `reset` button to put it in bootloader mode, and a USB volume named `XIAO SENSE` will appear 2. Copy the `firmware.uf2` file to the `XIAO SENSE` volume (refer to the last step of [Build Meshtastic](#2-build-meshtastic)) 3. The XIAO nrf52840's red LED will flash for several seconds as the firmware is copied -4. Once Meshtastic firmware succesfully boots, the: +4. Once Meshtastic firmware successfully boots, the: 1. Green LED will turn on 2. Red LED will flash several times to indicate flash memory writes during initial settings file creation 3. Green LED will blink every second once the firmware is running normally @@ -135,7 +135,7 @@ _(none)_ - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. - If the E22 gets hot to the touch: - - The power amplifier is likely running continually. Disconnect it and the XIAO from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). + - The power amplifier is likely running continually. Disconnect it and the XIAO from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertently high (usually due to a pin mapping conflict). ## 5. Notes diff --git a/variants/nrf52840/heltec_mesh_node_t096/platformio.ini b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini new file mode 100644 index 000000000..e1bdd529d --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/platformio.ini @@ -0,0 +1,34 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-node-t096] +custom_meshtastic_hw_model = 127 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T096 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node 096 +custom_meshtastic_images = heltec-mesh-node-t096.svg, heltec-mesh-node-t096-case.svg +custom_meshtastic_tags = Heltec + +extends = nrf52840_base +board = heltec_mesh_node_t096 +board_level = pr +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_node_t096 + -D HAS_LORA_FEM=1 + -D HELTEC_MESH_NODE_T096 + -D USE_TFTDISPLAY=1 + -D USER_SETUP_LOADED + -D ST7735_DRIVER + -D ST7735_REDTAB160x80 + -D TFT_SPI_PORT=SPI1 + -D TFT_CS=ST7735_CS ; Chip select control + -D TFT_DC=ST7735_RS ; Data Command control pin + -D TFT_RST=ST7735_RESET ; Reset pin + -D TFT_BL=ST7735_BL ; LED back-light + -D TFT_BACKLIGHT_ON=LOW +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t096> +lib_deps = + ${nrf52840_base.lib_deps} + bodmer/TFT_eSPI@2.5.43 ; renovate: datasource=platformio-registry depName=bodmer/TFT_eSPI diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.cpp b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp new file mode 100644 index 000000000..29158e8ba --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.cpp @@ -0,0 +1,76 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} + +void variant_shutdown() +{ + nrf_gpio_cfg_default(VEXT_ENABLE); + nrf_gpio_cfg_default(ST7735_CS); + nrf_gpio_cfg_default(ST7735_RS); + nrf_gpio_cfg_default(ST7735_SDA); + nrf_gpio_cfg_default(ST7735_SCK); + nrf_gpio_cfg_default(ST7735_RESET); + nrf_gpio_cfg_default(ST7735_BL); + + nrf_gpio_cfg_default(PIN_LED1); + + // nrf_gpio_cfg_default(LORA_PA_POWER); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, LOW); + + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CSD); + nrf_gpio_cfg_default(LORA_KCT8103L_PA_CTX); + + pinMode(ADC_CTRL, OUTPUT); + digitalWrite(ADC_CTRL, LOW); + + nrf_gpio_cfg_default(SX126X_CS); + nrf_gpio_cfg_default(SX126X_DIO1); + nrf_gpio_cfg_default(SX126X_BUSY); + nrf_gpio_cfg_default(SX126X_RESET); + + nrf_gpio_cfg_default(PIN_SPI_MISO); + nrf_gpio_cfg_default(PIN_SPI_MOSI); + nrf_gpio_cfg_default(PIN_SPI_SCK); + + nrf_gpio_cfg_default(PIN_GPS_PPS); + nrf_gpio_cfg_default(PIN_GPS_RESET); + nrf_gpio_cfg_default(PIN_GPS_EN); + nrf_gpio_cfg_default(GPS_TX_PIN); + nrf_gpio_cfg_default(GPS_RX_PIN); +} \ No newline at end of file diff --git a/variants/nrf52840/heltec_mesh_node_t096/variant.h b/variants/nrf52840/heltec_mesh_node_t096/variant.h new file mode 100644 index 000000000..04e22af26 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_node_t096/variant.h @@ -0,0 +1,202 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define VEXT_ENABLE (0 + 26) +#define VEXT_ON_VALUE HIGH + +// ST7735S TFT LCD +#define ST7735_CS (0 + 22) +#define ST7735_RS (0 + 15) // DC +#define ST7735_SDA (0 + 17) // MOSI +#define ST7735_SCK (0 + 20) +#define ST7735_RESET (0 + 13) +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define ST7735_BL (32 + 12) +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT 160 +#define TFT_WIDTH 80 +#define TFT_OFFSET_X 24 +#define TFT_OFFSET_Y 0 +#define TFT_INVERT false +#define SCREEN_TRANSITION_FRAMERATE 3 // fps +#define DISPLAY_FORCE_SMALL_FONTS + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (0 + 28) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_STATE_ON 1 // State when LED is lit + +// #define HAS_NEOPIXEL // Enable the use of neopixels +// #define NEOPIXEL_COUNT 2 // How many neopixels are connected +// #define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels +// #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +#define PIN_WIRE_SDA (0 + 7) // SDA +#define PIN_WIRE_SCL (0 + 8) // SCL + +// I2C bus 1 +#define PIN_WIRE1_SDA (0 + 4) // SDA (secondary bus) +#define PIN_WIRE1_SCL (0 + 27) // SCL (secondary bus) + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (0 + 5) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 5) +#define SX126X_DIO1 (0 + 21) +#define SX126X_BUSY (0 + 19) +#define SX126X_RESET (0 + 16) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The heltec_wireless_tracker_v2 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO12: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO41: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO30 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define USE_KCT8103L_PA +#define LORA_PA_POWER (0 + 30) // VFEM_Ctrl - KCT8103L LDO power enable +#define LORA_KCT8103L_PA_CSD (0 + 12) // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX \ + (32 + 9) // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 14) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (32 + 8) + +#define PIN_SPI1_MISO \ + ST7735_MISO // FIXME not really needed, but for now the SPI code requires something to be defined, pick an used GPIO +#define PIN_SPI1_MOSI ST7735_SDA +#define PIN_SPI1_SCK ST7735_SCK + +/* + * GPS pins + */ +#define GPS_UC6580 +#define GPS_BAUDRATE 115200 +#define PIN_GPS_RESET (32 + 14) // An output to reset UC6580 GPS. As per datasheet, low for > 100ms will reset the UC6580 +#define GPS_RESET_MODE LOW +#define PIN_GPS_EN (0 + 6) +#define GPS_EN_ACTIVE LOW +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PIN_GPS_PPS (32 + 11) +#define GPS_TX_PIN (0 + 25) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (0 + 23) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +#define ADC_CTRL (32 + 15) +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN (0 + 3) +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.916F) + +// rf52840 AIN1 = Pin 3 +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_1 + +// We have AIN1 with a VBAT divider so AIN1 = VBAT * (100/490) +// We have the device going deep sleep under 3.1V, which is AIN1 = 0.63V +// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V +// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means +// VBAT=4.04V +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8 + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index 7cbc5f6a9..ef8bd4a17 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip + https://github.com/meshtastic/st7789/archive/a787beea5c6c8f864ba6787eb432bbefc575e6ad.zip diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 9fbcc890d..c9b599382 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -15,6 +15,7 @@ custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 5000mAh custom_meshtastic_key = heltec_mesh_pocket +custom_meshtastic_has_ink_hud = true # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -78,6 +79,7 @@ custom_meshtastic_display_name = Heltec Mesh Pocket custom_meshtastic_actively_supported = true custom_meshtastic_variant = 10000mAh custom_meshtastic_key = heltec_mesh_pocket +custom_meshtastic_has_ink_hud = true # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index b4964a077..1b15c0758 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -16,7 +16,7 @@ lib_deps = # renovate: datasource=git-refs depName=NMIoT-meshsolar packageName=https://github.com/NMIoT/meshsolar gitBranch=main https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.5 + bblanchon/ArduinoJson@6.21.6 [env:heltec-mesh-solar] custom_meshtastic_hw_model = 108 @@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags} lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip + https://github.com/meshtastic/st7789/archive/a787beea5c6c8f864ba6787eb432bbefc575e6ad.zip diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index 727d2c741..f42c29308 100644 --- a/variants/nrf52840/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -2,12 +2,12 @@ ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files platform = # renovate: datasource=custom.pio depName=platformio/nordicnrf52 packageName=platformio/platform/nordicnrf52 - platformio/nordicnrf52@10.10.0 + platformio/nordicnrf52@10.11.0 extends = arduino_base platform_packages = - ; our custom Git version until they merge our PR + ; our custom Git version with C++17 support in platform.txt # TODO renovate - platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#74096746e5f167a2ff22e483d8e79bb1aef00591 + platformio/framework-arduinoadafruitnrf52 @ https://github.com/meshtastic/Adafruit_nRF52_Arduino#cpp17-platform ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 @@ -25,6 +25,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -Os + -std=gnu++17 build_unflags = -Ofast -Og @@ -35,6 +36,8 @@ build_unflags = -g -g1 -g0 + -std=c++11 + -std=gnu++11 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index ba48cf2a2..caa6ea328 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -15,7 +15,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index 714737c9d..84a582fd9 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -17,7 +17,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.7 + zinggjm/GxEPD2@1.6.8 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index e06a271aa..0fded96f4 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -14,7 +14,6 @@ build_flags = ${nrf52840_base.build_flags} -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 ; -DMESHTASTIC_EXCLUDE_PKI=1 - -DMESHTASTIC_EXCLUDE_POWER_FSM=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 ; -DMESHTASTIC_EXCLUDE_TZ=1 -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 @@ -36,7 +35,7 @@ lib_deps = # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson - bblanchon/ArduinoJson@6.21.5 + bblanchon/ArduinoJson@6.21.6 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds ;upload_protocol = jlink diff --git a/variants/nrf52840/rak_wismeshtag/variant.cpp b/variants/nrf52840/rak_wismeshtag/variant.cpp index a035fbaf0..a0394b2dd 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.cpp +++ b/variants/nrf52840/rak_wismeshtag/variant.cpp @@ -19,7 +19,11 @@ */ #include "variant.h" +#include "Arduino.h" +#include "FreeRTOS.h" #include "nrf.h" +#include "power/PowerHAL.h" +#include "sleep.h" #include "wiring_constants.h" #include "wiring_digital.h" @@ -40,3 +44,39 @@ void initVariant() pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } + +#ifdef LOW_VDD_SYSTEMOFF_DELAY_MS +void variant_nrf52LoopHook(void) +{ + // If VDD stays unsafe for a while (brownout), force System OFF. + // Skip when VBUS present to allow recovery while USB-powered. + if (!powerHAL_isVBUSConnected()) { + // Rate-limit VDD safety checks: powerHAL_isPowerLevelSafe() calls getVDDVoltage() each time. + static constexpr uint32_t POWER_LEVEL_CHECK_INTERVAL_MS = 100; + static uint32_t last_vdd_check_ms = 0; + static bool last_power_level_safe = true; + + const uint32_t now = millis(); + if (last_vdd_check_ms == 0 || (uint32_t)(now - last_vdd_check_ms) >= POWER_LEVEL_CHECK_INTERVAL_MS) { + last_vdd_check_ms = now; + last_power_level_safe = powerHAL_isPowerLevelSafe(); + } + + // Do not use millis()==0 as a sentinel: at boot, millis() may be 0 while VDD is unsafe. + static bool low_vdd_timer_armed = false; + static uint32_t low_vdd_since_ms = 0; + + if (!last_power_level_safe) { + if (!low_vdd_timer_armed) { + low_vdd_since_ms = now; + low_vdd_timer_armed = true; + } + if ((uint32_t)(now - low_vdd_since_ms) >= (uint32_t)LOW_VDD_SYSTEMOFF_DELAY_MS) { + cpuDeepSleep(portMAX_DELAY); + } + } else { + low_vdd_timer_armed = false; + } + } +} +#endif diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index 5b20e4d93..9ea215e42 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -225,7 +225,41 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER 1.73 -#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 +#define OCV_ARRAY 4160, 4020, 3940, 3870, 3810, 3760, 3740, 3720, 3680, 3620, 2990 // updated OCV array for rak_wismeshtag + +// Wake from System OFF when battery rises again (LPCOMP). +// BAT_ADC divider: R22=1M (top), R24=1.5M (bottom) => V_BAT_ADC = VBAT * (1.5 / (1.0 + 1.5)) = 0.6 * VBAT +// RAK4630 module: AIN0 = nrf52840 AIN3 = Pin 5 (A0/BATTERY_PIN) +#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3 +// LPCOMP compares the selected input to a fraction of VDD (here 5/8 of VDD at the LPCOMP input). +// With VDD โ‰ˆ 3.3 V: threshold at input โ‰ˆ (5/8) * 3.3 V โ‰ˆ 2.06 V. +// BAT_ADC divider: V_BAT_ADC = 0.6 * VBAT โ†’ equivalent VBAT โ‰ˆ 2.06 / 0.6 โ‰ˆ 3.4 V (wake when battery recovers). +// +// Note: if VDD is drooping/tracking VBAT in the low-voltage region, using a fraction >= divider ratio helps ensure the +// input is below the threshold at shutdown; the intended wake event happens when the supply recovers enough for a rising +// crossing to occur. +#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_5_8 + +// Low voltage protection: +// If VDD is below SAFE_VDD_VOLTAGE_THRESHOLD for longer than this delay (and no USB VBUS), +// the device will enter System OFF to avoid brownout loops and flash corruption. +#ifndef LOW_VDD_SYSTEMOFF_DELAY_MS +#define LOW_VDD_SYSTEMOFF_DELAY_MS 5000 +#endif + +// Prefer integer mV so platform code avoids floatโ†’int truncation quirks (e.g. 0.1 V โ†’ 99 vs 100 mV). +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_MV 2900 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV 100 +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD +#define SAFE_VDD_VOLTAGE_THRESHOLD (SAFE_VDD_VOLTAGE_THRESHOLD_MV / 1000.0f) +#endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HYST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HYST (SAFE_VDD_VOLTAGE_THRESHOLD_HYST_MV / 1000.0f) +#endif #define RAK_4631 1 diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index d8fbaf8ff..26f4de565 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -7,6 +7,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = Seeed Wio Tracker L1 E-Ink custom_meshtastic_images = wio_tracker_l1_eink.svg custom_meshtastic_tags = Seeed +custom_meshtastic_has_ink_hud = true board = seeed_wio_tracker_L1 extends = nrf52840_base diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 3f9a4f7af..7018d054e 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_DEFAULT + -DCONFIG_NFCT_PINS_AS_GPIOS=1 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink @@ -31,4 +32,4 @@ board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant - -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define \ No newline at end of file + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define diff --git a/variants/nrf52840/t-echo-plus/platformio.ini b/variants/nrf52840/t-echo-plus/platformio.ini index b77d54748..313eceadc 100644 --- a/variants/nrf52840/t-echo-plus/platformio.ini +++ b/variants/nrf52840/t-echo-plus/platformio.ini @@ -1,4 +1,15 @@ [env:t-echo-plus] +custom_meshtastic_hw_model = 33 +custom_meshtastic_hw_model_slug = T_ECHO_PLUS +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo Plus +custom_meshtastic_images = t-echo_plus.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_has_ink_hud = true + extends = nrf52840_base board = t-echo board_level = pr @@ -22,5 +33,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo- lib_deps = ${nrf52840_base.lib_deps} https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=PCF8563 packageName=lewisxhe/library/PCF8563_Library + lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library adafruit/Adafruit DRV2605 Library@1.2.4 diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 89ce488ad..58ad029ae 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -8,6 +8,7 @@ custom_meshtastic_support_level = 1 custom_meshtastic_display_name = LILYGO T-Echo custom_meshtastic_images = t-echo.svg custom_meshtastic_tags = LilyGo +custom_meshtastic_has_ink_hud = true extends = nrf52840_base board = t-echo diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 1d0423b9b..0f7c5adaa 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -38,6 +38,9 @@ void initVariant() pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, HIGH); + pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); + digitalWrite(T1000X_SENSOR_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); digitalWrite(BUZZER_EN_PIN, HIGH); diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index e258f63ae..de6916ac7 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -126,7 +126,7 @@ extern "C" { #define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) -// P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE +// P0.04 is sensor power enable, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE #define EXT_CHRG_DETECT (32 + 3) // P1.03 #define EXT_CHRG_DETECT_VALUE LOW @@ -148,12 +148,21 @@ extern "C" { #define PIN_BUZZER (0 + 25) // P0.25, pwm output #define T1000X_SENSOR_EN -#define T1000X_VCC_PIN (0 + 4) // P0.4 -#define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 -#define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 +#define T1000X_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) +#define T1000X_NTC_PIN (0 + 31) // P0.31/AIN7 +#define T1000X_LUX_PIN (0 + 29) // P0.29/AIN5 #define HAS_SCREEN 0 +// Enable Traffic Management Module for testing on T1000-E +// NRF52840 has 256KB RAM - 1024 entries uses ~10KB +#ifndef HAS_TRAFFIC_MANAGEMENT +#define HAS_TRAFFIC_MANAGEMENT 1 +#endif +#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE +#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1024 +#endif + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/wio-sdk-wm1110/platformio.ini b/variants/nrf52840/wio-sdk-wm1110/platformio.ini index 7c11ef6f6..9fac82289 100644 --- a/variants/nrf52840/wio-sdk-wm1110/platformio.ini +++ b/variants/nrf52840/wio-sdk-wm1110/platformio.ini @@ -9,6 +9,7 @@ extra_scripts = # Remove adafruit USB serial from the build (it is incompatible with using the ch340 serial chip on this board) build_unflags = + ${nrf52840_base.build_unflags} -Ofast -Og -ggdb3 diff --git a/variants/nrf52840/wio-t1000-s/variant.cpp b/variants/nrf52840/wio-t1000-s/variant.cpp index 3c54f6ce5..54d338a19 100644 --- a/variants/nrf52840/wio-t1000-s/variant.cpp +++ b/variants/nrf52840/wio-t1000-s/variant.cpp @@ -38,6 +38,9 @@ void initVariant() pinMode(PIN_3V3_ACC_EN, OUTPUT); digitalWrite(PIN_3V3_ACC_EN, LOW); + pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); + digitalWrite(T1000X_SENSOR_EN_PIN, HIGH); + pinMode(BUZZER_EN_PIN, OUTPUT); digitalWrite(BUZZER_EN_PIN, HIGH); diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index a55c42775..c9b98ab87 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -143,9 +143,9 @@ extern "C" { #define PIN_BUZZER (0 + 25) // P0.25, pwm output #define T1000X_SENSOR_EN -#define T1000X_VCC_PIN (0 + 4) // P0.4 -#define T1000X_NTC_PIN (0 + 31) // P0.31 -#define T1000X_LUX_PIN (0 + 29) // P0.29 +#define T1000X_SENSOR_EN_PIN (0 + 4) // P0.4, Power to Sensor (GPIO, not ADC) +#define T1000X_NTC_PIN (0 + 31) // P0.31 +#define T1000X_LUX_PIN (0 + 29) // P0.29 #ifdef __cplusplus } diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index bb0a4d3ce..d2c155398 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -2,7 +2,7 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.4.0 + platformio/ststm32@19.5.0 platform_packages = # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip @@ -27,7 +27,7 @@ build_flags = -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small. -DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported. - -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. + ;-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; Enable this if enabling debugg logging. It is REQUIRED for at least traceroute debug prints - without it the length returned by printf ends up uninitialized. -DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. -fmerge-all-constants -ffunction-sections diff --git a/version.properties b/version.properties index 1bbb87be7..8621dd9c9 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 20 +build = 22