diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 10a2db0bd..86ab775f9 100644 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -51,7 +51,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do fuzzer=$(basename "$f" .cpp) cp -f "$f" src/fuzzer.cpp pio run -vvv --environment "$PIO_ENV" - program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program" + program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/meshtasticd" cp "$program" "$OUT/$fuzzer" # Copy shared libraries used by the fuzzer. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index f7bf95f83..f79e4fdb5 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -2,4 +2,5 @@ self-hosted-runner: # Labels of self-hosted runner in array of strings. labels: + - arctastic - test-runner diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index a71ddfc4d..c048b7ac2 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -76,7 +76,7 @@ runs: done - name: PlatformIO ${{ inputs.arch }} download cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.platformio/.cache key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} @@ -100,9 +100,9 @@ runs: id: version - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: - name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip + name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true path: | ${{ inputs.artifact-paths }} diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index d7d26f0e8..de114be1c 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -64,7 +64,7 @@ jobs: PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 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 9ac84c23e..19381e211 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -18,9 +18,10 @@ permissions: read-all jobs: pio-build: name: build-${{ inputs.platform }} - runs-on: ubuntu-24.04 + # Use 'arctastic' self-hosted runner pool when building in the main repo + runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} outputs: - artifact-id: ${{ steps.upload.outputs.artifact-id }} + artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }} steps: - uses: actions/checkout@v6 with: @@ -55,15 +56,40 @@ jobs: ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} + - name: Job summary + env: + PIO_ENV: ${{ inputs.pio_env }} + run: | + echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY + echo "
Manifest" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 - id: upload + uses: actions/upload-artifact@v6 + id: upload-firmware with: - name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip + name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} overwrite: true path: | + release/*.mt.json release/*.bin release/*.elf release/*.uf2 release/*.hex - release/*-ota.zip + release/*.zip + release/device-*.sh + release/device-*.bat + + - name: Store manifests as an artifact + uses: actions/upload-artifact@v6 + id: upload-manifest + with: + name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} + overwrite: true + path: | + release/*.mt.json diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index e4b332a06..9cc0bac78 100644 --- a/.github/workflows/build_one_target.yml +++ b/.github/workflows/build_one_target.yml @@ -98,7 +98,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-*-* @@ -111,7 +111,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true @@ -119,7 +119,7 @@ jobs: ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex - ./firmware-*-ota.zip + ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin @@ -127,7 +127,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -139,14 +139,14 @@ jobs: - name: Device scripts permissions run: | - chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + chmod +x ./output/device-install.sh || true + chmod +x ./output/device-update.sh || true - name: Zip firmware run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true diff --git a/.github/workflows/first_time_contributor.yml b/.github/workflows/first_time_contributor.yml new file mode 100644 index 000000000..1ebc9a602 --- /dev/null +++ b/.github/workflows/first_time_contributor.yml @@ -0,0 +1,47 @@ +name: Welcome First-Time Contributor + +on: + issues: + types: opened + pull_request_target: + types: opened + +permissions: {} + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + issues: write # Required to post comments and labels on issues + pull-requests: write # Required to post comments and labels on PRs + steps: + - uses: plbstl/first-contribution@v4 + with: + labels: first-contribution + issue-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! :wave: + + Thanks for opening your first issue. If it's helpful, an easy way + to get logs is the "Open Serial Monitor" button on the [Web Flasher](https://flasher.meshtastic.org). + + If you have ideas for features, note that we often debate big ideas + in the [discussions tab](https://github.com/meshtastic/firmware/discussions/categories/ideas) + first. This tracker tends to be for ideas that have community + consensus and a clear implementation. + + We're very active [on discord](https://discord.com/invite/meshtastic), + especially the \#firmware and \#alphanauts-testing channels. If you'll + be around for a while, we'd love to see you there! + + Welcome to the community! :heart: + + pr-opened-msg: | + ### @{fc-author}, Welcome to Meshtastic! + + Thanks for opening your first pull request. We really appreciate it. + + We discuss work as a team in discord, please join us in the [#firmware channel](https://discord.com/invite/meshtastic). + There's a big backlog of patches at the moment. If you have time, + please help us with some code review and testing of [other PRs](https://github.com/meshtastic/firmware/pulls)! + + Welcome to the team :smile: diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 38373a2fc..4f7cbf194 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -8,7 +8,9 @@ on: branches: - master - develop + - pioarduino # Remove when merged // use `feature/` in the future. - event/* + - feature/* paths-ignore: - "**.md" - version.properties @@ -18,7 +20,9 @@ on: branches: - master - develop + - pioarduino # Remove when merged // use `feature/` in the future. - event/* + - feature/* paths-ignore: - "**.md" #- "**.yml" @@ -77,16 +81,21 @@ jobs: fail-fast: false matrix: check: ${{ fromJson(needs.setup.outputs.check) }} - - runs-on: ubuntu-latest + # Use 'arctastic' self-hosted runner pool when checking in the main repo + runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} steps: - uses: actions/checkout@v6 - - name: Build base - id: base - uses: ./.github/actions/setup-base + with: + submodules: recursive + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} - name: Check ${{ matrix.check.board }} - run: bin/check-all.sh ${{ matrix.check.board }} + uses: meshtastic/gh-action-firmware@main + with: + pio_platform: ${{ matrix.check.platform }} + pio_env: ${{ matrix.check.board }} + pio_target: check build: needs: [setup, version] @@ -168,7 +177,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -177,19 +186,17 @@ jobs: - 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@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: | + ./firmware-*.mt.json ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex - ./firmware-*-ota.zip + ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin @@ -197,7 +204,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -209,16 +216,16 @@ jobs: - name: Device scripts permissions run: | - chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + 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@v5 + uses: actions/upload-artifact@v6 with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: ./*.elf retention-days: 30 @@ -230,12 +237,55 @@ jobs: 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 + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v6 + if: github.event_name == 'pull_request_target' + 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 + 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 + 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' + 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' + # 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' + # run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/ + release-artifacts: runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} needs: + - setup - version - gather-artifacts - build-debian-src @@ -244,11 +294,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - name: Create release uses: softprops/action-gh-release@v2 id: create_release @@ -261,14 +306,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v6 + 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@v6 + uses: actions/download-artifact@v7 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -284,10 +329,25 @@ jobs: - name: Display structure of downloaded files run: ls -lR - - name: Add Linux sources to GtiHub Release + - name: Generate Release manifest + run: | + jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{ + "version": $ver, + "targets": $targets + }' > firmware-${{ needs.version.outputs.long }}.json + + - name: Save Release manifest artifact + uses: actions/upload-artifact@v6 + with: + name: manifest-${{ needs.version.outputs.long }} + overwrite: true + path: firmware-${{ needs.version.outputs.long }}.json + + - name: Add sources 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-${{ needs.version.outputs.long }}.json 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: @@ -318,7 +378,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -329,15 +389,15 @@ jobs: - name: Device scripts permissions run: | - chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + 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@v6 + - uses: actions/download-artifact@v7 with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./elfs @@ -373,12 +433,19 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - name: Get firmware artifacts + uses: actions/download-artifact@v7 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish + - name: Get manifest artifact + uses: actions/download-artifact@v7 + with: + pattern: manifest-${{ needs.version.outputs.long }} + path: ./publish + - name: Publish firmware to meshtastic.github.io uses: peaceiris/actions-gh-pages@v4 env: diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index 154b230c7..bd3f6d4eb 100644 --- a/.github/workflows/merge_queue.yml +++ b/.github/workflows/merge_queue.yml @@ -147,7 +147,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -160,7 +160,7 @@ jobs: run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat - name: Repackage in single firmware zip - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -168,7 +168,7 @@ jobs: ./firmware-*.bin ./firmware-*.uf2 ./firmware-*.hex - ./firmware-*-ota.zip + ./firmware-*.zip ./device-*.sh ./device-*.bat ./littlefs-*.bin @@ -176,7 +176,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -188,16 +188,16 @@ jobs: - name: Device scripts permissions run: | - chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + 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@v5 + uses: actions/upload-artifact@v6 with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true path: ./*.elf retention-days: 30 @@ -223,11 +223,6 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: 3.x - - name: Create release uses: softprops/action-gh-release@v2 id: create_release @@ -240,14 +235,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v6 + 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@v6 + uses: actions/download-artifact@v7 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -297,7 +292,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -308,15 +303,15 @@ jobs: - name: Device scripts permissions run: | - chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + 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@v6 + - uses: actions/download-artifact@v7 with: - name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip + name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./elfs @@ -352,7 +347,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v7 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml index 2b202ed95..63f1fe8a0 100644 --- a/.github/workflows/package_obs.yml +++ b/.github/workflows/package_obs.yml @@ -58,7 +58,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 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 cb10a79f3..82ffe66e9 100644 --- a/.github/workflows/package_pio_deps.yml +++ b/.github/workflows/package_pio_deps.yml @@ -56,7 +56,7 @@ jobs: PLATFORMIO_CORE_DIR: pio/core - name: Store binaries as an artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 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 2e3278041..9a463dbea 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/pr_enforce_labels.yml b/.github/workflows/pr_enforce_labels.yml index 543e23558..d60c9c8ca 100644 --- a/.github/workflows/pr_enforce_labels.yml +++ b/.github/workflows/pr_enforce_labels.yml @@ -17,7 +17,7 @@ jobs: with: script: | const labels = context.payload.pull_request.labels.map(label => label.name); - const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk']; + const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']; const hasRequiredLabel = labels.some(label => requiredLabels.includes(label)); if (!hasRequiredLabel) { core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 048186538..6306d777f 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -50,9 +50,9 @@ jobs: - name: Download test artifacts if: needs.native-tests.result != 'skipped' - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: - name: platformio-test-report-${{ steps.version.outputs.long }}.zip + name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Parse test results and create detailed summary diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index f21b13ee1..badbb31d4 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -102,7 +102,7 @@ jobs: PIP_DISABLE_PIP_VERSION_CHECK: 1 - name: Create Bumps pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: base: ${{ github.event.repository.default_branch }} branch: create-pull-request/bump-version diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d044f9038..d93449d6d 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@v5 + uses: actions/upload-artifact@v6 with: name: report.sarif overwrite: true diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 11ba59386..fc0702bd8 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Stale PR+Issues - uses: actions/stale@v10.1.0 + uses: actions/stale@v10.1.1 with: days-before-stale: 45 stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml index a2328022e..40fcd7109 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -40,7 +40,7 @@ jobs: - name: Integration test run: | - .pio/build/coverage/program -s & + .pio/build/coverage/meshtasticd -s & PID=$! timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" echo "Simulator started, launching python test..." @@ -59,10 +59,10 @@ jobs: id: version - name: Save coverage information - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() # run this step even if previous step failed with: - name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}.zip + name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} overwrite: true path: ./coverage_*.info @@ -94,9 +94,9 @@ jobs: - name: Save test results if: always() # run this step even if previous step failed - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: - name: platformio-test-report-${{ steps.version.outputs.long }}.zip + name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true path: ./testreport.xml @@ -108,10 +108,10 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() # run this step even if previous step failed with: - name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}.zip + name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} overwrite: true path: ./coverage_*.info @@ -137,22 +137,22 @@ jobs: id: version - name: Download test artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: - name: platformio-test-report-${{ steps.version.outputs.long }}.zip + name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true - name: Test Report - uses: dorny/test-reporter@v2.2.0 + uses: dorny/test-reporter@v2.4.0 with: name: PlatformIO Tests path: testreport.xml reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: - pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip + pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report merge-multiple: true @@ -163,7 +163,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@v5 + uses: actions/upload-artifact@v6 with: - name: code-coverage-report-${{ steps.version.outputs.long }}.zip + 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 4a97853e2..241f2cd10 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout code uses: actions/checkout@v6 - # - uses: actions/setup-python@v5 + # - uses: actions/setup-python@v6 # with: # python-version: '3.10' diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index af0557fda..35565d1e4 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -16,7 +16,7 @@ jobs: submodules: true - name: Update submodule - if: ${{ github.ref == 'refs/heads/master' }} + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} run: | git submodule update --remote protobufs @@ -31,7 +31,7 @@ jobs: ./bin/regen-protos.sh - name: Create pull request - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: branch: create-pull-request/update-protobufs labels: submodules diff --git a/.gitignore b/.gitignore index cc742c6c1..06e8c472f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,12 @@ src/mesh/raspihttp/private_key.pem # Ignore logo (set at build time with platformio-custom.py) data/boot/logo.* + +# pioarduino platform +managed_components/* +arduino-lib-builder* +dependencies.lock +idf_component.yml +CMakeLists.txt +sdkconfig.* +.dummy/* diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 1fd8790f2..705f0177c 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,31 +4,31 @@ cli: plugins: sources: - id: trunk - ref: v1.7.3 + ref: v1.7.4 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.492 - - renovate@42.5.4 - - prettier@3.6.2 - - trufflehog@3.90.13 + - checkov@3.2.496 + - renovate@42.66.14 + - prettier@3.7.4 + - trufflehog@3.92.4 - yamllint@1.37.1 - - bandit@1.8.6 - - trivy@0.67.2 + - bandit@1.9.2 + - trivy@0.68.2 - taplo@0.10.0 - - ruff@0.14.4 + - ruff@0.14.10 - isort@7.0.0 - - markdownlint@0.45.0 - - oxipng@9.1.5 + - markdownlint@0.47.0 + - oxipng@10.0.0 - svgo@4.0.0 - - actionlint@1.7.8 + - actionlint@1.7.9 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 - shellcheck@0.11.0 - - black@25.11.0 + - black@25.12.0 - git-diff-check - - gitleaks@8.29.0 + - gitleaks@8.30.0 - clang-format@16.0.3 ignore: - linters: [ALL] diff --git a/SECURITY.md b/SECURITY.md index 5092595e1..a77a4d028 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Firmware Version | Supported | | ---------------- | ------------------ | -| 2.6.x | :white_check_mark: | -| <= 2.5.x | :x: | +| 2.7.x | :white_check_mark: | +| <= 2.6.x | :x: | ## Reporting a Vulnerability diff --git a/alpine.Dockerfile b/alpine.Dockerfile index bdee57d79..b3b384101 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -28,7 +28,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \ # ##### PRODUCTION BUILD ############# -FROM alpine:3.22 +FROM alpine:3.23 LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.url="https://meshtastic.org" \ diff --git a/arch/nrf52/nrf52832.ini b/arch/nrf52/nrf52832.ini deleted file mode 100644 index ce94283b1..000000000 --- a/arch/nrf52/nrf52832.ini +++ /dev/null @@ -1,7 +0,0 @@ -[nrf52832_base] -extends = nrf52_base - -build_flags = ${nrf52_base.build_flags} - -lib_deps = - ${nrf52_base.lib_deps} diff --git a/bin/analyze_map.py b/bin/analyze_map.py new file mode 100644 index 000000000..99997c703 --- /dev/null +++ b/bin/analyze_map.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Summarise linker map output to highlight heavy object files and libraries. + +Usage: + python bin/analyze_map.py --map .pio/build/rak4631/output.map --top 20 + +The script parses GNU ld map files and aggregates section sizes per object file +and per archive/library, then prints sortable tables that make it easy to spot +modules worth trimming or hiding behind feature flags. +""" +from __future__ import annotations + +import argparse +import collections +import os +import re +import sys +from typing import DefaultDict, Dict, Tuple + + +SECTION_LINE_RE = re.compile(r"^\s+(?P
\S+)\s+0x[0-9A-Fa-f]+\s+0x(?P[0-9A-Fa-f]+)\s+(?P.+)$") +ARCHIVE_MEMBER_RE = re.compile(r"^(?P.+)\((?P[^)]+)\)$") + + +def human_size(num_bytes: int) -> str: + """Return a friendly size string with one decimal place.""" + if num_bytes < 1024: + return f"{num_bytes:,} B" + num = float(num_bytes) + for unit in ("KB", "MB", "GB"): + num /= 1024.0 + if num < 1024.0: + return f"{num:.1f} {unit}" + return f"{num:.1f} TB" + + +def shorten_path(path: str, root: str) -> str: + """Prefer repository-relative paths for readability.""" + path = path.strip() + if not path: + return path + + # Normalise Windows archives (backslashes) to POSIX style for consistency. + path = path.replace("\\", "/") + + # Attempt to strip the root when an absolute path lives inside the repo. + if os.path.isabs(path): + try: + rel = os.path.relpath(path, root) + if not rel.startswith(".."): + return rel + except ValueError: + # relpath can fail on mixed drives on Windows; fall back to basename. + pass + return path + + +def describe_object(raw_object: str, root: str) -> Tuple[str, str]: + """Return a human friendly object label and the library it belongs to.""" + raw_object = raw_object.strip() + lib_label = "[app]" + match = ARCHIVE_MEMBER_RE.match(raw_object) + if match: + archive = shorten_path(match.group("archive"), root) + obj = match.group("object") + lib_label = os.path.basename(archive) or archive + label = f"{archive}:{obj}" + else: + label = shorten_path(raw_object, root) + # If the object lives under libs, hint at the containing directory. + parent = os.path.basename(os.path.dirname(label)) + if parent: + lib_label = parent + return label, lib_label + + +def parse_map(map_path: str, repo_root: str) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, int]]]: + per_object: DefaultDict[str, int] = collections.defaultdict(int) + per_library: DefaultDict[str, int] = collections.defaultdict(int) + per_object_sections: DefaultDict[str, DefaultDict[str, int]] = collections.defaultdict(lambda: collections.defaultdict(int)) + + try: + with open(map_path, "r", encoding="utf-8", errors="ignore") as handle: + for line in handle: + match = SECTION_LINE_RE.match(line) + if not match: + continue + + section = match.group("section") + if section.startswith("*") or section in {"LOAD", "ORIGIN"}: + continue + + size = int(match.group("size"), 16) + if size == 0: + continue + + obj_token = match.group("object").strip() + if not obj_token or obj_token.startswith("*") or "load address" in obj_token: + continue + + label, lib_label = describe_object(obj_token, repo_root) + per_object[label] += size + per_library[lib_label] += size + per_object_sections[label][section] += size + except FileNotFoundError: + raise SystemExit(f"error: map file '{map_path}' not found. Run a build first.") + + return per_object, per_library, per_object_sections + + +def format_section_breakdown(section_sizes: Dict[str, int], total: int, limit: int = 3) -> str: + items = sorted(section_sizes.items(), key=lambda kv: kv[1], reverse=True) + parts = [] + for section, size in items[:limit]: + pct = (size / total) * 100 if total else 0 + parts.append(f"{section} {pct:.1f}%") + if len(items) > limit: + remainder = total - sum(size for _, size in items[:limit]) + pct = (remainder / total) * 100 if total else 0 + parts.append(f"other {pct:.1f}%") + return ", ".join(parts) + + +def print_report(map_path: str, top_n: int, per_object: Dict[str, int], per_library: Dict[str, int], per_object_sections: Dict[str, Dict[str, int]]): + total_bytes = sum(per_object.values()) + if total_bytes == 0: + print("No section data found in map file.") + return + + print(f"Map file: {map_path}") + print(f"Accounted size: {human_size(total_bytes)} across {len(per_object)} object files\n") + + sorted_objects = sorted(per_object.items(), key=lambda kv: kv[1], reverse=True) + print(f"Top {min(top_n, len(sorted_objects))} object files by linked size:") + for idx, (obj, size) in enumerate(sorted_objects[:top_n], 1): + pct = (size / total_bytes) * 100 + breakdown = format_section_breakdown(per_object_sections[obj], size) + print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size)") + print(f" {obj}") + if breakdown: + print(f" sections: {breakdown}") + print() + + sorted_libs = sorted(per_library.items(), key=lambda kv: kv[1], reverse=True) + print(f"Top {min(top_n, len(sorted_libs))} libraries or source roots:") + for idx, (lib, size) in enumerate(sorted_libs[:top_n], 1): + pct = (size / total_bytes) * 100 + print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size) {lib}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Highlight heavy object files from a GNU ld map file.") + parser.add_argument("--map", default=".pio/build/rak4631/output.map", help="Path to the map file (default: %(default)s)") + parser.add_argument("--top", type=int, default=20, help="Number of entries to display per table (default: %(default)s)") + args = parser.parse_args() + + map_path = os.path.abspath(args.map) + repo_root = os.path.abspath(os.getcwd()) + + per_object, per_library, per_object_sections = parse_map(map_path, repo_root) + print_report(os.path.relpath(map_path, repo_root), args.top, per_object, per_library, per_object_sections) + + +if __name__ == "__main__": + main() diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh index 92836db23..d07a09a16 100755 --- a/bin/build-esp32.sh +++ b/bin/build-esp32.sh @@ -5,7 +5,8 @@ set -e VERSION=`bin/buildinfo.py long` SHORT_VERSION=`bin/buildinfo.py short` -OUTDIR=release/ +BUILDDIR=.pio/build/$1 +OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true @@ -14,33 +15,27 @@ rm -r $OUTDIR/* || true platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" -rm -f .pio/build/$1/firmware.* +rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v -SRCELF=.pio/build/$1/firmware.elf -cp $SRCELF $OUTDIR/$basename.elf +pio run --environment $1 -t mtjson # -v + +cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying ESP32 bin file" -SRCBIN=.pio/build/$1/firmware.factory.bin -cp $SRCBIN $OUTDIR/$basename.bin +cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin echo "Copying ESP32 update bin file" -SRCBIN=.pio/build/$1/firmware.bin -cp $SRCBIN $OUTDIR/$basename-update.bin +cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin -echo "Building Filesystem for ESP32 targets" -# If you want to build the webui, uncomment the following lines -# pio run --environment $1 -t buildfs -# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin -# # Remove webserver files from the filesystem and rebuild -# ls -l data/static # Diagnostic list of files -# rm -rf data/static -pio run --environment $1 -t buildfs -cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin -cp bin/device-install.* $OUTDIR -cp bin/device-update.* $OUTDIR \ No newline at end of file +echo "Copying Filesystem for ESP32 targets" +cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin +cp bin/device-install.* $OUTDIR/ +cp bin/device-update.* $OUTDIR/ + +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-native.sh b/bin/build-native.sh index fff86e87e..f35e46a87 100755 --- a/bin/build-native.sh +++ b/bin/build-native.sh @@ -17,15 +17,19 @@ VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) PIO_ENV=${1:-native} -OUTDIR=release/ +BUILDDIR=.pio/build/$PIO_ENV +OUTDIR=release -rm -f $OUTDIR/firmware* +rm -f $OUTDIR/meshtasticd* mkdir -p $OUTDIR/ rm -r $OUTDIR/* || true +basename=meshtasticd-$1-$VERSION + # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale pio pkg install --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed -cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)" -cp bin/native-install.* $OUTDIR + +cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)" +cp bin/native-install.* $OUTDIR/ diff --git a/bin/build-nrf52.sh b/bin/build-nrf52.sh index deca209d2..99187ba0d 100755 --- a/bin/build-nrf52.sh +++ b/bin/build-nrf52.sh @@ -5,7 +5,8 @@ set -e VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) -OUTDIR=release/ +BUILDDIR=.pio/build/$1 +OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true @@ -14,40 +15,38 @@ rm -r $OUTDIR/* || true platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" -rm -f .pio/build/$1/firmware.* +rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION +ota_basename=${basename}-ota -pio run --environment $1 # -v -SRCELF=.pio/build/$1/firmware.elf -cp $SRCELF $OUTDIR/$basename.elf +pio run --environment $1 -t mtjson # -v -echo "Generating NRF52 dfu file" -DFUPKG=.pio/build/$1/firmware.zip -cp $DFUPKG $OUTDIR/$basename-ota.zip +cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf -echo "Generating NRF52 uf2 file" -SRCHEX=.pio/build/$1/firmware.hex +echo "Copying NRF52 dfu (OTA) file" +cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip -# if WM1110 target, merge hex with softdevice 7.3.0 +echo "Copying NRF52 UF2 file" +cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 +cp bin/*.uf2 $OUTDIR/ + +SRCHEX=$BUILDDIR/$basename.hex + +# if WM1110 target, copy the merged.hex if (echo $1 | grep -q "wio-sdk-wm1110"); then - echo "Merging with softdevice" - bin/mergehex -m bin/s140_nrf52_7.3.0_softdevice.hex $SRCHEX -o .pio/build/$1/$basename.hex - SRCHEX=.pio/build/$1/$basename.hex - bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840 - cp $SRCHEX $OUTDIR - cp bin/*.uf2 $OUTDIR -else - bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840 - cp bin/device-install.* $OUTDIR - cp bin/device-update.* $OUTDIR - cp bin/*.uf2 $OUTDIR + echo "Copying .merged.hex file" + SRCHEX=$BUILDDIR/$basename.merged.hex + cp $SRCHEX $OUTDIR/ fi if (echo $1 | grep -q "rak4631"); then - echo "Copying hex file" - cp .pio/build/$1/firmware.hex $OUTDIR/$basename.hex -fi \ No newline at end of file + echo "Copying .hex file" + cp $SRCHEX $OUTDIR/ +fi + +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-rp2xx0.sh b/bin/build-rp2xx0.sh index cb4865914..992a39be7 100755 --- a/bin/build-rp2xx0.sh +++ b/bin/build-rp2xx0.sh @@ -5,7 +5,8 @@ set -e VERSION=`bin/buildinfo.py long` SHORT_VERSION=`bin/buildinfo.py short` -OUTDIR=release/ +BUILDDIR=.pio/build/$1 +OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true @@ -14,20 +15,19 @@ rm -r $OUTDIR/* || true platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" -rm -f .pio/build/$1/firmware.* +rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v -SRCELF=.pio/build/$1/firmware.elf -cp $SRCELF $OUTDIR/$basename.elf +pio run --environment $1 -t mtjson # -v + +cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Copying uf2 file" -SRCBIN=.pio/build/$1/firmware.uf2 -cp $SRCBIN $OUTDIR/$basename.uf2 +cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 -cp bin/device-install.* $OUTDIR -cp bin/device-update.* $OUTDIR +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/build-stm32wl.sh b/bin/build-stm32wl.sh index f62df4842..64eb36586 100755 --- a/bin/build-stm32wl.sh +++ b/bin/build-stm32wl.sh @@ -5,7 +5,8 @@ set -e VERSION=$(bin/buildinfo.py long) SHORT_VERSION=$(bin/buildinfo.py short) -OUTDIR=release/ +BUILDDIR=.pio/build/$1 +OUTDIR=release rm -f $OUTDIR/firmware* rm -r $OUTDIR/* || true @@ -14,16 +15,19 @@ rm -r $OUTDIR/* || true platformio pkg install -e $1 echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" -rm -f .pio/build/$1/firmware.* +rm -f $BUILDDIR/firmware* # The shell vars the build tool expects to find export APP_VERSION=$VERSION basename=firmware-$1-$VERSION -pio run --environment $1 # -v -SRCELF=.pio/build/$1/firmware.elf -cp $SRCELF $OUTDIR/$basename.elf +pio run --environment $1 -t mtjson # -v -SRCBIN=.pio/build/$1/firmware.bin -cp $SRCBIN $OUTDIR/$basename.bin +cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf + +echo "Copying STM32 bin file" +cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin + +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index b4cc81792..adf804ba9 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -184,6 +184,8 @@ Input: Logging: LogLevel: info # debug, info, warn, error # TraceFile: /var/log/meshtasticd.json +# JSONFile: /packets.json # File location for JSON output of decoded packets +# JSONFilter: position # filter for packets to save to JSON file # AsciiLogs: true # default if not specified is !isatty() on stdout Webserver: diff --git a/bin/config.d/lora-usb-meshstick-1262.yaml b/bin/config.d/lora-usb-meshstick-1262.yaml new file mode 100644 index 000000000..a539d76a1 --- /dev/null +++ b/bin/config.d/lora-usb-meshstick-1262.yaml @@ -0,0 +1,14 @@ +Lora: + Module: sx1262 + CS: 0 + IRQ: 6 + Reset: 2 + Busy: 4 + RXen: 1 + DIO2_AS_RF_SWITCH: true + spidev: ch341 + DIO3_TCXO_VOLTAGE: true +# USB_Serialnum: 12345678 + USB_PID: 0x5512 + USB_VID: 0x1A86 + SX126X_MAX_POWER: 22 \ No newline at end of file diff --git a/bin/device-install.bat b/bin/device-install.bat index 519073b08..c200a3201 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -5,22 +5,14 @@ TITLE Meshtastic device-install SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" -SET "TFT_BUILD=0" -SET "BIGDB8=0" -SET "MUIDB8=0" -SET "BIGDB16=0" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" SET "BPS_RESET=0" - -@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. -SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv heltec-v4" -SET "C3=esp32c3" -@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. -SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" -SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator" -SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv heltec-v4" +@REM Default offsets. +@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202 +SET "OTA_OFFSET=0x260000" +SET "SPIFFS_OFFSET=0x300000" GOTO getopts :help @@ -29,7 +21,7 @@ ECHO. ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO. ECHO Options: -ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required) +ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required) ECHO The file must be located in this current directory. ECHO -p PORT Set the environment variable for ESPTOOL_PORT. ECHO If not set, ESPTOOL iterates all ports (Dangerous). @@ -40,12 +32,12 @@ ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset -ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 -ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 +ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11 +ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11 GOTO eof :version -ECHO %SCRIPT_NAME% [Version 2.6.2] +ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO Meshtastic GOTO eof @@ -78,8 +70,8 @@ IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported." GOTO help ) - IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" ( - CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file." + IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( + CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file." GOTO help ) @REM Remove ".\" or "./" file prefix if present. @@ -93,12 +85,26 @@ IF NOT EXIST !FILENAME! ( GOTO eof ) -IF NOT "!FILENAME:update=!"=="!FILENAME!" ( - CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" - CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!." - GOTO eof +CALL :LOG_MESSAGE DEBUG "Checking for metadata..." +@REM Derive metadata filename from firmware filename. +SET "METAFILE=!FILENAME:.factory.bin=!.mt.json" +IF EXIST !METAFILE! ( + @REM Print parsed json with powershell + CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!" + powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()" + + @REM Save metadata values to variables for later use. + FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ + "(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A" + FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ + "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"` + ) DO SET "OTA_OFFSET=%%A" + FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^ + "(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"` + ) DO SET "SPIFFS_OFFSET=%%A" ) ELSE ( - CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" + CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!" + GOTO eof ) :skip-filename @@ -108,7 +114,7 @@ IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." ) ELSE ( - CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool... + CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." WHERE esptool >nul 2>&1 IF %ERRORLEVEL% EQU 0 ( @REM WHERE exits with code 0 if esptool is found. @@ -146,100 +152,26 @@ IF %BPS_RESET% EQU 1 ( GOTO eof ) -@REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. -@REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3 -IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" ( - CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!" - SET "TFT_BUILD=1" +@REM Extract PROGNAME from %FILENAME% for later use. +SET "PROGNAME=!FILENAME:.factory.bin=!" +CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!" + +IF "__!MCU!__" == "__esp32s3__" ( + @REM We are working with ESP32-S3 + SET "OTA_FILENAME=bleota-s3.bin" +) ELSE IF "__!MCU!__" == "__esp32c3__" ( + @REM We are working with ESP32-C3 + SET "OTA_FILENAME=bleota-c3.bin" ) ELSE ( - CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!" + @REM Everything else + SET "OTA_FILENAME=bleota.bin" ) - -FOR %%a IN (%BIGDB_8MB%) DO ( - IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( - @REM We are working with any of %BIGDB_8MB%. - SET "BIGDB8=1" - GOTO end_loop_bigdb_8mb - ) -) -:end_loop_bigdb_8mb - -FOR %%a IN (%MUIDB_8MB%) DO ( - IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( - @REM We are working with any of %MUIDB_8MB%. - SET "MUIDB8=1" - GOTO end_loop_muidb_8mb - ) -) -:end_loop_muidb_8mb - -FOR %%a IN (%BIGDB_16MB%) DO ( - IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( - @REM We are working with any of %BIGDB_16MB%. - SET "BIGDB16=1" - GOTO end_loop_bigdb_16mb - ) -) -:end_loop_bigdb_16mb - -IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected." -IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected." -IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected." - -@REM Extract BASENAME from %FILENAME% for later use. -SET "BASENAME=!FILENAME:firmware-=!" -CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!" - -@REM Account for S3 and C3 board's different OTA partition. -FOR %%a IN (%S3%) DO ( - IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( - @REM We are working with any of %S3%. - SET "OTA_FILENAME=bleota-s3.bin" - GOTO :end_loop_s3 - ) -) - -FOR %%a IN (%C3%) DO ( - IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( - @REM We are working with any of %C3%. - SET "OTA_FILENAME=bleota-c3.bin" - GOTO :end_loop_c3 - ) -) - -@REM Everything else -SET "OTA_FILENAME=bleota.bin" -:end_loop_s3 -:end_loop_c3 CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" @REM Set SPIFFS filename with "littlefs-" prefix. -SET "SPIFFS_FILENAME=littlefs-%BASENAME%" +SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" -@REM Default offsets. -@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202 -SET "OTA_OFFSET=0x260000" -SET "SPIFFS_OFFSET=0x300000" - -@REM Offsets for BigDB 8mb. -IF %BIGDB8% EQU 1 ( - SET "OTA_OFFSET=0x340000" - SET "SPIFFS_OFFSET=0x670000" -) - -@REM Offsets for MUIDB 8mb. -IF %MUIDB8% EQU 1 ( - SET "OTA_OFFSET=0x5D0000" - SET "SPIFFS_OFFSET=0x670000" -) - -@REM Offsets for BigDB 16mb. -IF %BIGDB16% EQU 1 ( - SET "OTA_OFFSET=0x650000" - SET "SPIFFS_OFFSET=0xc90000" -) - CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!" diff --git a/bin/device-install.sh b/bin/device-install.sh index 69e4794ba..1778a952d 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -2,69 +2,15 @@ PYTHON=${PYTHON:-$(which python3 python | head -n 1)} BPS_RESET=false -TFT_BUILD=false MCU="" # Constants RESET_BAUD=1200 FIRMWARE_OFFSET=0x00 - -# Variant groups -BIGDB_8MB=( - "crowpanel-esp32s3" - "heltec_capsule_sensor_v3" - "heltec-v3" - "heltec-vision-master-e213" - "heltec-vision-master-e290" - "heltec-vision-master-t190" - "heltec-wireless-paper" - "heltec-wireless-tracker" - "heltec-wsl-v3" - "icarus" - "seeed-xiao-s3" - "tbeam-s3-core" - "tracksenger" -) -MUIDB_8MB=( - "picomputer-s3" - "unphone" - "seeed-sensecap-indicator" -) -BIGDB_16MB=( - "dreamcatcher" - "elecrow-adv" - "ESP32-S3-Pico" - "heltec-v4" - "m5stack-cores3" - "mesh-tab" - "station-g2" - "t-deck" - "t-energy-s3" - "t-eth-elite" - "t-watch-s3" - "tlora-pager" -) -S3_VARIANTS=( - "s3" - "-v3" - "-v4" - "t-deck" - "wireless-paper" - "wireless-tracker" - "station-g2" - "unphone" - "t-eth-elite" - "tlora-pager" - "mesh-tab" - "dreamcatcher" - "ESP32-S3-Pico" - "seeed-sensecap-indicator" - "heltec_capsule_sensor_v3" - "vision-master" - "icarus" - "tracksenger" - "elecrow-adv" -) +# Default littlefs* offset. +OFFSET=0x300000 +# Default OTA Offset +OTA_OFFSET=0x260000 # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then @@ -78,6 +24,14 @@ else exit 1 fi +# Check for jq +if ! command -v jq >/dev/null 2>&1; then + echo "Error: jq not found" >&2 + echo "Install jq with your package manager." >&2 + echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2 + exit 1 +fi + set -e # Usage info @@ -89,7 +43,7 @@ Flash image file to device, but first erasing and writing system information. -h Display this help and exit. -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") - -f FILENAME The firmware .bin file to flash. Custom to your device type and region. + -f FILENAME The firmware *.factory.bin file to flash. Custom to your device type and region. --1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) EOF @@ -138,69 +92,43 @@ fi shift } -if [[ "$FILENAME" != firmware-* ]]; then - echo "Filename must be a firmware-* file." +if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then + echo "Filename must be a firmware-*.factory.bin file." exit 1 fi -# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. -if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then - TFT_BUILD=true -fi +# Extract PROGNAME from %FILENAME% for later use. +PROGNAME="${FILENAME/.factory.bin/}" +# Derive metadata filename from %PROGNAME%. +METAFILE="${PROGNAME}.mt.json" -# Extract BASENAME from %FILENAME% for later use. -BASENAME="${FILENAME/firmware-/}" - -if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then - # Default littlefs* offset. - OFFSET=0x300000 - - # Default OTA Offset - OTA_OFFSET=0x260000 - - # littlefs* offset for BigDB 8mb and OTA OFFSET. - for variant in "${BIGDB_8MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0x670000 - OTA_OFFSET=0x340000 - fi - done - - for variant in "${MUIDB_8MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0x670000 - OTA_OFFSET=0x5D0000 - fi - done - - # littlefs* offset for BigDB 16mb and OTA OFFSET. - for variant in "${BIGDB_16MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0xc90000 - OTA_OFFSET=0x650000 - fi - done - - # Account for S3 board's different OTA partition - # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable - for variant in "${S3_VARIANTS[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - MCU="esp32s3" - fi - done - - if [ "$MCU" != "esp32s3" ]; then - if [ -n "${FILENAME##*"esp32c3"*}" ]; then - OTAFILE=bleota.bin - else - OTAFILE=bleota-c3.bin +if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then + # Display metadata if it exists + if [[ -f "$METAFILE" ]]; then + echo "Firmware metadata: ${METAFILE}" + jq . "$METAFILE" + # Extract relevant fields from metadata + if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then + OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE") + SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE") fi + MCU=$(jq -r '.mcu' "$METAFILE") else + echo "ERROR: No metadata file found at ${METAFILE}" + exit 1 + fi + + # Determine OTA filename based on MCU type + if [ "$MCU" == "esp32s3" ]; then OTAFILE=bleota-s3.bin + elif [ "$MCU" == "esp32c3" ]; then + OTAFILE=bleota-c3.bin + else + OTAFILE=bleota.bin fi # Set SPIFFS filename with "littlefs-" prefix. - SPIFFSFILE=littlefs-${BASENAME} + SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin" if [[ ! -f "$FILENAME" ]]; then echo "Error: file ${FILENAME} wasn't found. Terminating." diff --git a/bin/device-update.bat b/bin/device-update.bat index a263da992..a9f7a9e1e 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -30,11 +30,11 @@ ECHO --change-mode Attempt to place the device in correct mode. (1200bps ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode -ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11 +ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 GOTO eof :version -ECHO %SCRIPT_NAME% [Version 2.6.2] +ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO Meshtastic GOTO eof @@ -78,12 +78,12 @@ IF NOT EXIST !FILENAME! ( GOTO eof ) -IF "!FILENAME:update=!"=="!FILENAME!" ( - CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" +IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( + CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!" CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!." GOTO eof ) ELSE ( - CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" + CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!" ) :skip-filename diff --git a/bin/device-update.sh b/bin/device-update.sh index f64280a5b..1c3d6be70 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -29,7 +29,7 @@ Flash image file to device, leave existing system intact." -h Display this help and exit -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") - -f FILENAME The *update.bin file to flash. Custom to your device type. + -f FILENAME The *.bin file to flash. Custom to your device type. --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) EOF @@ -78,7 +78,7 @@ fi shift } -if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then +if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then echo "Trying to flash update ${FILENAME}" $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" else diff --git a/bin/exception_decoder.py b/bin/exception_decoder.py index ec94ce20e..ffe6d3f24 100755 --- a/bin/exception_decoder.py +++ b/bin/exception_decoder.py @@ -75,7 +75,7 @@ TOOLS = { } BACKTRACE_REGEX = re.compile( - r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b" + r"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-F]{8}\b" ) EXCEPTION_REGEX = re.compile("^Exception \\((?P[0-9]*)\\):$") COUNTER_REGEX = re.compile( @@ -89,7 +89,7 @@ POINTER_REGEX = re.compile( STACK_BEGIN = ">>>stack>>>" STACK_END = "<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$" + r"^(?P[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+)(\W.*)?$" ) StackLine = namedtuple("StackLine", ["offset", "content"]) @@ -223,7 +223,7 @@ class AddressResolver(object): if match is None: if last is not None and line.startswith("(inlined by)"): line = line[12:].strip() - self._address_map[last] += "\n \-> inlined by: " + line + self._address_map[last] += "\n \\-> inlined by: " + line continue if match.group("result") == "?? ??:0": diff --git a/bin/native-gdbserver.sh b/bin/native-gdbserver.sh index f779d6670..a45a2dc26 100755 --- a/bin/native-gdbserver.sh +++ b/bin/native-gdbserver.sh @@ -2,4 +2,4 @@ set -e pio run --environment native -gdbserver --once localhost:2345 .pio/build/native/program "$@" +gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@" diff --git a/bin/native-run.sh b/bin/native-run.sh index 6566fc591..a8309c2d3 100755 --- a/bin/native-run.sh +++ b/bin/native-run.sh @@ -2,4 +2,4 @@ set -e pio run --environment native -.pio/build/native/program "$@" +.pio/build/native/meshtasticd "$@" diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 243edca0c..cb8985ee6 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.18 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16 diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 4a1887d9d..90d733ca7 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -6,94 +6,188 @@ from os.path import join import subprocess import json import re -import time from datetime import datetime +from typing import Dict from readprops import readProps Import("env") platform = env.PioPlatform() +progname = env.get("PROGNAME") +lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin" +manifest_ran = False +def infer_architecture(board_cfg): + try: + mcu = board_cfg.get("build.mcu") if board_cfg else None + except KeyError: + mcu = None + except Exception: + mcu = None + if not mcu: + return None + mcu_l = str(mcu).lower() + if "esp32s3" in mcu_l: + return "esp32-s3" + if "esp32c6" in mcu_l: + return "esp32-c6" + if "esp32c3" in mcu_l: + return "esp32-c3" + if "esp32" in mcu_l: + return "esp32" + if "rp2040" in mcu_l: + return "rp2040" + if "rp2350" in mcu_l: + return "rp2350" + if "nrf52" in mcu_l or "nrf52840" in mcu_l: + return "nrf52840" + if "stm32" in mcu_l: + return "stm32" + return None -def esp32_create_combined_bin(source, target, env): - # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3 - # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py - print("Generating combined binary for serial flashing") +def manifest_gather(source, target, env): + global manifest_ran + if manifest_ran: + return + # Skip manifest generation if we cannot determine architecture (host/native builds) + board_arch = infer_architecture(env.BoardConfig()) + if not board_arch: + print(f"Skipping mtjson generation for unknown architecture (env={env.get('PIOENV')})") + manifest_ran = True + return + manifest_ran = True + out = [] + board_platform = env.BoardConfig().get("platform") + needs_ota_suffix = board_platform == "nordicnrf52" + check_paths = [ + progname, + f"{progname}.elf", + f"{progname}.bin", + f"{progname}.factory.bin", + f"{progname}.hex", + f"{progname}.merged.hex", + f"{progname}.uf2", + f"{progname}.factory.uf2", + f"{progname}.zip", + lfsbin + ] + for p in check_paths: + f = env.File(env.subst(f"$BUILD_DIR/{p}")) + if f.exists(): + manifest_name = p + if needs_ota_suffix and p == f"{progname}.zip": + manifest_name = f"{progname}-ota.zip" + d = { + "name": manifest_name, + "md5": f.get_content_hash(), # Returns MD5 hash + "bytes": f.get_size() # Returns file size in bytes + } + out.append(d) + print(d) + manifest_write(out, env) - app_offset = 0x10000 +def manifest_write(files, env): + # Defensive: also skip manifest writing if we cannot determine architecture + def get_project_option(name): + try: + return env.GetProjectOption(name) + except Exception: + return None - new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") - sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) - firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") - chip = env.get("BOARD_MCU") - flash_size = env.BoardConfig().get("upload.flash_size") - flash_freq = env.BoardConfig().get("build.f_flash", "40m") - flash_freq = flash_freq.replace("000000L", "m") - flash_mode = env.BoardConfig().get("build.flash_mode", "dio") - memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi") - if flash_mode == "qio" or flash_mode == "qout": - flash_mode = "dio" - if memory_type == "opi_opi" or memory_type == "opi_qspi": - flash_mode = "dout" - cmd = [ - "--chip", - chip, - "merge_bin", - "-o", - new_file_name, - "--flash_mode", - flash_mode, - "--flash_freq", - flash_freq, - "--flash_size", - flash_size, + def get_project_option_any(names): + for name in names: + val = get_project_option(name) + if val is not None: + return val + return None + + def as_bool(val): + return str(val).strip().lower() in ("1", "true", "yes", "on") + + def as_int(val): + try: + return int(str(val), 10) + except (TypeError, ValueError): + return None + + def as_list(val): + return [item.strip() for item in str(val).split(",") if item.strip()] + + manifest = { + "version": verObj["long"], + "build_epoch": build_epoch, + "platformioTarget": env.get("PIOENV"), + "mcu": env.get("BOARD_MCU"), + "repo": repo_owner, + "files": files, + "has_mui": False, + "has_inkhud": False, + } + # Get partition table (generated in esp32_pre.py) if it exists + if env.get("custom_mtjson_part"): + # custom_mtjson_part is a JSON string, convert it back to a dict + pj = json.loads(env.get("custom_mtjson_part")) + manifest["part"] = pj + # Enable has_mui for TFT builds + if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): + manifest["has_mui"] = True + if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []): + manifest["has_inkhud"] = True + + pioenv = env.get("PIOENV") + device_meta = {} + device_meta_fields = [ + ("hwModel", ["custom_meshtastic_hw_model"], as_int), + ("hwModelSlug", ["custom_meshtastic_hw_model_slug"], str), + ("architecture", ["custom_meshtastic_architecture"], str), + ("activelySupported", ["custom_meshtastic_actively_supported"], as_bool), + ("displayName", ["custom_meshtastic_display_name"], str), + ("supportLevel", ["custom_meshtastic_support_level"], as_int), + ("images", ["custom_meshtastic_images"], as_list), + ("tags", ["custom_meshtastic_tags"], as_list), + ("requiresDfu", ["custom_meshtastic_requires_dfu"], as_bool), + ("partitionScheme", ["custom_meshtastic_partition_scheme"], str), + ("url", ["custom_meshtastic_url"], str), + ("key", ["custom_meshtastic_key"], str), + ("variant", ["custom_meshtastic_variant"], str), ] - print(" Offset | File") - for section in sections: - sect_adr, sect_file = section.split(" ", 1) - print(f" - {sect_adr} | {sect_file}") - cmd += [sect_adr, sect_file] - print(f" - {hex(app_offset)} | {firmware_name}") - cmd += [hex(app_offset), firmware_name] + for manifest_key, option_keys, caster in device_meta_fields: + raw_val = get_project_option_any(option_keys) + if raw_val is None: + continue + parsed = caster(raw_val) if callable(caster) else raw_val + if parsed is not None and parsed != "": + device_meta[manifest_key] = parsed - print("Using esptool.py arguments: %s" % " ".join(cmd)) + # Determine architecture once; if we can't infer it, skip manifest generation + board_arch = device_meta.get("architecture") or infer_architecture(env.BoardConfig()) + if not board_arch: + print(f"Skipping mtjson write for unknown architecture (env={env.get('PIOENV')})") + return - esptool.main(cmd) + device_meta["architecture"] = board_arch + # Always set requiresDfu: true for nrf52840 targets + if board_arch == "nrf52840": + device_meta["requiresDfu"] = True -if platform.name == "espressif32": - sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) - import esptool + device_meta.setdefault("displayName", pioenv) + device_meta.setdefault("activelySupported", False) - env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) + if device_meta: + manifest.update(device_meta) - esp32_kind = env.GetProjectOption("custom_esp32_kind") - if esp32_kind == "esp32": - # Free up some IRAM by removing auxiliary SPI flash chip drivers. - # Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c. - env.Append( - LINKFLAGS=[ - "-Wl,--wrap=esp_flash_chip_gd", - "-Wl,--wrap=esp_flash_chip_issi", - "-Wl,--wrap=esp_flash_chip_winbond", - ] - ) - else: - # For newer ESP32 targets, using newlib nano works better. - env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"]) - -if platform.name == "nordicnrf52": - env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", - env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"", - "Generating UF2 file")) + # Write the manifest to the build directory + with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f: + json.dump(manifest, f, indent=2) Import("projenv") prefsLoc = projenv["PROJECT_DIR"] + "/version.properties" verObj = readProps(prefsLoc) -print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV")) +print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}") # get repository owner if git is installed try: @@ -139,10 +233,10 @@ flags = [ "-DBUILD_EPOCH=" + str(build_epoch), ] + pref_flags -print ("Using flags:") +print("Using flags:") for flag in flags: print(flag) - + projenv.Append( CCFLAGS=flags, ) @@ -180,4 +274,42 @@ def load_boot_logo(source, target, env): # Load the boot logo on TFT builds if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): - env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo) + env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo) + +board_arch = infer_architecture(env.BoardConfig()) +should_skip_manifest = board_arch is None + +# For host/native envs, avoid depending on 'buildprog' (some targets don't define it) +mtjson_deps = [] if should_skip_manifest else ["buildprog"] +if not should_skip_manifest and platform.name == "espressif32": + # Build littlefs image as part of mtjson target + # Equivalent to `pio run -t buildfs` + target_lfs = env.DataToBin( + join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR" + ) + mtjson_deps.append(target_lfs) + +if should_skip_manifest: + def skip_manifest(source, target, env): + print(f"mtjson: skipped for native environment: {env.get('PIOENV')}") + + env.AddCustomTarget( + name="mtjson", + dependencies=mtjson_deps, + actions=[skip_manifest], + title="Meshtastic Manifest (skipped)", + description="mtjson generation is skipped for native environments", + always_build=True, + ) +else: + env.AddCustomTarget( + name="mtjson", + dependencies=mtjson_deps, + actions=[manifest_gather], + title="Meshtastic Manifest", + description="Generating Meshtastic manifest JSON + Checksums", + always_build=True, + ) + + # Run manifest generation as part of the default build pipeline for non-native builds. + env.Default("mtjson") diff --git a/bin/platformio-pre.py b/bin/platformio-pre.py new file mode 100644 index 000000000..16278b813 --- /dev/null +++ b/bin/platformio-pre.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports +Import("env") +platform = env.PioPlatform() + +if platform.name == "native": + env.Replace(PROGNAME="meshtasticd") +else: + from readprops import readProps + prefsLoc = env["PROJECT_DIR"] + "/version.properties" + verObj = readProps(prefsLoc) + env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}") + env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}") + +# Print the new program name for verification +print(f"PROGNAME: {env.get('PROGNAME')}") +if platform.name == "espressif32": + print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}") diff --git a/bin/readprops.py b/bin/readprops.py index 731a3d0d3..4b92d63dd 100644 --- a/bin/readprops.py +++ b/bin/readprops.py @@ -18,8 +18,9 @@ def readProps(prefsLoc): # Try to find current build SHA if if the workspace is clean. This could fail if git is not installed try: + # Pin abbreviation length to keep local builds and CI matching (avoid auto-shortening) sha = ( - subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + subprocess.check_output(["git", "rev-parse", "--short=7", "HEAD"]) .decode("utf-8") .strip() ) diff --git a/bin/shame.py b/bin/shame.py new file mode 100644 index 000000000..f2253bfdc --- /dev/null +++ b/bin/shame.py @@ -0,0 +1,95 @@ +import sys +import os +import json +from github import Github + +def parseFile(path): + with open(path, "r") as f: + data = json.loads(f) + for file in data["files"]: + if file["name"].endswith(".bin"): + return file["name"], file["bytes"] + +if len(sys.argv) != 4: + print(f"expected usage: {sys.argv[0]} ") + sys.exit(1) + +pr_number = int(sys.argv[1]) + +token = os.getenv("GITHUB_TOKEN") +if not token: + raise EnvironmentError("GITHUB_TOKEN not found in environment.") + +repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo" +if not repo_name: + raise EnvironmentError("GITHUB_REPOSITORY not found in environment.") + +oldFiles = sys.argv[2] +old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f)) +newFiles = sys.argv[3] +new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f)) + +startMarkdown = "# Target Size Changes\n\n" +markdown = "" + +newlyIntroduced = new - old +if len(newlyIntroduced) > 0: + markdown += "## Newly Introduced Targets\n\n" + # create a table + markdown += "| File | Size |\n" + markdown += "| ---- | ---- |\n" + for f in newlyIntroduced: + name, size = parseFile(f) + markdown += f"| `{name}` | {size}b |\n" + +# do not log removed targets +# PRs only run a small subset of builds, so removed targets are not meaningful +# since they are very likely to just be not ran in PR CI + +both = old & new +degradations = [] +improvements = [] +for f in both: + oldName, oldSize = parseFile(f) + _, newSize = parseFile(f) + if oldSize != newSize: + if newSize < oldSize: + improvements.append((oldName, oldSize, newSize)) + else: + degradations.append((oldName, oldSize, newSize)) + +if len(degradations) > 0: + markdown += "\n## Degradation\n\n" + # create a table + markdown += "| File | Difference | Old Size | New Size |\n" + markdown += "| ---- | ---------- | -------- | -------- |\n" + for oldName, oldSize, newSize in degradations: + markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" + +if len(improvements) > 0: + markdown += "\n## Improvement\n\n" + # create a table + markdown += "| File | Difference | Old Size | New Size |\n" + markdown += "| ---- | ---------- | -------- | -------- |\n" + for oldName, oldSize, newSize in improvements: + markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n" + +if len(markdown) == 0: + markdown = "No changes in target sizes detected." + +g = Github(token) +repo = g.get_repo(repo_name) +pr = repo.get_pull(pr_number) + +existing_comment = None +for comment in pr.get_issue_comments(): + if comment.body.startswith(startMarkdown): + existing_comment = comment + break + +final_markdown = startMarkdown + markdown + +if existing_comment: + existing_comment.edit(body=final_markdown) +else: + pr.create_issue_comment(body=final_markdown) diff --git a/bin/test-simulator.sh b/bin/test-simulator.sh index 3c5f8f811..92ed21a7a 100755 --- a/bin/test-simulator.sh +++ b/bin/test-simulator.sh @@ -3,7 +3,7 @@ set -e echo "Starting simulator" -.pio/build/native/program & +.pio/build/native/meshtasticd -s & sleep 20 # 5 seconds was not enough echo "Simulator started, launching python test..." diff --git a/boards/hackaday-communicator.json b/boards/hackaday-communicator.json new file mode 100644 index 000000000..6e6c1ad2d --- /dev/null +++ b/boards/hackaday-communicator.json @@ -0,0 +1,41 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "hackaday-communicator" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "hackaday-communicator (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 1500000 + }, + "url": "hackaday.com", + "vendor": "hackaday" +} diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json index 8eac3a9b2..9827be83f 100644 --- a/boards/heltec_v4.json +++ b/boards/heltec_v4.json @@ -9,7 +9,7 @@ "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], diff --git a/boards/t-beam-1w.json b/boards/t-beam-1w.json new file mode 100644 index 000000000..ac1d1f15e --- /dev/null +++ b/boards/t-beam-1w.json @@ -0,0 +1,50 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_1W", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "t-beam-1w" + }, + "connectivity": [ + "wifi", + "bluetooth", + "lora" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "LilyGo TBeam-1W", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 5a0f543eb..5f25d53ad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +meshtasticd (2.7.18.0) unstable; urgency=medium + + * Version 2.7.18 + + -- GitHub Actions Fri, 02 Jan 2026 12:45:36 +0000 + +meshtasticd (2.7.17.0) unstable; urgency=medium + + * Version 2.7.17 + + -- GitHub Actions Fri, 28 Nov 2025 15:11:34 +0000 + meshtasticd (2.7.16.0) unstable; urgency=medium * Version 2.7.16 diff --git a/debian/rules b/debian/rules index 0b5d1ac57..ebb572153 100755 --- a/debian/rules +++ b/debian/rules @@ -28,5 +28,4 @@ override_dh_auto_build: # Build with platformio $(PIO_ENV) platformio run -e native-tft # Move the binary and default config to the correct name - mv .pio/build/native-tft/program .pio/build/native-tft/meshtasticd cp bin/config-dist.yaml bin/config.yaml diff --git a/extra_scripts/disable_adafruit_usb.py b/extra_scripts/disable_adafruit_usb.py index 596242184..3b901e2db 100644 --- a/extra_scripts/disable_adafruit_usb.py +++ b/extra_scripts/disable_adafruit_usb.py @@ -1,10 +1,9 @@ +#!/usr/bin/env python3 # trunk-ignore-all(flake8/F821) # trunk-ignore-all(ruff/F821) Import("env") -# NOTE: This is not currently used, but can serve as an example on how to write extra_scripts - # print("Current CLI targets", COMMAND_LINE_TARGETS) # print("Current Build targets", BUILD_TARGETS) # print("CPP defs", env.get("CPPDEFINES")) diff --git a/extra_scripts/esp32_extra.py b/extra_scripts/esp32_extra.py new file mode 100755 index 000000000..f7698561a --- /dev/null +++ b/extra_scripts/esp32_extra.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports +# trunk-ignore-all(ruff/E402): Hacky esptool import +# trunk-ignore-all(flake8/E402): Hacky esptool import +import sys +from os.path import join + +Import("env") +platform = env.PioPlatform() + +sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) +# IntelHex workaround, remove after fixed upstream +# https://github.com/platformio/platform-espressif32/issues/1632 +try: + import intelhex +except ImportError: + env.Execute("$PYTHONEXE -m pip install intelhex") +import esptool + + +def esp32_create_combined_bin(source, target, env): + # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3 + # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py + print("Generating combined binary for serial flashing") + + app_offset = 0x10000 + + new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") + sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) + firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") + chip = env.get("BOARD_MCU") + board = env.BoardConfig() + flash_size = board.get("upload.flash_size") + flash_freq = board.get("build.f_flash", "40m") + flash_freq = flash_freq.replace("000000L", "m") + flash_mode = board.get("build.flash_mode", "dio") + memory_type = board.get("build.arduino.memory_type", "qio_qspi") + if flash_mode == "qio" or flash_mode == "qout": + flash_mode = "dio" + if memory_type == "opi_opi" or memory_type == "opi_qspi": + flash_mode = "dout" + cmd = [ + "--chip", + chip, + "merge_bin", + "-o", + new_file_name, + "--flash_mode", + flash_mode, + "--flash_freq", + flash_freq, + "--flash_size", + flash_size, + ] + + print(" Offset | File") + for section in sections: + sect_adr, sect_file = section.split(" ", 1) + print(f" - {sect_adr} | {sect_file}") + cmd += [sect_adr, sect_file] + + print(f" - {hex(app_offset)} | {firmware_name}") + cmd += [hex(app_offset), firmware_name] + + print("Using esptool.py arguments: %s" % " ".join(cmd)) + + esptool.main(cmd) + + +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) + +esp32_kind = env.GetProjectOption("custom_esp32_kind") +if esp32_kind == "esp32": + # Free up some IRAM by removing auxiliary SPI flash chip drivers. + # Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c. + env.Append( + LINKFLAGS=[ + "-Wl,--wrap=esp_flash_chip_gd", + "-Wl,--wrap=esp_flash_chip_issi", + "-Wl,--wrap=esp_flash_chip_winbond", + ] + ) +else: + # For newer ESP32 targets, using newlib nano works better. + env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"]) diff --git a/extra_scripts/esp32_pre.py b/extra_scripts/esp32_pre.py new file mode 100755 index 000000000..8e21770e9 --- /dev/null +++ b/extra_scripts/esp32_pre.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports +import json +import sys +from os.path import isfile + +Import("env") + + +# From https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py +def _parse_size(value): + if isinstance(value, int): + return value + elif value.isdigit(): + return int(value) + elif value.startswith("0x"): + return int(value, 16) + elif value[-1].upper() in ("K", "M"): + base = 1024 if value[-1].upper() == "K" else 1024 * 1024 + return int(value[:-1]) * base + return value + + +def _parse_partitions(env): + partitions_csv = env.subst("$PARTITIONS_TABLE_CSV") + if not isfile(partitions_csv): + sys.stderr.write( + "Could not find the file %s with partitions " "table.\n" % partitions_csv + ) + env.Exit(1) + return + + result = [] + # The first offset is 0x9000 because partition table is flashed to 0x8000 and + # occupies an entire flash sector, which size is 0x1000 + next_offset = 0x9000 + with open(partitions_csv) as fp: + for line in fp.readlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + tokens = [t.strip() for t in line.split(",")] + if len(tokens) < 5: + continue + + bound = 0x10000 if tokens[1] in ("0", "app") else 4 + calculated_offset = (next_offset + bound - 1) & ~(bound - 1) + partition = { + "name": tokens[0], + "type": tokens[1], + "subtype": tokens[2], + "offset": tokens[3] or calculated_offset, + "size": tokens[4], + "flags": tokens[5] if len(tokens) > 5 else None, + } + result.append(partition) + next_offset = _parse_size(partition["offset"]) + _parse_size( + partition["size"] + ) + + return result + + +def mtjson_esp32_part(target, source, env): + part = _parse_partitions(env) + pj = json.dumps(part) + # print(f"JSON_PARTITIONS: {pj}") + # Dump json string to 'custom_mtjson_part' variable to use later when writing the manifest + env.Replace(custom_mtjson_part=pj) + + +env.AddPreAction("mtjson", mtjson_esp32_part) diff --git a/extra_scripts/nrf52_extra.py b/extra_scripts/nrf52_extra.py new file mode 100755 index 000000000..8e95e42bf --- /dev/null +++ b/extra_scripts/nrf52_extra.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports + +import sys +from os.path import basename + +Import("env") + + +# Custom HEX from ELF +# Convert hex to uf2 for nrf52 +def nrf52_hex_to_uf2(source, target, env): + hex_path = target[0].get_abspath() + # When using merged hex, drop 'merged' from uf2 filename + uf2_path = hex_path.replace(".merged.", ".") + uf2_path = uf2_path.replace(".hex", ".uf2") + env.Execute( + env.VerboseAction( + f'"{sys.executable}" ./bin/uf2conv.py "{hex_path}" -c -f 0xADA52840 -o "{uf2_path}"', + f"Generating UF2 file from {basename(hex_path)}", + ) + ) + + +def nrf52_mergehex(source, target, env): + hex_path = target[0].get_abspath() + merged_hex_path = hex_path.replace(".hex", ".merged.hex") + merge_with = None + if "wio-sdk-wm1110" == str(env.get("PIOENV")): + merge_with = env.subst("$PROJECT_DIR/bin/s140_nrf52_7.3.0_softdevice.hex") + else: + print("merge_with not defined for this target") + + if merge_with is not None: + env.Execute( + env.VerboseAction( + f'"$PROJECT_DIR/bin/mergehex" -m "{hex_path}" "{merge_with}" -o "{merged_hex_path}"', + "Merging HEX with SoftDevice", + ) + ) + print(f'Merged file saved at "{basename(merged_hex_path)}"') + nrf52_hex_to_uf2([hex_path, merge_with], [env.File(merged_hex_path)], env) + + +# if WM1110 target, merge hex with softdevice 7.3.0 +if "wio-sdk-wm1110" == env.get("PIOENV"): + env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_mergehex) +else: + env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_hex_to_uf2) diff --git a/extra_scripts/extra_stm32.py b/extra_scripts/stm32_extra.py similarity index 95% rename from extra_scripts/extra_stm32.py rename to extra_scripts/stm32_extra.py index f3bd8c514..afceb7d81 100755 --- a/extra_scripts/extra_stm32.py +++ b/extra_scripts/stm32_extra.py @@ -1,7 +1,9 @@ +#!/usr/bin/env python3 # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports Import("env") + # Custom HEX from ELF env.AddPostAction( "$BUILD_DIR/${PROGNAME}.elf", diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index b9152c4a3..3456001f0 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -33,7 +33,6 @@ BuildRequires: python3dist(grpcio[protobuf]) BuildRequires: python3dist(grpcio-tools) BuildRequires: git-core BuildRequires: gcc-c++ -BuildRequires: (glibc-devel >= 2.38) or pkgconfig(libbsd-overlay) BuildRequires: pkgconfig(yaml-cpp) BuildRequires: pkgconfig(libgpiod) BuildRequires: pkgconfig(bluez) @@ -77,7 +76,7 @@ platformio run -e native-tft %install # Install meshtasticd binary mkdir -p %{buildroot}%{_bindir} -install -m 0755 .pio/build/native-tft/program %{buildroot}%{_bindir}/meshtasticd +install -m 0755 .pio/build/native-tft/meshtasticd %{buildroot}%{_bindir}/meshtasticd # Install portduino VFS dir install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd diff --git a/platformio.ini b/platformio.ini index d6ff155e4..11ef72b21 100644 --- a/platformio.ini +++ b/platformio.ini @@ -5,7 +5,7 @@ default_envs = tbeam extra_configs = - arch/*/*.ini + variants/*/*.ini variants/*/*/platformio.ini variants/*/diy/*/platformio.ini src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -14,7 +14,9 @@ description = Meshtastic [env] test_build_src = true -extra_scripts = bin/platformio-custom.py +extra_scripts = + pre:bin/platformio-pre.py + bin/platformio-custom.py ; note: we add src to our include search path so that lmic_project_config can override ; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile ; of code is a heap corruption bug! @@ -62,7 +64,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/2887bf4a19f64d92c984dcc8fd5ca7429e425e4a.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master @@ -90,7 +92,7 @@ framework = arduino lib_deps = ${env.lib_deps} # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL - end2endzone/NonBlockingRTTTL@1.3.0 + end2endzone/NonBlockingRTTTL@1.4.0 build_flags = ${env.build_flags} -Os build_src_filter = ${env.build_src_filter} - - @@ -101,17 +103,13 @@ lib_deps = thingsboard/TBPubSubClient@2.12.1 # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient arduino-libraries/NTPClient@3.2.1 + +; 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 -; Minimal networking libs for nrf52 (excludes Syslog to save flash) -[nrf52_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 - [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib @@ -121,7 +119,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/28167c67dfd13015a0b5eef1828f95fe8e3ab7c3.zip + https://github.com/meshtastic/device-ui/archive/a8e2f947f7abaf0c5ac8e6dd189a22156335beaa.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -160,8 +158,8 @@ lib_deps = 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=INA3221 packageName=sgtwilko/INA3221 - https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a + # 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 @@ -169,7 +167,7 @@ lib_deps = # 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.4 + robtillaart/INA226@0.6.5 # 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 @@ -182,8 +180,8 @@ lib_deps = 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/BH1750_WE@^1.1.10 - wollewald/BH1750_WE@^1.1.10 + # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE + wollewald/BH1750_WE@1.1.10 ; (not included in native / portduino) [environmental_extra] @@ -205,7 +203,7 @@ lib_deps = # 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=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 - ClosedCube OPT3001@1.1.2 + closedcube/ClosedCube OPT3001@1.1.2 # 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 @@ -213,6 +211,6 @@ lib_deps = # 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.1 + sensirion/Sensirion Core@0.7.2 # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x sensirion/Sensirion I2C SCD4x@1.1.0 diff --git a/protobufs b/protobufs index 52fa252f1..f78b3f0dc 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 52fa252f1e01be87ad2f7ab17ceef7882b2a4a93 +Subproject commit f78b3f0dcc078372c90493945155081648605699 diff --git a/src/AudioThread.h b/src/AudioThread.h index df4892b6e..23552c421 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -50,8 +50,11 @@ class AudioThread : public concurrency::OSThread delete i2sRtttl; i2sRtttl = nullptr; } - delete rtttlFile; - rtttlFile = nullptr; + + if (rtttlFile != nullptr) { + delete rtttlFile; + rtttlFile = nullptr; + } setCPUFast(false); #ifdef T_LORA_PAGER @@ -99,9 +102,9 @@ class AudioThread : public concurrency::OSThread }; AudioGeneratorRTTTL *i2sRtttl = nullptr; - AudioOutputI2S *audioOut; + AudioOutputI2S *audioOut = nullptr; - AudioFileSourcePROGMEM *rtttlFile; + AudioFileSourcePROGMEM *rtttlFile = nullptr; }; #endif diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 246cf0022..d88f9fc9f 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -31,6 +31,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: return useShortName ? "LongF" : "LongFast"; break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + return useShortName ? "LongT" : "LongTurbo"; + break; case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: return useShortName ? "LongM" : "LongMod"; break; diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp new file mode 100644 index 000000000..c96645b1c --- /dev/null +++ b/src/MessageStore.cpp @@ -0,0 +1,426 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "FSCommon.h" +#include "MessageStore.h" +#include "NodeDB.h" +#include "SPILock.h" +#include "SafeFile.h" +#include "gps/RTC.h" +#include "graphics/draw/MessageRenderer.h" +#include // memcpy + +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + +// Global message text pool and state +static char *g_messagePool = nullptr; +static size_t g_poolWritePos = 0; + +// Reset pool (called on boot or clear) +static inline void resetMessagePool() +{ + if (!g_messagePool) { + g_messagePool = static_cast(malloc(MESSAGE_TEXT_POOL_SIZE)); + if (!g_messagePool) { + LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); + return; + } + } + g_poolWritePos = 0; + memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); +} + +// Allocate text in pool and return offset +// If not enough space remains, wrap around (ring buffer style) +static inline uint16_t storeTextInPool(const char *src, size_t len) +{ + if (len >= MAX_MESSAGE_SIZE) + len = MAX_MESSAGE_SIZE - 1; + + // Wrap pool if out of space + if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { + g_poolWritePos = 0; + } + + uint16_t offset = g_poolWritePos; + memcpy(&g_messagePool[g_poolWritePos], src, len); + g_messagePool[g_poolWritePos + len] = '\0'; + g_poolWritePos += (len + 1); + return offset; +} + +// Retrieve a const pointer to message text by offset +static inline const char *getTextFromPool(uint16_t offset) +{ + if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE) + return ""; + return &g_messagePool[offset]; +} + +// Helper: assign a timestamp (RTC if available, else boot-relative) +static inline void assignTimestamp(StoredMessage &sm) +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs) { + sm.timestamp = nowSecs; + sm.isBootRelative = false; + } else { + sm.timestamp = millis() / 1000; + sm.isBootRelative = true; + } +} + +// Generic push with cap (used by live + persisted queues) +template static inline void pushWithLimit(std::deque &queue, const T &msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.push_back(msg); +} + +template static inline void pushWithLimit(std::deque &queue, T &&msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.emplace_back(std::move(msg)); +} + +MessageStore::MessageStore(const std::string &label) +{ + filename = "/Messages_" + label + ".msgs"; + resetMessagePool(); // initialize text pool on boot +} + +// Live message handling (RAM only) +void MessageStore::addLiveMessage(StoredMessage &&msg) +{ + pushWithLimit(liveMessages, std::move(msg)); +} +void MessageStore::addLiveMessage(const StoredMessage &msg) +{ + pushWithLimit(liveMessages, msg); +} + +// Add from incoming/outgoing packet +const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) +{ + StoredMessage sm; + assignTimestamp(sm); + sm.channelIndex = packet.channel; + + const char *payload = reinterpret_cast(packet.decoded.payload.bytes); + size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); + sm.textOffset = storeTextInPool(payload, len); + sm.textLength = len; + + // Determine sender + uint32_t localNode = nodeDB->getNodeNum(); + sm.sender = (packet.from == 0) ? localNode : packet.from; + + sm.dest = packet.to; + + bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST); + + if (packet.from == 0) { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::NONE; + } else { + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; + sm.ackStatus = AckStatus::ACKED; + } + + addLiveMessage(sm); + return liveMessages.back(); +} + +// Outgoing/manual message +void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) +{ + StoredMessage sm; + + // Always use our local time (helper handles RTC vs boot time) + assignTimestamp(sm); + + sm.sender = sender; + sm.channelIndex = channelIndex; + sm.textOffset = storeTextInPool(text.c_str(), text.size()); + sm.textLength = text.size(); + + // Use the provided destination + sm.dest = sender; + sm.type = MessageType::DM_TO_US; + + // Outgoing messages always start with unknown ack status + sm.ackStatus = AckStatus::NONE; + + addLiveMessage(sm); +} + +#if ENABLE_MESSAGE_PERSISTENCE + +// Compact, fixed-size on-flash representation using offset + length +struct __attribute__((packed)) StoredMessageRecord { + uint32_t timestamp; + uint32_t sender; + uint8_t channelIndex; + uint32_t dest; + uint8_t isBootRelative; + uint8_t ackStatus; // static_cast(AckStatus) + uint8_t type; // static_cast(MessageType) + uint16_t textLength; // message length + char text[MAX_MESSAGE_SIZE]; // store actual text here +}; + +// Serialize one StoredMessage to flash +static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) +{ + StoredMessageRecord rec = {}; + rec.timestamp = m.timestamp; + rec.sender = m.sender; + rec.channelIndex = m.channelIndex; + rec.dest = m.dest; + rec.isBootRelative = m.isBootRelative; + rec.ackStatus = static_cast(m.ackStatus); + rec.type = static_cast(m.type); + rec.textLength = m.textLength; + + // Copy the actual text into the record from RAM pool + const char *txt = getTextFromPool(m.textOffset); + strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); + rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; + + f.write(reinterpret_cast(&rec), sizeof(rec)); +} + +// Deserialize one StoredMessage from flash; returns false on short read +static inline bool readMessageRecord(File &f, StoredMessage &m) +{ + StoredMessageRecord rec = {}; + if (f.readBytes(reinterpret_cast(&rec), sizeof(rec)) != sizeof(rec)) + return false; + + m.timestamp = rec.timestamp; + m.sender = rec.sender; + m.channelIndex = rec.channelIndex; + m.dest = rec.dest; + m.isBootRelative = rec.isBootRelative; + m.ackStatus = static_cast(rec.ackStatus); + m.type = static_cast(rec.type); + m.textLength = rec.textLength; + + // 💡 Re-store text into pool and update offset + m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); + m.textOffset = storeTextInPool(rec.text, m.textLength); + + return true; +} + +void MessageStore::saveToFlash() +{ +#ifdef FSCom + // Ensure root exists + spiLock->lock(); + FSCom.mkdir("/"); + spiLock->unlock(); + + SafeFile f(filename.c_str(), false); + + spiLock->lock(); + uint8_t count = static_cast(liveMessages.size()); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + f.write(&count, 1); + + for (uint8_t i = 0; i < count; ++i) { + writeMessageRecord(f, liveMessages[i]); + } + spiLock->unlock(); + + f.close(); +#endif +} + +void MessageStore::loadFromFlash() +{ + std::deque().swap(liveMessages); + resetMessagePool(); // reset pool when loading + +#ifdef FSCom + concurrency::LockGuard guard(spiLock); + + if (!FSCom.exists(filename.c_str())) + return; + + auto f = FSCom.open(filename.c_str(), FILE_O_READ); + if (!f) + return; + + uint8_t count = 0; + f.readBytes(reinterpret_cast(&count), 1); + if (count > MAX_MESSAGES_SAVED) + count = MAX_MESSAGES_SAVED; + + for (uint8_t i = 0; i < count; ++i) { + StoredMessage m; + if (!readMessageRecord(f, m)) + break; + liveMessages.push_back(m); + } + + f.close(); +#endif +} + +#else +// If persistence is disabled, these functions become no-ops +void MessageStore::saveToFlash() {} +void MessageStore::loadFromFlash() {} +#endif + +// Clear all messages (RAM + persisted queue) +void MessageStore::clearAllMessages() +{ + std::deque().swap(liveMessages); + resetMessagePool(); + +#ifdef FSCom + SafeFile f(filename.c_str(), false); + uint8_t count = 0; + f.write(&count, 1); // write "0 messages" + f.close(); +#endif +} + +// Internal helper: erase first or last message matching a predicate +template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) +{ + if (fromBack) { + // Iterate from the back and erase all matches from the end + for (auto it = deque.rbegin(); it != deque.rend();) { + if (pred(*it)) { + it = std::deque::reverse_iterator(deque.erase(std::next(it).base())); + } else { + ++it; + } + } + } else { + // Manual forward search to erase all matches + for (auto it = deque.begin(); it != deque.end();) { + if (pred(*it)) { + it = deque.erase(it); + } else { + ++it; + } + } + } +} + +// Delete oldest message (RAM + persisted queue) +void MessageStore::deleteOldestMessage() +{ + eraseIf(liveMessages, [](StoredMessage &) { return true; }); + saveToFlash(); +} + +// Delete oldest message in a specific channel +void MessageStore::deleteOldestMessageInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred); + saveToFlash(); +} + +void MessageStore::deleteAllMessagesInChannel(uint8_t channel) +{ + auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; + eraseIf(liveMessages, pred, false /* delete ALL, not just first */); + saveToFlash(); +} + +void MessageStore::deleteAllMessagesWithPeer(uint32_t peer) +{ + uint32_t local = nodeDB->getNodeNum(); + auto pred = [&](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == local) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred, false); + saveToFlash(); +} + +// Delete oldest message in a direct chat with a node +void MessageStore::deleteOldestMessageWithPeer(uint32_t peer) +{ + auto pred = [peer](const StoredMessage &m) { + if (m.type != MessageType::DM_TO_US) + return false; + uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + return other == peer; + }; + eraseIf(liveMessages, pred); + saveToFlash(); +} + +std::deque MessageStore::getChannelMessages(uint8_t channel) const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { + result.push_back(m); + } + } + return result; +} + +std::deque MessageStore::getDirectMessages() const +{ + std::deque result; + for (const auto &m : liveMessages) { + if (m.type == MessageType::DM_TO_US) { + result.push_back(m); + } + } + return result; +} + +// Upgrade boot-relative timestamps once RTC is valid +// Only same-boot boot-relative messages are healed. +// Persisted boot-relative messages from old boots stay ??? forever. +void MessageStore::upgradeBootRelativeTimestamps() +{ + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + if (nowSecs == 0) + return; // Still no valid RTC + + uint32_t bootNow = millis() / 1000; + + auto fix = [&](std::deque &dq) { + for (auto &m : dq) { + if (m.isBootRelative && m.timestamp <= bootNow) { + uint32_t bootOffset = nowSecs - bootNow; + m.timestamp += bootOffset; + m.isBootRelative = false; + } + } + }; + fix(liveMessages); +} + +const char *MessageStore::getText(const StoredMessage &msg) +{ + // Wrapper around the internal helper + return getTextFromPool(msg.textOffset); +} + +uint16_t MessageStore::storeText(const char *src, size_t len) +{ + // Wrapper around the internal helper + return storeTextInPool(src, len); +} + +// Global definition +MessageStore messageStore("default"); +#endif \ No newline at end of file diff --git a/src/MessageStore.h b/src/MessageStore.h new file mode 100644 index 000000000..41eb56b66 --- /dev/null +++ b/src/MessageStore.h @@ -0,0 +1,131 @@ +#pragma once + +#if HAS_SCREEN + +// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints +#if defined(HELTEC_MESH_SOLAR) +#define LOG_DEBUG(...) +#endif + +// Enable or disable message persistence (flash storage) +// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely +#ifndef ENABLE_MESSAGE_PERSISTENCE +#define ENABLE_MESSAGE_PERSISTENCE 1 +#endif + +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include +#include + +// How many messages are stored (RAM + flash). +// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. +#ifndef MESSAGE_HISTORY_LIMIT +#define MESSAGE_HISTORY_LIMIT 20 +#endif + +// Internal alias used everywhere in code – do NOT redefine elsewhere. +#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT + +// Maximum text payload size per message in bytes. +// This still defines the max message length, but we no longer reserve this space per message. +#define MAX_MESSAGE_SIZE 220 + +// Total shared text pool size for all messages combined. +// The text pool is RAM-only. Text is re-stored from flash into the pool on boot. +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + +// Explicit message classification +enum class MessageType : uint8_t { + BROADCAST = 0, // broadcast message + DM_TO_US = 1 // direct message addressed to this node +}; + +// Delivery status for messages we sent +enum class AckStatus : uint8_t { + NONE = 0, // just sent, waiting (no symbol shown) + ACKED = 1, // got a valid ACK from destination + NACKED = 2, // explicitly failed + TIMEOUT = 3, // no ACK after retry window + RELAYED = 4 // got an ACK from relay, not destination +}; + +struct StoredMessage { + uint32_t timestamp; // When message was created (secs since boot or RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + uint32_t dest; // Destination node (broadcast or direct) + MessageType type; // Derived from dest (explicit classification) + bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute + AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) + + // Text storage metadata — rebuilt from flash at boot + uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) + uint16_t textLength; // Length of text in bytes + + // Default constructor initializes all fields safely + StoredMessage() + : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), + ackStatus(AckStatus::NONE), textOffset(0), textLength(0) + { + } +}; + +class MessageStore +{ + public: + explicit MessageStore(const std::string &label); + + // Live RAM methods (always current, used by UI and runtime) + void addLiveMessage(StoredMessage &&msg); + void addLiveMessage(const StoredMessage &msg); // convenience overload + const std::deque &getLiveMessages() const { return liveMessages; } + + // Add new messages from packets or manual input + const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only + void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add + + // Persistence methods (used only on boot/shutdown) + void saveToFlash(); // Save messages to flash + void loadFromFlash(); // Load messages from flash + + // Clear all messages (RAM + persisted queue + text pool) + void clearAllMessages(); + + // Delete helpers + void deleteOldestMessage(); // remove oldest from RAM (and flash on save) + void deleteOldestMessageInChannel(uint8_t channel); + void deleteOldestMessageWithPeer(uint32_t peer); + void deleteAllMessagesInChannel(uint8_t channel); + void deleteAllMessagesWithPeer(uint32_t peer); + + // Unified accessor (for UI code, defaults to RAM buffer) + const std::deque &getMessages() const { return liveMessages; } + + // Helper filters for future use + std::deque getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel + std::deque getDirectMessages() const; // Only direct messages + + // Upgrade boot-relative timestamps once RTC is valid + void upgradeBootRelativeTimestamps(); + + // Retrieve the C-string text for a stored message + static const char *getText(const StoredMessage &msg); + + // Allocate text into pool (used by sender-side code) + static uint16_t storeText(const char *src, size_t len); + + // Used when loading from flash to rebuild the text pool + static uint16_t rebuildTextFromFlash(const char *src, size_t len); + + private: + std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) + std::string filename; // Flash filename for persistence +}; + +// Global instance (defined in MessageStore.cpp) +extern MessageStore messageStore; + +#endif diff --git a/src/Power.cpp b/src/Power.cpp index 0ac89670f..33dda8e11 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -11,6 +11,7 @@ * For more information, see: https://meshtastic.org/ */ #include "power.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "Throttle.h" @@ -707,7 +708,6 @@ bool Power::setup() []() { power->setIntervalFromNow(0); runASAP = true; - BaseType_t higherWake = 0; }, CHANGE); #endif @@ -717,7 +717,6 @@ bool Power::setup() []() { power->setIntervalFromNow(0); runASAP = true; - BaseType_t higherWake = 0; }, CHANGE); #endif @@ -777,7 +776,7 @@ void Power::shutdown() if (screen) { #ifdef T_DECK_PRO screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button -#elif USE_EINK +#elif defined(USE_EINK) screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen #else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen @@ -788,7 +787,9 @@ void Power::shutdown() playShutdownMelody(); #endif nodeDB->saveToDisk(); - +#if HAS_SCREEN + messageStore.saveToFlash(); +#endif #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 ledOff(PIN_LED1); @@ -1455,7 +1456,7 @@ class LipoCharger : public HasBatteryLevel /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } + virtual bool isVbusIn() override { return PPM->isVbusIn(); } /** * return true if the battery is currently charging diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 322b877ff..9f8097b84 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -57,21 +57,21 @@ static bool isPowered() static void sdsEnter() { - LOG_DEBUG("State: SDS"); + LOG_POWERFSM("State: SDS"); // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); } static void lowBattSDSEnter() { - LOG_DEBUG("State: Lower batt SDS"); + LOG_POWERFSM("State: Lower batt SDS"); doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); } extern Power *power; static void shutdownEnter() { - LOG_DEBUG("State: SHUTDOWN"); + LOG_POWERFSM("State: SHUTDOWN"); shutdownAtMsec = millis(); } @@ -81,7 +81,7 @@ static uint32_t secsSlept; static void lsEnter() { - LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); + LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); if (screen) screen->setOn(false); secsSlept = 0; // How long have we been sleeping this time @@ -155,12 +155,12 @@ static void lsIdle() static void lsExit() { - LOG_INFO("Exit state: LS"); + LOG_POWERFSM("State: lsExit"); } static void nbEnter() { - LOG_DEBUG("State: NB"); + LOG_POWERFSM("State: nbEnter"); if (screen) screen->setOn(false); #ifdef ARCH_ESP32 @@ -173,6 +173,7 @@ static void nbEnter() static void darkEnter() { + LOG_POWERFSM("State: darkEnter"); setBluetoothEnable(true); if (screen) screen->setOn(false); @@ -180,7 +181,7 @@ static void darkEnter() static void serialEnter() { - LOG_DEBUG("State: SERIAL"); + LOG_POWERFSM("State: serialEnter"); setBluetoothEnable(false); if (screen) { screen->setOn(true); @@ -189,13 +190,14 @@ static void serialEnter() static void serialExit() { + LOG_POWERFSM("State: serialExit"); // Turn bluetooth back on when we leave serial stream API setBluetoothEnable(true); } static void powerEnter() { - // LOG_DEBUG("State: POWER"); + LOG_POWERFSM("State: powerEnter"); if (!isPowered()) { // If we got here, we are in the wrong state - we should be in powered, let that state handle things LOG_INFO("Loss of power in Powered"); @@ -210,6 +212,7 @@ static void powerEnter() static void powerIdle() { + // LOG_POWERFSM("State: powerIdle"); // very chatty if (!isPowered()) { // If we got here, we are in the wrong state LOG_INFO("Loss of power in Powered"); @@ -219,14 +222,13 @@ static void powerIdle() static void powerExit() { - if (screen) - screen->setOn(true); + LOG_POWERFSM("State: powerExit"); setBluetoothEnable(true); } static void onEnter() { - LOG_DEBUG("State: ON"); + LOG_POWERFSM("State: onEnter"); if (screen) screen->setOn(true); setBluetoothEnable(true); @@ -234,6 +236,7 @@ static void onEnter() static void onIdle() { + LOG_POWERFSM("State: onIdle"); if (isPowered()) { // If we got here, we are in the wrong state - we should be in powered, let that state handle things powerFSM.trigger(EVENT_POWER_CONNECTED); @@ -242,7 +245,7 @@ static void onIdle() static void bootEnter() { - LOG_DEBUG("State: BOOT"); + LOG_POWERFSM("State: bootEnter"); } State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); @@ -319,11 +322,6 @@ void PowerFSM_setup() // if any packet destined for phone arrives, turn on bluetooth at least powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // Removed 2.7: we don't show the nodes individually for every node on the screen anymore - // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - // Show the received text message powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); @@ -372,7 +370,7 @@ void PowerFSM_setup() // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated // through the modules -#if HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI) +#if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI) bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 6330a5fc6..182ac082a 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -2,6 +2,12 @@ #include "configuration.h" +#ifdef PowerFSMDebug +#define LOG_POWERFSM(...) LOG_DEBUG(__VA_ARGS__) +#else +#define LOG_POWERFSM(...) +#endif + // See sw-design.md for documentation #define EVENT_PRESS 1 diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 9624a4593..895dcb147 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -131,6 +131,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format, int hour = hms / SEC_PER_HOUR; int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + #ifdef ARCH_PORTDUINO ::printf("%s ", logLevel); if (color) { diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 7de6c0740..0716dedd0 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -24,6 +24,9 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) switch (event->inputEvent) { case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: + playClick(); // Low delay feedback + break; + case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback @@ -58,4 +61,4 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) } return 0; // Allow other handlers to process the event -} \ No newline at end of file +} diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index b0d162a44..00ad71031 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -16,6 +16,7 @@ struct ToneDuration { }; // Some common frequencies. +#define NOTE_SILENT 1 #define NOTE_C3 131 #define NOTE_CS3 139 #define NOTE_D3 147 @@ -29,11 +30,16 @@ struct ToneDuration { #define NOTE_AS3 233 #define NOTE_B3 247 #define NOTE_CS4 277 +#define NOTE_B4 494 +#define NOTE_F5 698 +#define NOTE_G6 1568 +#define NOTE_E7 2637 +const int DURATION_1_16 = 62; // 1/16 note const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_4 = 250; // 1/4 note const int DURATION_1_2 = 500; // 1/2 note -const int DURATION_3_4 = 750; // 1/4 note +const int DURATION_3_4 = 750; // 3/4 note const int DURATION_1_1 = 1000; // 1/1 note void playTones(const ToneDuration *tone_durations, int size) @@ -71,13 +77,24 @@ void playLongBeep() void playGPSEnableBeep() { +#if defined(R1_NEO) || defined(MUZI_BASE) + ToneDuration melody[] = { + {NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}}; +#else ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; +#endif playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } void playGPSDisableBeep() { +#if defined(R1_NEO) || defined(MUZI_BASE) + ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, + {NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8}, + {NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}}; +#else ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; +#endif playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } @@ -96,7 +113,14 @@ void playShutdownMelody() void playChirp() { // A short, friendly "chirp" sound for key presses - ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playClick() +{ + // A very short "click" sound with minimum delay; ideal for rotary encoder events + ToneDuration melody[] = {{NOTE_AS3, 1}}; // Very Short AS3 playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index c25a54a5b..fa0c8c496 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -9,6 +9,7 @@ void playGPSDisableBeep(); void playComboTune(); void playBoop(); void playChirp(); +void playClick(); void playLongPressLeadUp(); bool playNextLeadUpNote(); // Play the next note in the lead-up sequence void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index d37269995..ec1b9acc2 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -29,13 +29,36 @@ along with this program. If not, see . #if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#if __has_include("pcf8563.h") -#include "pcf8563.h" +#if __has_include("SensorRtcHelper.hpp") +#include "SensorRtcHelper.hpp" #endif /* Offer chance for variant-specific defines */ #include "variant.h" +// ----------------------------------------------------------------------------- +// Display feature overrides +// ----------------------------------------------------------------------------- + +// Allow build environments to opt-in explicitly to the E-Ink UI stack while +// keeping headless targets slim by default. Existing variants that already +// define USE_EINK continue to work without additional flags. +#ifndef MESHTASTIC_USE_EINK_UI +#ifdef USE_EINK +#define MESHTASTIC_USE_EINK_UI 1 +#else +#define MESHTASTIC_USE_EINK_UI 0 +#endif +#endif + +#if MESHTASTIC_USE_EINK_UI +#ifndef USE_EINK +#define USE_EINK +#endif +#else +#undef USE_EINK +#endif + // ----------------------------------------------------------------------------- // Version // ----------------------------------------------------------------------------- @@ -250,8 +273,9 @@ along with this program. If not, see . // Touchscreen // ----------------------------------------------------------------------------- #define FT6336U_ADDR 0x48 -#define CST328_ADDR 0x1A +#define CST328_ADDR 0x1A // same address as CST226SE #define CHSC6X_ADDR 0x2E +#define CST226SE_ADDR_ALT 0x5A // ----------------------------------------------------------------------------- // RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) @@ -370,6 +394,9 @@ along with this program. If not, see . #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 0 #endif +#ifndef USE_TFTDISPLAY +#define USE_TFTDISPLAY 0 +#endif #ifndef HW_VENDOR #error HW_VENDOR must be defined @@ -417,6 +444,18 @@ along with this program. If not, see . #endif #endif +// BME680 BSEC2 support detection +#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED) +#if defined(RAK_4631) || defined(TBEAM_V10) + +#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1 +#define MESHTASTIC_BME680_HEADER +#else +#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0 +#define MESHTASTIC_BME680_HEADER +#endif // defined(RAK_4631) +#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED) + // ----------------------------------------------------------------------------- // Global switches to turn off features for a minimized build // ----------------------------------------------------------------------------- diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 8ac503b83..83a455de7 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const ScanI2C::FoundDevice ScanI2C::firstRTC() const { - ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE}; - return firstOfOrNONE(3, types); + ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE}; + return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstKeyboard() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 55980face..3a79d97c5 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -14,6 +14,7 @@ class ScanI2C SCREEN_ST7567, RTC_RV3028, RTC_PCF8563, + RTC_PCF85063, RTC_RX8130CE, CARDKB, TDECKKB, @@ -85,7 +86,8 @@ class ScanI2C DRV2605, BH1750, DA217, - CHSC6X + CHSC6X, + CST226SE } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index bcf49286e..83acfdc18 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -202,6 +202,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address) #endif +#ifdef PCF85063_RTC + SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address) +#endif + case CARDKB_ADDR: // Do we have the RAK14006 instead? registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1); @@ -483,7 +487,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; case TSL25911_ADDR: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xA0 | 0x12), 1); if (registerValue == 0x50) { type = TSL2591; logFoundDevice("TSL25911", (uint8_t)addr.address); @@ -499,7 +503,18 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(CST328_ADDR, CST328, "CST328", (uint8_t)addr.address); + case CST328_ADDR: + // Do we have the CST328 or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); + if (registerValue == 0xA9) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else { + type = CST328; + logFoundDevice("CST328", (uint8_t)addr.address); + } + break; + SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); case LTR553ALS_ADDR: registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register @@ -528,8 +543,12 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) #endif case MLX90614_ADDR_DEF: - registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1); - if (registerValue == 0x5a) { + // Do we have the MLX90614 or the MPR121KB or the CST226SE + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); + if (registerValue == 0xAB) { + type = CST226SE; + logFoundDevice("CST226SE", (uint8_t)addr.address); + } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) { type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 1122f0a51..25cd3ceff 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -66,26 +66,26 @@ RTCSetResult readFromRTC() currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } -#elif defined(PCF8563_RTC) +#elif defined(PCF8563_RTC) || defined(PCF85063_RTC) +#if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { +#elif defined(PCF85063_RTC) + if (rtc_found.address == PCF85063_RTC) { +#endif uint32_t now = millis(); - PCF8563_Class rtc; + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(); + rtc.begin(Wire); #endif - auto tc = rtc.getDateTime(); - tm t; - t.tm_year = tc.year - 1900; - t.tm_mon = tc.month - 1; - t.tm_mday = tc.day; - t.tm_hour = tc.hour; - t.tm_min = tc.minute; - t.tm_sec = tc.second; + RTC_DateTime datetime = rtc.getDateTime(); + tm t = datetime.toUnixTime(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms @@ -100,14 +100,16 @@ RTCSetResult readFromRTC() } #endif - LOG_DEBUG("Read RTC time from PCF8563 getDateTime 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); + 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) { timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { @@ -232,20 +234,28 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); LOG_DEBUG("RV3028_RTC setTime %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); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } -#elif defined(PCF8563_RTC) +#elif defined(PCF8563_RTC) || defined(PCF85063_RTC) +#if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { - PCF8563_Class rtc; +#elif defined(PCF85063_RTC) + if (rtc_found.address == PCF85063_RTC) { +#endif + SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); #else - rtc.begin(); + rtc.begin(Wire); #endif tm *t = gmtime(&tv->tv_sec); - rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); - LOG_DEBUG("PCF8563_RTC setDateTime %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); + rtc.setDateTime(*t); + LOG_DEBUG("%s setDateTime %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); + } else { + LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address); } #elif defined(RX8130CE_RTC) if (rtc_found.address == RX8130CE_RTC) { diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index dc9806156..0012aeb5d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -46,6 +46,7 @@ along with this program. If not, see . #endif #include "FSCommon.h" #include "MeshService.h" +#include "MessageStore.h" #include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" @@ -64,12 +65,13 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" +extern MessageStore messageStore; -using graphics::Emote; -using graphics::emotes; -using graphics::numEmotes; - +#if USE_TFTDISPLAY extern uint16_t TFT_MESH; +#else +uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94); +#endif #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -115,10 +117,6 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; // we'll need to hold onto pointers for the modules that can draw a frame. std::vector moduleFrames; -// Global variables for screen function overlay symbols -std::vector functionSymbol; -std::string functionSymbolString; - #if HAS_GPS // GeoCoord object for the screen GeoCoord geoCoord; @@ -226,24 +224,9 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t { LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - if (NotificationRenderer::virtualKeyboard) { - delete NotificationRenderer::virtualKeyboard; - NotificationRenderer::virtualKeyboard = nullptr; - } - - NotificationRenderer::textInputCallback = nullptr; - - NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); - if (header) { - NotificationRenderer::virtualKeyboard->setHeader(header); - } - if (initialText) { - NotificationRenderer::virtualKeyboard->setInputText(initialText); - } - - // Set up callback with safer cleanup mechanism + // Start OnScreenKeyboardModule session (non-touch variant) + OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); NotificationRenderer::textInputCallback = textCallback; - NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); // Store the message and set the expiration timestamp (use same pattern as other notifications) strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); @@ -274,19 +257,11 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int } else { // otherwise, just display the module frame that's aligned with the current frame module_frame = state->currentFrame; - // LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame); } - // LOG_DEBUG("Draw Module Frame %d", module_frame); MeshModule &pi = *moduleFrames.at(module_frame); pi.drawFrame(display, state, x, y); } -// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled -static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) -{ - return packet->from != 0 && !moduleConfig.store_forward.enabled; -} - /** * Given a recent lat/lon return a guess of the heading the user is walking on. * @@ -324,7 +299,7 @@ static int8_t prevFrame = -1; // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Uses a single frame and changes data every few seconds (E-Ink variant is separate) -#if defined(ESP_PLATFORM) && defined(USE_ST7789) +#if defined(ESP_PLATFORM) && (defined(USE_ST7789) || defined(USE_ST7796)) SPIClass SPI1(HSPI); #endif @@ -333,16 +308,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O { graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); int32_t rawRGB = uiconfig.screen_rgb_color; - if (rawRGB > 0 && rawRGB <= 255255255) { - uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF; - uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF; - uint8_t TFT_MESH_b = rawRGB & 0xFF; - LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); - if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) { - TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + // Only validate the combined value once + if (rawRGB > 0 && rawRGB <= 255255255) { + // Extract each component as a normal int first + int r = (rawRGB >> 16) & 0xFF; + int g = (rawRGB >> 8) & 0xFF; + int b = rawRGB & 0xFF; + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { + TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } } @@ -356,7 +331,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); #endif - static_cast(dispdev)->setRGB(TFT_MESH); +#elif defined(USE_ST7796) +#ifdef ESP_PLATFORM + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA, + ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY); +#else + dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); +#endif #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); @@ -369,7 +350,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O LOG_INFO("SSD1306 init success"); } #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) + 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) @@ -399,6 +380,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O isAUTOOled = true; #endif +#if defined(USE_ST7789) + static_cast(dispdev)->setRGB(TFT_MESH); +#elif defined(USE_ST7796) + static_cast(dispdev)->setRGB(TFT_MESH); +#endif + ui = new OLEDDisplayUi(dispdev); cmdQueue.setReader(this); } @@ -474,6 +461,15 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) pinMode(VTFT_LEDA, OUTPUT); digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); #endif +#endif +#ifdef USE_ST7796 + ui->init(); +#ifdef ESP_PLATFORM + analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); +#else + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); +#endif #endif enabled = true; setInterval(0); // Draw ASAP @@ -512,6 +508,21 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) nrf_gpio_cfg_default(ST7789_NSS); #endif #endif +#ifdef USE_ST7796 + SPI1.end(); +#if defined(ARCH_ESP32) + pinMode(VTFT_LEDA, OUTPUT); + digitalWrite(VTFT_LEDA, LOW); + pinMode(ST7796_RESET, ANALOG); + pinMode(ST7796_RS, ANALOG); + pinMode(ST7796_NSS, ANALOG); +#else + nrf_gpio_cfg_default(VTFT_LEDA); + nrf_gpio_cfg_default(ST7796_RESET); + nrf_gpio_cfg_default(ST7796_RS); + nrf_gpio_cfg_default(ST7796_NSS); +#endif +#endif #ifdef T_WATCH_S3 PMU->disablePowerOutput(XPOWERS_ALDO2); @@ -525,10 +536,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) void Screen::setup() { - // === Enable display rendering === + // Enable display rendering useDisplay = true; - // === Load saved brightness from UI config === + // Load saved brightness from UI config // For OLED displays (SSD1306), default brightness is 255 if not set if (uiconfig.screen_brightness == 0) { #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) @@ -540,7 +551,7 @@ void Screen::setup() brightness = uiconfig.screen_brightness; } - // === Detect OLED subtype (if supported by board variant) === + // Detect OLED subtype (if supported by board variant) #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -557,8 +568,12 @@ void Screen::setup() #if defined(MUZI_BASE) dispdev->delayPoweron = true; #endif +#if defined(USE_ST7796) && defined(TFT_MESH) + // Custom text color, if defined in variant.h + static_cast(dispdev)->setRGB(TFT_MESH); +#endif - // === Initialize display and UI system === + // Initialize display and UI system ui->init(); displayWidth = dispdev->width(); displayHeight = dispdev->height(); @@ -570,7 +585,7 @@ void Screen::setup() ui->disableAllIndicators(); // Disable page indicator dots ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - // === Apply loaded brightness === + // Apply loaded brightness #if defined(ST7789_CS) static_cast(dispdev)->setDisplayBrightness(brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) @@ -578,20 +593,20 @@ void Screen::setup() #endif LOG_INFO("Applied screen brightness: %d", brightness); - // === Set custom overlay callbacks === + // Set custom overlay callbacks static OverlayCallback overlays[] = { graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // === Enable UTF-8 to display mapping === + // Enable UTF-8 to display mapping dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT logo_timeout *= 2; // Give more time for branded boot logos #endif - // === Configure alert frames (e.g., "Resuming..." or region name) === + // Configure alert frames (e.g., "Resuming..." or region name) EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 @@ -607,26 +622,28 @@ void Screen::setup() ui->setFrames(alertFrames, 1); ui->disableAutoTransition(); // Require manual navigation between frames - // === Log buffer for on-screen logs (3 lines max) === + // Log buffer for on-screen logs (3 lines max) dispdev->setLogBuffer(3, 32); - // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) === + // Optional screen mirroring or flipping (e.g. for T-Beam orientation) #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); +#elif defined(USE_ST7796) + static_cast(dispdev)->mirrorScreen(); #elif !defined(M5STACK_UNITC6L) dispdev->flipScreenVertically(); #endif } #endif - // === Generate device ID from MAC address === + // Generate device ID from MAC address uint8_t dmac[6]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); @@ -635,9 +652,9 @@ void Screen::setup() handleSetOn(false); // Ensure proper init for Arduino targets #endif - // === Turn on display and trigger first draw === + // Turn on display and trigger first draw handleSetOn(true); - determineResolution(dispdev->height(), dispdev->width()); + graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width()); ui->update(); #ifndef USE_EINK ui->update(); // Some SSD1306 clones drop the first draw, so run twice @@ -652,13 +669,13 @@ void Screen::setup() touchScreenImpl1->init(); } } -#elif HAS_TOUCHSCREEN && !defined(USE_EINK) +#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE touchScreenImpl1 = new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); #endif - // === Subscribe to device status updates === + // Subscribe to device status updates powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); @@ -666,12 +683,14 @@ void Screen::setup() #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif - if (textMessageModule) - textMessageObserver.observe(textMessageModule); if (inputBroker) inputObserver.observe(inputBroker); - // === Notify modules that support UI events === + // Load persisted messages into RAM + messageStore.loadFromFlash(); + LOG_INFO("MessageStore loaded from flash"); + + // Notify modules that support UI events MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -742,6 +761,23 @@ int32_t Screen::runOnce() if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } + + // Detect frame transitions and clear message cache when leaving text message screen + { + static int8_t lastFrameIndex = -1; + int8_t currentFrameIndex = ui->getUiState()->currentFrame; + int8_t textMsgIndex = framesetInfo.positions.textMessage; + + if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) { + + if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) { + graphics::MessageRenderer::clearMessageCache(); + } + } + + lastFrameIndex = currentFrameIndex; + } + menuHandler::handleMenuSwitch(dispdev); // Show boot screen for first logo_timeout seconds, then switch to normal operation. @@ -797,17 +833,17 @@ int32_t Screen::runOnce() break; case Cmd::ON_PRESS: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); + showFrame(FrameDirection::NEXT); } break; case Cmd::SHOW_PREV_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } break; case Cmd::SHOW_NEXT_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); + showFrame(FrameDirection::NEXT); } break; case Cmd::START_ALERT_FRAME: { @@ -828,6 +864,7 @@ int32_t Screen::runOnce() break; case Cmd::STOP_ALERT_FRAME: NotificationRenderer::pauseBanner = false; + break; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { @@ -998,9 +1035,6 @@ void Screen::setFrames(FrameFocus focus) } #endif - // Declare this early so it’s available in FOCUS_PRESERVE block - bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - if (!hiddenFrames.home) { fsi.positions.home = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; @@ -1012,11 +1046,16 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist) { - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + if (!hiddenFrames.nodelist_nodes) { + fsi.positions.nodelist_nodes = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; indicatorIcons.push_back(icon_nodes); } + if (!hiddenFrames.nodelist_location) { + fsi.positions.nodelist_location = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location; + indicatorIcons.push_back(icon_list); + } #endif // Show detailed node views only on E-Ink builds @@ -1038,11 +1077,13 @@ void Screen::setFrames(FrameFocus focus) } #endif #if HAS_GPS +#ifdef USE_EINK if (!hiddenFrames.nodelist_bearings) { fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); } +#endif if (!hiddenFrames.gps) { fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; @@ -1142,7 +1183,7 @@ void Screen::setFrames(FrameFocus focus) } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE - this->frameCount = numframes; // ✅ Save frame count for use in custom overlay + this->frameCount = numframes; // Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); @@ -1162,10 +1203,6 @@ void Screen::setFrames(FrameFocus focus) case FOCUS_FAULT: ui->switchToFrame(fsi.positions.fault); break; - case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // ✅ Clear when message is *viewed* - ui->switchToFrame(fsi.positions.textMessage); - break; case FOCUS_MODULE: // Whichever frame was marked by MeshModule::requestFocus(), if any // If no module requested focus, will show the first frame instead @@ -1208,8 +1245,11 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) void Screen::toggleFrameVisibility(const std::string &frameName) { #ifndef USE_EINK - if (frameName == "nodelist") { - hiddenFrames.nodelist = !hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") { + hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; + } + if (frameName == "nodelist_location") { + hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location; } #endif #ifdef USE_EINK @@ -1224,9 +1264,11 @@ void Screen::toggleFrameVisibility(const std::string &frameName) } #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") { hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; } +#endif if (frameName == "gps") { hiddenFrames.gps = !hiddenFrames.gps; } @@ -1248,8 +1290,10 @@ void Screen::toggleFrameVisibility(const std::string &frameName) bool Screen::isFrameHidden(const std::string &frameName) const { #ifndef USE_EINK - if (frameName == "nodelist") - return hiddenFrames.nodelist; + if (frameName == "nodelist_nodes") + return hiddenFrames.nodelist_nodes; + if (frameName == "nodelist_location") + return hiddenFrames.nodelist_location; #endif #ifdef USE_EINK if (frameName == "nodelist_lastheard") @@ -1260,8 +1304,10 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.nodelist_distance; #endif #if HAS_GPS +#ifdef USE_EINK if (frameName == "nodelist_bearings") return hiddenFrames.nodelist_bearings; +#endif if (frameName == "gps") return hiddenFrames.gps; #endif @@ -1277,37 +1323,6 @@ bool Screen::isFrameHidden(const std::string &frameName) const return false; } -// Dismisses the currently displayed screen frame, if possible -// Relevant for text message, waypoint, others in future? -// Triggered with a CardKB keycombo -void Screen::hideCurrentFrame() -{ - uint8_t currentFrame = ui->getUiState()->currentFrame; - bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Hide Text Message"); - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Hide Waypoint"); - devicestate.has_rx_waypoint = false; - hiddenFrames.waypoint = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Hide WiFi Screen"); - hiddenFrames.wifi = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.lora) { - LOG_INFO("Hide LoRa"); - hiddenFrames.lora = true; - dismissed = true; - } - - if (dismissed) { - setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE - } -} - void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("Show firmware screen"); @@ -1360,28 +1375,6 @@ void Screen::decreaseBrightness() /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ } -void Screen::setFunctionSymbol(std::string sym) -{ - if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { - functionSymbol.push_back(sym); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); - } -} - -void Screen::removeFunctionSymbol(std::string sym) -{ - functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); -} - void Screen::handleOnPress() { // If screen was off, just wake it, otherwise advance to next frame @@ -1393,23 +1386,17 @@ void Screen::handleOnPress() } } -void Screen::handleShowPrevFrame() +void Screen::showFrame(FrameDirection direction) { - // If screen was off, just wake it, otherwise go back to previous frame - // If we are in a transition, the press must have bounced, drop it. + // Only advance frames when UI is stable if (ui->getUiState()->frameState == FIXED) { - ui->previousFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } -} -void Screen::handleShowNextFrame() -{ - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); + if (direction == FrameDirection::NEXT) { + ui->nextFrame(); + } else { + ui->previousFrame(); + } + lastScreenTransition = millis(); setFastFramerate(); } @@ -1435,7 +1422,6 @@ void Screen::setFastFramerate() int Screen::handleStatusUpdate(const meshtastic::Status *arg) { - // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); switch (arg->getStatusType()) { case STATUS_TYPE_NODE: if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { @@ -1467,14 +1453,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Incoming message devicestate.has_rx_text_message = true; // Needed to include the message frame hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) // Only wake/force display if the configuration allows it if (shouldWakeOnReceivedMessage()) { setOn(true); // Wake up the screen first forceDisplay(); // Forces screen redraw } - // === Prepare banner content === + // === Prepare banner/popup content === const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_Channel channel = channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); @@ -1498,38 +1484,84 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any // 'mute' preferences set to any specific node or channel. - if (isAlert) { - if (longName && longName[0]) { - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - } else { - strcpy(banner, "Alert Received"); + // If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + // Wake and force redraw so popup is visible immediately + if (shouldWakeOnReceivedMessage()) { + setOn(true); + forceDisplay(); } - screen->showSimpleBanner(banner, 3000); - } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { - if (longName && longName[0]) { -#if defined(M5STACK_UNITC6L) - strcpy(banner, "New Message"); -#else - snprintf(banner, sizeof(banner), "New Message from\n%s", longName); -#endif + // Build popup: title = message source name, content = message text (sanitized) + // Title + char titleBuf[64] = {0}; + if (longName && longName[0]) { + // Sanitize sender name + std::string t = sanitizeString(longName); + strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1); } else { - strcpy(banner, "New Message"); + strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); } + + // Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize + char content[256] = {0}; + { + std::string raw; + raw.reserve(packet->decoded.payload.size); + for (size_t i = 0; i < packet->decoded.payload.size; ++i) { + char c = msgRaw[i]; + if (c == ASCII_BELL) + continue; // strip bell + raw.push_back(c); + } + std::string sanitized = sanitizeString(raw); + strncpy(content, sanitized.c_str(), sizeof(content) - 1); + } + + NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000); + +// Maintain existing buzzer behavior on M5 if applicable #if defined(M5STACK_UNITC6L) - screen->setOn(true); - screen->showSimpleBanner(banner, 1500); if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (!isBroadcast(packet->to) && isToUs(packet))) { - // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either - // - packet contains an alert and alert bell buzzer is enabled - // - packet is a non-broadcast that is addressed to this node playLongBeep(); } -#else - screen->showSimpleBanner(banner, 3000); #endif + } else { + // No keyboard active: use regular banner flow, respecting mute settings + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + } else { + strcpy(banner, "Alert Received"); + } + screen->showSimpleBanner(banner, 3000); + } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) { + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else { + strcpy(banner, "New Message"); + } +#if defined(M5STACK_UNITC6L) + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || + (isAlert && moduleConfig.external_notification.alert_bell_buzzer) || + (!isBroadcast(packet->to) && isToUs(packet))) { + // Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either + // - packet contains an alert and alert bell buzzer is enabled + // - packet is a non-broadcast that is addressed to this node + playLongBeep(); + } +#else + screen->showSimpleBanner(banner, 3000); +#endif + } } } } @@ -1547,16 +1579,26 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { setFrames(FOCUS_MODULE); + } - // Regenerate the frameset, while Attempt to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) + // Regenerate the frameset, while attempting to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { setFrames(FOCUS_PRESERVE); + } // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { setFastFramerate(); + } + + // Jump directly to the Text Message screen + else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) { + setFrames(FOCUS_PRESERVE); // preserve current frame ordering + ui->switchToFrame(framesetInfo.positions.textMessage); + setFastFramerate(); // force redraw ASAP + } } return 0; @@ -1564,6 +1606,7 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { + LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar); if (!screenOn) return 0; @@ -1593,7 +1636,48 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::handleMenuSwitch(dispdev); return 0; } + // UP/DOWN in message screen scrolls through message threads + if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (event->inputEvent == INPUT_BROKER_UP) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollUp(); + setFastFramerate(); // match existing behavior + return 0; + } + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + if (messageStore.getMessages().empty()) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else { + graphics::MessageRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } + } + // UP/DOWN in node list screens scrolls through node pages + if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + if (event->inputEvent == INPUT_BROKER_UP) { + graphics::NodeListRenderer::scrollUp(); + setFastFramerate(); + return 0; + } + + if (event->inputEvent == INPUT_BROKER_DOWN) { + graphics::NodeListRenderer::scrollDown(); + setFastFramerate(); + return 0; + } + } // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose if (showingNormalScreen) { @@ -1607,10 +1691,39 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { +#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2 + bool handledEncoderScroll = false; + const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 && + this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && + !messageStore.getMessages().empty()); + if (isTextMessageFrame) { + if (event->inputEvent == INPUT_BROKER_UP_LONG) { + graphics::MessageRenderer::nudgeScroll(-1); + handledEncoderScroll = true; + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + graphics::MessageRenderer::nudgeScroll(1); + handledEncoderScroll = true; + } + } + + if (handledEncoderScroll) { + setFastFramerate(); + return 0; + } +#endif if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { + showFrame(FrameDirection::NEXT); + } else if (event->inputEvent == INPUT_BROKER_UP_LONG) { + // Long press up button for fast frame switching + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { + // Long press down button for fast frame switching showNextFrame(); + } else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) && + this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); @@ -1625,20 +1738,21 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (devicestate.rx_text_message.from) { + if (!messageStore.getMessages().empty()) { menuHandler::messageResponseMenu(); } else { -#if defined(M5STACK_UNITC6L) - menuHandler::textMessageMenu(); -#else - menuHandler::textMessageBaseMenu(); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + menuHandler::textMessageMenu(); + } else { + menuHandler::textMessageBaseMenu(); + } } } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { menuHandler::favoriteBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || @@ -1649,7 +1763,7 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::wifiBaseMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_CANCEL) { setOn(false); } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 375bc2805..4bb808970 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -40,7 +40,6 @@ class Screen FOCUS_DEFAULT, // No specific frame FOCUS_PRESERVE, // Return to the previous frame FOCUS_FAULT, - FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, FOCUS_SYSTEM, @@ -55,8 +54,6 @@ class Screen void startFirmwareUpdateScreen() {} void increaseBrightness() {} void decreaseBrightness() {} - void setFunctionSymbol(std::string) {} - void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} void showOverlayBanner(BannerOverlayOptions) {} @@ -83,6 +80,8 @@ class Screen #include #elif defined(USE_SPISSD1306) #include +#elif defined(USE_ST7796) +#include #else // the SH1106/SSD1306 variant is auto-detected #include @@ -170,6 +169,8 @@ class Point namespace graphics { +enum class FrameDirection { NEXT, PREVIOUS }; + // Forward declarations class Screen; @@ -209,8 +210,6 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleStatusUpdate); CallbackObserver nodeStatusObserver = CallbackObserver(this, &Screen::handleStatusUpdate); - CallbackObserver textMessageObserver = - CallbackObserver(this, &Screen::handleTextMessage); CallbackObserver uiFrameEventObserver = CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = @@ -221,6 +220,10 @@ class Screen : public concurrency::OSThread public: OLEDDisplay *getDisplayDevice() { return dispdev; } explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); + + // Screen dimension accessors + inline int getHeight() const { return displayHeight; } + inline int getWidth() const { return displayWidth; } size_t frameCount = 0; // Total number of active frames ~Screen(); @@ -229,7 +232,6 @@ class Screen : public concurrency::OSThread FOCUS_DEFAULT, // No specific frame FOCUS_PRESERVE, // Return to the previous frame FOCUS_FAULT, - FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_CLOCK, FOCUS_SYSTEM, @@ -277,6 +279,7 @@ class Screen : public concurrency::OSThread void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } + void showFrame(FrameDirection direction); // generic alert start void startAlert(FrameCallback _alertFrame) @@ -344,9 +347,6 @@ class Screen : public concurrency::OSThread void increaseBrightness(); void decreaseBrightness(); - void setFunctionSymbol(std::string sym); - void removeFunctionSymbol(std::string sym); - /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } @@ -577,7 +577,7 @@ class Screen : public concurrency::OSThread // Handle observer events int handleStatusUpdate(const meshtastic::Status *arg); - int handleTextMessage(const meshtastic_MeshPacket *arg); + int handleTextMessage(const meshtastic_MeshPacket *packet); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); int handleAdminMessage(AdminModule_ObserverData *arg); @@ -588,9 +588,6 @@ class Screen : public concurrency::OSThread /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); - // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) - void hideCurrentFrame(); - // Menu-driven Show / Hide Toggle void toggleFrameVisibility(const std::string &frameName); bool isFrameHidden(const std::string &frameName) const; @@ -638,8 +635,6 @@ class Screen : public concurrency::OSThread // Implementations of various commands, called from doTask(). void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); void handleOnPress(); - void handleShowNextFrame(); - void handleShowPrevFrame(); void handleStartFirmwareUpdateScreen(); // Info collected by setFrames method. @@ -659,7 +654,8 @@ class Screen : public concurrency::OSThread uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; - uint8_t nodelist = 255; + uint8_t nodelist_nodes = 255; + uint8_t nodelist_location = 255; uint8_t nodelist_lastheard = 255; uint8_t nodelist_hopsignal = 255; uint8_t nodelist_distance = 255; @@ -682,7 +678,8 @@ class Screen : public concurrency::OSThread bool home = false; bool clock = false; #ifndef USE_EINK - bool nodelist = false; + bool nodelist_nodes = false; + bool nodelist_location = false; #endif #ifdef USE_EINK bool nodelist_lastheard = false; @@ -690,7 +687,9 @@ class Screen : public concurrency::OSThread bool nodelist_distance = false; #endif #if HAS_GPS +#ifdef USE_EINK bool nodelist_bearings = false; +#endif bool gps = false; #endif bool lora = false; diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index c497a27b2..d54fc9958 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -73,7 +73,8 @@ #endif #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(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 1645789a7..8f06fcf9f 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -8,6 +8,7 @@ #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include #include @@ -15,26 +16,48 @@ namespace graphics { -void determineResolution(int16_t screenheight, int16_t screenwidth) +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) { - if (screenwidth > 128) { - isHighResolution = true; + +#ifdef FORCE_LOW_RES + return ScreenResolution::Low; +#else + // Unit C6L and other ultra low res screens + if (screenwidth <= 64 || screenheight <= 48) { + return ScreenResolution::UltraLow; } + // Standard OLED screens if (screenwidth > 128 && screenheight <= 64) { - isHighResolution = false; + return ScreenResolution::Low; } - // Special case for Heltec Wireless Tracker v1.1 - if (screenwidth == 160 && screenheight == 80) { - isHighResolution = false; + // High Resolutions screens like T114, TDeck, TLora Pager, etc + if (screenwidth > 128) { + return ScreenResolution::High; } + + // Default to low resolution + return ScreenResolution::Low; +#endif +} + +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) +{ + hour = 0; + minute = 0; + second = 0; + if (rtc_sec == 0) + return; + uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = hms % SEC_PER_MIN; } // === Shared External State === bool hasUnreadMessage = false; -bool isMuted = false; -bool isHighResolution = false; +ScreenResolution currentResolution = ScreenResolution::Low; // === Internal State === bool isBoltVisibleShared = true; @@ -90,7 +113,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti display->setColor(BLACK); display->fillRect(0, 0, screenW, highlightHeight + 2); display->setColor(WHITE); - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawLine(0, 20, screenW, 20); } else { display->drawLine(0, 14, screenW, 14); @@ -128,7 +151,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } #endif - bool useHorizontalBattery = (isHighResolution && screenW >= screenH); + bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; int batteryX = 1; @@ -138,7 +161,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; batteryY += 2; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution); batteryX += 20; // Icon + 1 pixel } else { @@ -199,8 +222,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti if (rtc_sec > 0) { // === Build Time String === long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int hour, minute, second; + graphics::decomposeTime(rtc_sec, hour, minute, second); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); // === Build Date String === @@ -208,7 +231,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); char dateLine[40]; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr); } else { if (hasUnreadMessage) { @@ -283,8 +306,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { - if (isHighResolution) { + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; @@ -302,7 +325,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - if (isInverted) { + if (isInverted && !force_no_invert) { display->setColor(WHITE); display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); display->setColor(BLACK); @@ -360,8 +383,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } - } else if (isMuted) { - if (isHighResolution) { + } else if (externalNotificationModule->getMute()) { + if (currentResolution == ScreenResolution::High) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); @@ -380,7 +403,7 @@ const int *getTextPositions(OLEDDisplay *display) { static int textPositions[7]; // Static array that persists beyond function scope - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { textPositions[0] = textZeroLine; textPositions[1] = textFirstLine_medium; textPositions[2] = textSecondLine_medium; @@ -413,8 +436,12 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) } if (drawConnectionState) { - if (isHighResolution) { - const int scale = 2; + const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1; + display->setColor(BLACK); + display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale), + (connection_icon_height * scale) + (2 * scale)); + display->setColor(WHITE); + if (currentResolution == ScreenResolution::High) { const int bytesPerRow = (connection_icon_width + 7) / 8; int iconX = 0; int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); @@ -443,18 +470,49 @@ bool isAllowedPunctuation(char c) return allowed.find(c) != std::string::npos; } +static void replaceAll(std::string &s, const std::string &from, const std::string &to) +{ + if (from.empty()) + return; + size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); + } +} + std::string sanitizeString(const std::string &input) { std::string output; bool inReplacement = false; - for (char c : input) { - if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + // Make a mutable copy so we can normalize UTF-8 “smart punctuationâ€� into ASCII first. + std::string s = input; + + // Curly single quotes: ‘ ’ + replaceAll(s, "\xE2\x80\x98", "'"); // U+2018 + replaceAll(s, "\xE2\x80\x99", "'"); // U+2019 + + // Curly double quotes: “ â€� + replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C + replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D + + // En dash / Em dash: – — + replaceAll(s, "\xE2\x80\x93", "-"); // U+2013 + replaceAll(s, "\xE2\x80\x94", "-"); // U+2014 + + // Non-breaking space + replaceAll(s, "\xC2\xA0", " "); // U+00A0 + + // Now do your original sanitize pass over the normalized string. + for (unsigned char uc : s) { + char c = static_cast(uc); + if (std::isalnum(uc) || isAllowedPunctuation(c)) { output += c; inReplacement = false; } else { if (!inReplacement) { - output += 0xbf; // ISO-8859-1 for inverted question mark + output += static_cast(0xBF); // ISO-8859-1 for inverted question mark inReplacement = true; } } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index b51dfea36..a8ecdfada 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -41,9 +41,11 @@ namespace graphics // Shared state (declare inside namespace) extern bool hasUnreadMessage; -extern bool isMuted; -extern bool isHighResolution; -void determineResolution(int16_t screenheight, int16_t screenwidth); +enum class ScreenResolution : uint8_t { UltraLow = 0, Low = 1, High = 2 }; +extern ScreenResolution currentResolution; +ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth); + +void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second); // Rounded highlight (used for inverted headers) void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 87593b0d4..12fac4f34 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1,5 +1,6 @@ #include "configuration.h" #include "main.h" +#if USE_TFTDISPLAY #if ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" @@ -123,6 +124,11 @@ static void rak14014_tpIntHandle(void) _rak14014_touch_int = true; } +#elif defined(HACKADAY_COMMUNICATOR) +#include +Arduino_DataBus *bus = nullptr; +Arduino_GFX *tft = nullptr; + #elif defined(ST72xx_DE) #include #include @@ -1133,9 +1139,6 @@ static LGFX *tft = nullptr; #endif -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ - defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ - (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -1271,12 +1274,15 @@ void TFTDisplay::display(bool fromBlank) x_LastPixelUpdate = x; } } - +#if defined(HACKADAY_COMMUNICATOR) + tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate], + (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1); +#else // Step 4: Send the changed pixels on this line to the screen as a single block transfer. // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, &linePixelBuffer[x_FirstPixelUpdate]); - +#endif somethingChanged = true; } y++; @@ -1340,6 +1346,8 @@ void TFTDisplay::sendCommand(uint8_t com) display(true); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); +#elif defined(HACKADAY_COMMUNICATOR) + tft->displayOn(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->wakeup(); tft->powerSaveOff(); @@ -1352,7 +1360,8 @@ void TFTDisplay::sendCommand(uint8_t com) unphone.backlight(true); // using unPhone library #endif #ifdef RAK14014 -#elif !defined(M5STACK) && !defined(ST7789_CS) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function +#elif !defined(M5STACK) && !defined(ST7789_CS) && \ + !defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function tft->setBrightness(172); #endif break; @@ -1364,6 +1373,8 @@ void TFTDisplay::sendCommand(uint8_t com) tft->clear(); if (portduino_config.displayBacklight.pin > 0) digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); +#elif defined(HACKADAY_COMMUNICATOR) + tft->displayOff(); #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) tft->sleep(); tft->powerSaveOn(); @@ -1376,7 +1387,7 @@ void TFTDisplay::sendCommand(uint8_t com) unphone.backlight(false); // using unPhone library #endif #ifdef RAK14014 -#elif !defined(M5STACK) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(0); #endif break; @@ -1392,7 +1403,7 @@ void TFTDisplay::setDisplayBrightness(uint8_t _brightness) { #ifdef RAK14014 // todo -#else +#elif !defined(HACKADAY_COMMUNICATOR) tft->setBrightness(_brightness); LOG_DEBUG("Brightness is set to value: %i ", _brightness); #endif @@ -1410,7 +1421,7 @@ bool TFTDisplay::hasTouch(void) { #ifdef RAK14014 return true; -#elif !defined(M5STACK) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) return tft->touch() != nullptr; #else return false; @@ -1429,7 +1440,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y) } else { return false; } -#elif !defined(M5STACK) +#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) return tft->getTouch(x, y); #else return false; @@ -1448,6 +1459,12 @@ bool TFTDisplay::connect() LOG_INFO("Do TFT init"); #ifdef RAK14014 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 */); + tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */, + 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations, + sizeof(nv3007_279_init_operations)); + #else tft = new LGFX; #endif @@ -1458,8 +1475,15 @@ bool TFTDisplay::connect() #ifdef UNPHONE unphone.backlight(true); // using unPhone library #endif - +#ifdef HACKADAY_COMMUNICATOR + bool beginStatus = tft->begin(); + if (beginStatus) + LOG_DEBUG("TFT Success!"); + else + LOG_ERROR("TFT Fail!"); +#else tft->init(); +#endif #if defined(M5STACK) tft->setRotation(0); @@ -1492,4 +1516,4 @@ bool TFTDisplay::connect() return true; } -#endif +#endif // USE_TFTDISPLAY diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index 8062a0338..a24f5b15c 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -354,8 +354,6 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16 if (screenHeight <= 64) { textY = boxY + (boxHeight - inputLineH) / 2; } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; const int innerTop = boxY + 1; const int innerBottom = boxY + boxHeight - 2; @@ -506,6 +504,9 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool centeredTextY -= 1; } } +#ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE + centeredTextY -= 2; +#endif display->drawString(textX, centeredTextY, keyText.c_str()); } diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index cc6a70957..66bbe1bfe 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -1,15 +1,10 @@ #include "configuration.h" #if HAS_SCREEN #include "ClockRenderer.h" -#include "NodeDB.h" -#include "UIRenderer.h" -#include "configuration.h" -#include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" -#include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" @@ -23,6 +18,31 @@ namespace graphics namespace ClockRenderer { +// Segment bitmaps for numerals 0-9 stored in flash to save RAM. +// Each row is a digit, each column is a segment state (1 = on, 0 = off). +// Segment layout reference: +// +// ___1___ +// 6 | | 2 +// |_7___| +// 5 | | 3 +// |___4_| +// +// Segment order: [1, 2, 3, 4, 5, 6, 7] +// +static const uint8_t PROGMEM digitSegments[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, // 0 + {0, 1, 1, 0, 0, 0, 0}, // 1 + {1, 1, 0, 1, 1, 0, 1}, // 2 + {1, 1, 1, 1, 0, 0, 1}, // 3 + {0, 1, 1, 0, 0, 1, 1}, // 4 + {1, 0, 1, 1, 0, 1, 1}, // 5 + {1, 0, 1, 1, 1, 1, 1}, // 6 + {1, 1, 1, 0, 0, 1, 0}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 + {1, 1, 1, 1, 0, 1, 1} // 9 +}; + void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { uint16_t segmentWidth = SEGMENT_WIDTH * scale; @@ -30,7 +50,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - uint16_t topAndBottomX = x + (4 * scale); + uint16_t topAndBottomX = x + static_cast(4 * scale); uint16_t quarterCellHeight = cellHeight / 4; @@ -43,34 +63,16 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) { - // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of - // segment {innerIndex + 1} - // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. - uint8_t numbers[10][7] = { - {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key - {0, 1, 1, 0, 0, 0, 0}, // 1 1 - {1, 1, 0, 1, 1, 0, 1}, // 2 ___ - {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 - {0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_| - {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 - {1, 0, 1, 1, 1, 1, 1}, // 6 |___| - {1, 1, 1, 0, 0, 1, 0}, // 7 - {1, 1, 1, 1, 1, 1, 1}, // 8 4 - {1, 1, 1, 1, 0, 1, 1}, // 9 - }; - - // the width and height of each segment's central rectangle: - // _____________________ - // â‹°| (only this part, |⋱ - // â‹° | not including | ⋱ - // ⋱ | the triangles | â‹° - // ⋱| on the ends) |â‹° - // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + // Read 7-segment pattern for the digit from flash + uint8_t seg[7]; + for (uint8_t i = 0; i < 7; i++) { + seg[i] = pgm_read_byte(&digitSegments[number][i]); + } uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - // segment x and y coordinates + // Precompute segment positions uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneY = y; @@ -92,33 +94,21 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n uint16_t segmentSevenX = segmentOneX; uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - if (numbers[number][0]) { - graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - } - - if (numbers[number][1]) { - graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - } - - if (numbers[number][2]) { - graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - } - - if (numbers[number][3]) { - graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } - - if (numbers[number][4]) { - graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - } - - if (numbers[number][5]) { - graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - } - - if (numbers[number][6]) { - graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); - } + // Draw only the active segments + if (seg[0]) + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + if (seg[1]) + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + if (seg[2]) + drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + if (seg[3]) + drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + if (seg[4]) + drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + if (seg[5]) + drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + if (seg[6]) + drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); } void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) @@ -147,42 +137,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); } -/* -void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - if (digitalMode) { - uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; - uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); - uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); - - display->drawCircle(centerX, centerY, radius); - display->drawCircle(centerX, centerY, radius + 1); - display->drawLine(centerX, centerY, centerX, centerY - radius + 3); - display->drawLine(centerX, centerY, centerX + radius - 3, centerY); - } else { - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentOneX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; - - uint16_t segmentFourX = x; - uint16_t segmentFourY = y + segmentHeight + 2; - - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } -} -*/ -// Draw a digital clock void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); @@ -192,7 +146,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); - int line = 0; uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone char timeString[16]; @@ -237,7 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 float target_width = display->getWidth() * screenwidth_target_ratio; float target_height = display->getHeight() - - (isHighResolution + ((currentResolution == ScreenResolution::High) ? 46 : 33); // Be careful adjusting this number, we have to account for header and the text under the time @@ -268,10 +221,9 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 scaleInitialized = true; } - size_t len = strlen(timeString); - // calculate hours:minutes string width - uint16_t timeStringWidth = len * 5; // base spacing between characters + size_t len = strlen(timeString); + uint16_t timeStringWidth = len * 5; for (size_t i = 0; i < len; i++) { char character = timeString[i]; @@ -310,9 +262,16 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // draw seconds string + AM/PM display->setFont(FONT_SMALL); - int xOffset = (isHighResolution) ? 0 : -1; + int xOffset = -1; + if (currentResolution == ScreenResolution::High) { + xOffset = 0; + } if (hour >= 10) { - xOffset += (isHighResolution) ? 32 : 18; + if (currentResolution == ScreenResolution::High) { + xOffset += 32; + } else { + xOffset += 18; + } } if (config.display.use_12h_clock) { @@ -320,7 +279,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 } #ifndef USE_EINK - xOffset = (isHighResolution) ? 18 : 10; + xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10; if (scale >= 2.0f) { xOffset -= (int)(4.5f * scale); } @@ -339,19 +298,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true, true); - int line = 0; // clock face center coordinates int16_t centerX = display->getWidth() / 2; int16_t centerY = display->getHeight() / 2; // clock face radius - int16_t radius = 0; - if (display->getHeight() < display->getWidth()) { - radius = (display->getHeight() / 2) * 0.9; - } else { - radius = (display->getWidth() / 2) * 0.9; - } + int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; #ifdef T_WATCH_S3 radius = (display->getWidth() / 2) * 0.8; #endif @@ -366,17 +319,8 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // tick mark outer y coordinate; (first nested circle) int16_t tickMarkOuterNoonY = secondHandNoonY; - // seconds tick mark inner y coordinate; (second nested circle) - double secondsTickMarkInnerNoonY = (double)noonY + 4; - if (isHighResolution) { - secondsTickMarkInnerNoonY = (double)noonY + 8; - } - - // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 6; - if (isHighResolution) { - hoursTickMarkInnerNoonY = (double)noonY + 16; - } + double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4); + double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6); // minute hand y coordinate int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; @@ -386,7 +330,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // hour hand radius and y coordinate int16_t hourHandRadius = radius * 0.35; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { hourHandRadius = radius * 0.55; } int16_t hourHandNoonY = centerY - hourHandRadius; @@ -396,19 +340,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + int hour, minute, second; + decomposeTime(rtc_sec, hour, minute, second); - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - bool isPM = hour >= 12; if (config.display.use_12h_clock) { - isPM = hour >= 12; + bool isPM = hour >= 12; display->setFont(FONT_SMALL); - int yOffset = isHighResolution ? 1 : 0; + int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; #ifdef USE_EINK yOffset += 3; #endif @@ -499,12 +437,13 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); #else #ifdef USE_EINK - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } #else - if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + if (currentResolution == ScreenResolution::High && + (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); } @@ -516,7 +455,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { // draw minute tick mark display->drawLine(startX, startY, endX, endY); } diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 629949ffd..42600ce96 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -48,7 +48,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, // This could draw a "N" indicator or north arrow // For now, we'll draw a simple north indicator // const float radius = 17.0f; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { radius += 4; } Point north(0, -radius); @@ -59,7 +59,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); } else { display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 79c1e7e61..75b65c65f 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -97,7 +97,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - ARCH_PORTDUINO) && \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); @@ -109,7 +109,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); @@ -125,7 +126,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ - ARCH_PORTDUINO) && \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); @@ -281,13 +282,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); // Line 1 (Still) -#if !defined(M5STACK_UNITC6L) - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - if (config.display.heading_bold) - display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (currentResolution != graphics::ScreenResolution::UltraLow) { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - display->setColor(WHITE); -#endif + display->setColor(WHITE); + } // Setup string to assemble analogClock string std::string analogClock = ""; @@ -300,9 +301,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + int hour, min, sec; + graphics::decomposeTime(rtc_sec, hour, min, sec); char timebuf[12]; @@ -378,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int line = 1; // === Set Title - const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa"; + const char *titleStr = (currentResolution == ScreenResolution::High) ? "LoRa Info" : "LoRa"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -390,11 +390,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char shortnameble[35]; getMacAddr(dmac); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); -#if defined(M5STACK_UNITC6L) - snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); -#else - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId); + } else { + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); + } int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); @@ -413,11 +413,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { -#if defined(M5STACK_UNITC6L) - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); -#else - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region); + } else { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -429,17 +429,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { -#if defined(M5STACK_UNITC6L) - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); -#else - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); + } } else { -#if defined(M5STACK_UNITC6L) - snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); -#else - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); + } } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { @@ -455,12 +455,13 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (isHighResolution) ? 100 : 50; - int chutil_bar_height = (isHighResolution) ? 12 : 7; - int extraoffset = (isHighResolution) ? 6 : 3; + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; @@ -529,15 +530,18 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x int line = 1; const int barHeight = 6; const int labelX = x; - int barsOffset = (isHighResolution) ? 24 : 0; + int barsOffset = (currentResolution == ScreenResolution::High) ? 24 : 0; #ifdef USE_EINK +#ifndef T_DECK_PRO barsOffset -= 12; #endif -#if defined(M5STACK_UNITC6L) - const int barX = x + 45 + barsOffset; -#else - const int barX = x + 40 + barsOffset; #endif + int barX = x + barsOffset; + if (currentResolution == ScreenResolution::UltraLow) { + barX += 45; + } else { + barX += 40; + } auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; @@ -545,7 +549,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x int percent = (used * 100) / total; char combinedStr[24]; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { @@ -573,7 +577,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x #endif // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); + display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); }; // === Memory values === @@ -625,25 +629,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x line += 1; char appversionstr[35]; - snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION)); char appversionstr_formatted[40]; - char *lastDot = strrchr(appversionstr, '.'); -#if defined(M5STACK_UNITC6L) - if (lastDot != nullptr) { - *lastDot = '\0'; // truncate string + + const char *ver = optstr(APP_VERSION); + char verbuf[32]; + strncpy(verbuf, ver, sizeof(verbuf) - 1); + verbuf[sizeof(verbuf) - 1] = '\0'; + + char *lastDot = strrchr(verbuf, '.'); + + if (currentResolution == ScreenResolution::UltraLow) { + if (lastDot != nullptr) { + *lastDot = '\0'; + } + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } else { + if (lastDot) { + size_t prefixLen = (size_t)(lastDot - verbuf); + snprintf(appversionstr_formatted, sizeof(appversionstr_formatted), "Ver: %.*s", (int)prefixLen, verbuf); + strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); + strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); + appversionstr[sizeof(appversionstr) - 1] = '\0'; + } else { + snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", verbuf); + } } -#else - if (lastDot) { - size_t prefixLen = lastDot - appversionstr; - strncpy(appversionstr_formatted, appversionstr, prefixLen); - appversionstr_formatted[prefixLen] = '\0'; - strncat(appversionstr_formatted, " (", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, lastDot + 1, sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncat(appversionstr_formatted, ")", sizeof(appversionstr_formatted) - strlen(appversionstr_formatted) - 1); - strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); - appversionstr[sizeof(appversionstr) - 1] = '\0'; - } -#endif int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -662,7 +674,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x const char *clientWord = nullptr; // Determine if narrow or wide screen - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { clientWord = "Client"; } else { clientWord = "App"; @@ -703,11 +715,23 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1 int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int textX_offset = 10; - if (isHighResolution) { - iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); - iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; + if (currentResolution == ScreenResolution::High) { textX_offset = textX_offset * 4; - display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); + const int scale = 2; + const int bytesPerRow = (chirpy_width + 7) / 8; + + for (int yy = 0; yy < chirpy_height; ++yy) { + iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3); + iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2; + const uint8_t *rowPtr = chirpy + yy * bytesPerRow; + for (int xx = 0; xx < chirpy_width; ++xx) { + const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3)); + const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first + if (byteVal & bitMask) { + display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale); + } + } + } } else { display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); } diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h index 6f1929ebd..c55e66ede 100644 --- a/src/graphics/draw/DrawRenderers.h +++ b/src/graphics/draw/DrawRenderers.h @@ -11,7 +11,6 @@ #include "graphics/draw/CompassRenderer.h" #include "graphics/draw/DebugRenderer.h" #include "graphics/draw/NodeListRenderer.h" -#include "graphics/draw/ScreenRenderer.h" #include "graphics/draw/UIRenderer.h" namespace graphics @@ -30,8 +29,6 @@ using namespace ClockRenderer; using namespace CompassRenderer; using namespace DebugRenderer; using namespace NodeListRenderer; -using namespace ScreenRenderer; -using namespace UIRenderer; } // namespace DrawRenderers diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index bd647c3d8..138995ebe 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -1,30 +1,63 @@ #include "configuration.h" #if HAS_SCREEN #include "ClockRenderer.h" +#include "Default.h" #include "GPS.h" #include "MenuHandler.h" #include "MeshRadio.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "buzz.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" #include "graphics/draw/UIRenderer.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" #include "main.h" +#include "mesh/Default.h" #include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" #include "modules/KeyVerificationModule.h" - #include "modules/TraceRouteModule.h" +#include +#include #include +#include extern uint16_t TFT_MESH; namespace graphics { + +namespace +{ + +// Caller must ensure the provided options array outlives the banner callback. +template +BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N], + std::array &labels, Callback &&onSelection) +{ + for (size_t i = 0; i < N; ++i) { + labels[i] = options[i].label; + } + + const MenuOption *optionsPtr = options; + auto callback = std::function &, int)>(std::forward(onSelection)); + + BannerOverlayOptions bannerOptions; + bannerOptions.message = message; + bannerOptions.optionsArrayPtr = labels.data(); + bannerOptions.optionsCount = static_cast(N); + bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); }; + return bannerOptions; +} + +} // namespace + menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; @@ -74,51 +107,60 @@ void menuHandler::OnboardMessage() void menuHandler::LoraRegionPicker(uint32_t duration) { - static const char *optionsArray[] = {"Back", - "US", - "EU_433", - "EU_868", - "CN", - "JP", - "ANZ", - "KR", - "TW", - "RU", - "IN", - "NZ_865", - "TH", - "LORA_24", - "UA_433", - "UA_868", - "MY_433", - "MY_" - "919", - "SG_" - "923", - "PH_433", - "PH_868", - "PH_915", - "ANZ_433", - "KZ_433", - "KZ_863", - "NP_865", - "BR_902"}; - BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "LoRa Region"; -#else - bannerOptions.message = "Set the LoRa region"; -#endif - bannerOptions.durationMs = duration; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 27; - bannerOptions.InitialSelected = 0; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { - config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + static const LoraRegionOption regionOptions[] = { + {"Back", OptionsAction::Back}, + {"US", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_US}, + {"EU_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_433}, + {"EU_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_EU_868}, + {"CN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_CN}, + {"JP", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_JP}, + {"ANZ", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ}, + {"KR", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KR}, + {"TW", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TW}, + {"RU", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_RU}, + {"IN", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_IN}, + {"NZ_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NZ_865}, + {"TH", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_TH}, + {"LORA_24", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_LORA_24}, + {"UA_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_433}, + {"UA_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_UA_868}, + {"MY_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_433}, + {"MY_919", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_MY_919}, + {"SG_923", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_SG_923}, + {"PH_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_433}, + {"PH_868", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_868}, + {"PH_915", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_PH_915}, + {"ANZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ANZ_433}, + {"KZ_433", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_433}, + {"KZ_863", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_KZ_863}, + {"NP_865", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_NP_865}, + {"BR_902", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_BR_902}, + }; + + constexpr size_t regionCount = sizeof(regionOptions) / sizeof(regionOptions[0]); + static std::array regionLabels{}; + + const char *bannerMessage = "Set the LoRa region"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerMessage = "LoRa Region"; + } + + auto bannerOptions = + createStaticBannerOptions(bannerMessage, regionOptions, regionLabels, [](const LoraRegionOption &option, int) -> void { + if (!option.hasValue) { + return; + } + + auto selectedRegion = option.value; + if (config.lora.region == selectedRegion) { + return; + } + + config.lora.region = selectedRegion; auto changes = SEGMENT_CONFIG; - // This is needed as we wait til picking the LoRa region to generate keys for the first time. + // 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; @@ -155,8 +197,19 @@ void menuHandler::LoraRegionPicker(uint32_t duration) service->reloadConfig(changes); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + bannerOptions.durationMs = duration; + + int initialSelection = 0; + for (size_t i = 0; i < regionCount; ++i) { + if (regionOptions[i].hasValue && regionOptions[i].value == config.lora.region) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -196,48 +249,38 @@ void menuHandler::DeviceRolePicker() void menuHandler::RadioPresetPicker() { - static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow", - "MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"}; - enum optionsNumbers { - Back = 0, - radiopreset_LongSlow = 1, - radiopreset_LongModerate = 2, - radiopreset_LongFast = 3, - radiopreset_MediumSlow = 4, - radiopreset_MediumFast = 5, - radiopreset_ShortSlow = 6, - radiopreset_ShortFast = 7, - radiopreset_ShortTurbo = 8 - }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Radio Preset"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 9; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; - screen->runNow(); - return; - } else if (selected == radiopreset_LongSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW; - } else if (selected == radiopreset_LongModerate) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE; - } else if (selected == radiopreset_LongFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; - } else if (selected == radiopreset_MediumSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW; - } else if (selected == radiopreset_MediumFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST; - } else if (selected == radiopreset_ShortSlow) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW; - } else if (selected == radiopreset_ShortFast) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST; - } else if (selected == radiopreset_ShortTurbo) { - config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO; - } - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + static const RadioPresetOption presetOptions[] = { + {"Back", OptionsAction::Back}, + {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO}, + {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE}, + {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST}, + {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW}, + {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST}, + {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW}, + {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST}, + {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO}, }; + + constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]); + static std::array presetLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + config.lora.modem_preset = option.value; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + screen->showOverlayBanner(bannerOptions); } @@ -281,102 +324,100 @@ void menuHandler::showConfirmationBanner(const char *message, std::function void { - if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == Digital) { - uiconfig.is_clockface_analog = false; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } else { - uiconfig.is_clockface_analog = true; - saveUIConfig(); - screen->setFrames(Screen::FOCUS_CLOCK); - } + static const ClockFaceOption clockFaceOptions[] = { + {"Back", OptionsAction::Back}, + {"Digital", OptionsAction::Select, false}, + {"Analog", OptionsAction::Select, true}, }; + + constexpr size_t clockFaceCount = sizeof(clockFaceOptions) / sizeof(clockFaceOptions[0]); + static std::array clockFaceLabels{}; + + auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, + [](const ClockFaceOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.is_clockface_analog == option.value) { + return; + } + + uiconfig.is_clockface_analog = option.value; + saveUIConfig(); + screen->setFrames(Screen::FOCUS_CLOCK); + }); + bannerOptions.InitialSelected = uiconfig.is_clockface_analog ? 2 : 1; screen->showOverlayBanner(bannerOptions); } void menuHandler::TZPicker() { - static const char *optionsArray[] = {"Back", - "US/Hawaii", - "US/Alaska", - "US/Pacific", - "US/Arizona", - "US/Mountain", - "US/Central", - "US/Eastern", - "BR/Brasilia", - "UTC", - "EU/Western", - "EU/" - "Central", - "EU/Eastern", - "Asia/Kolkata", - "Asia/Hong_Kong", - "AU/AWST", - "AU/ACST", - "AU/AEST", - "Pacific/NZ"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Pick Timezone"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 19; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 0) { - menuHandler::menuQueue = menuHandler::clock_menu; - screen->runNow(); - } else if (selected == 1) { // Hawaii - strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); - } else if (selected == 2) { // Alaska - strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 3) { // Pacific - strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 4) { // Arizona - strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); - } else if (selected == 5) { // Mountain - strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 6) { // Central - strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 7) { // Eastern - strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 8) { // Brazil - strncpy(config.device.tzdef, "BRT3", sizeof(config.device.tzdef)); - } else if (selected == 9) { // UTC - strncpy(config.device.tzdef, "UTC0", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Western - strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 11) { // EU/Central - strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 12) { // EU/Eastern - strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 13) { // Asia/Kolkata - strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 14) { // China - strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/AWST - strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 16) { // AU/ACST - strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 17) { // AU/AEST - strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 18) { // NZ - strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); - } - if (selected != 0) { + static const TimezoneOption timezoneOptions[] = { + {"Back", OptionsAction::Back}, + {"US/Hawaii", OptionsAction::Select, "HST10"}, + {"US/Alaska", OptionsAction::Select, "AKST9AKDT,M3.2.0,M11.1.0"}, + {"US/Pacific", OptionsAction::Select, "PST8PDT,M3.2.0,M11.1.0"}, + {"US/Arizona", OptionsAction::Select, "MST7"}, + {"US/Mountain", OptionsAction::Select, "MST7MDT,M3.2.0,M11.1.0"}, + {"US/Central", OptionsAction::Select, "CST6CDT,M3.2.0,M11.1.0"}, + {"US/Eastern", OptionsAction::Select, "EST5EDT,M3.2.0,M11.1.0"}, + {"BR/Brasilia", OptionsAction::Select, "BRT3"}, + {"UTC", OptionsAction::Select, "UTC0"}, + {"EU/Western", OptionsAction::Select, "GMT0BST,M3.5.0/1,M10.5.0"}, + {"EU/Central", OptionsAction::Select, "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"EU/Eastern", OptionsAction::Select, "EET-2EEST,M3.5.0/3,M10.5.0/4"}, + {"Asia/Kolkata", OptionsAction::Select, "IST-5:30"}, + {"Asia/Hong_Kong", OptionsAction::Select, "HKT-8"}, + {"AU/AWST", OptionsAction::Select, "AWST-8"}, + {"AU/ACST", OptionsAction::Select, "ACST-9:30ACDT,M10.1.0,M4.1.0/3"}, + {"AU/AEST", OptionsAction::Select, "AEST-10AEDT,M10.1.0,M4.1.0/3"}, + {"Pacific/NZ", OptionsAction::Select, "NZST-12NZDT,M9.5.0,M4.1.0/3"}, + }; + + constexpr size_t timezoneCount = sizeof(timezoneOptions) / sizeof(timezoneOptions[0]); + static std::array timezoneLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuHandler::menuQueue = menuHandler::clock_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (strncmp(config.device.tzdef, option.value, sizeof(config.device.tzdef)) == 0) { + return; + } + + strncpy(config.device.tzdef, option.value, sizeof(config.device.tzdef)); + config.device.tzdef[sizeof(config.device.tzdef) - 1] = '\0'; + setenv("TZ", config.device.tzdef, 1); service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < timezoneCount; ++i) { + if (timezoneOptions[i].hasValue && + strncmp(config.device.tzdef, timezoneOptions[i].value, sizeof(config.device.tzdef)) == 0) { + initialSelection = static_cast(i); + break; } - }; + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -406,72 +447,453 @@ void menuHandler::clockMenu() }; screen->showOverlayBanner(bannerOptions); } - void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"}; -#else - static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; -#endif - static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; - int options = 3; + enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd }; - if (kb_found) { - optionsArray[options] = "Reply via Freetext"; - optionsEnumArray[options++] = Freetext; + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + int threadChannel = graphics::MessageRenderer::getThreadChannel(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // New Reply submenu (replaces Preset and Freetext directly in this menu) + optionsArray[options] = "Reply"; + optionsEnumArray[options++] = ReplyMenu; + + optionsArray[options] = "View Chats"; + optionsEnumArray[options++] = ViewMode; + + // If viewing ALL chats, hide “Mute Chatâ€� + if (mode != graphics::MessageRenderer::ThreadMode::ALL && mode != graphics::MessageRenderer::ThreadMode::DIRECT) { + const uint8_t chIndex = (threadChannel != 0) ? (uint8_t)threadChannel : channels.getPrimaryIndex(); + auto &chan = channels.getByIndex(chIndex); + + optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel"; + optionsEnumArray[options++] = MuteChannel; } + // Delete submenu + optionsArray[options] = "Delete"; + optionsEnumArray[options++] = 900; + #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; optionsEnumArray[options++] = Aloud; #endif + BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Message"; -#else bannerOptions.message = "Message Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Message"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Dismiss) { - screen->hideCurrentFrame(); - } else if (selected == Preset) { - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + LOG_DEBUG("messageResponseMenu: selected %d", selected); + + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); + + if (selected == ViewMode) { + menuHandler::menuQueue = menuHandler::message_viewmode_menu; + screen->runNow(); + + // Reply submenu + } else if (selected == ReplyMenu) { + menuHandler::menuQueue = menuHandler::reply_menu; + screen->runNow(); + + } else if (selected == MuteChannel) { + const uint8_t chIndex = (ch != 0) ? (uint8_t)ch : channels.getPrimaryIndex(); + auto &chan = channels.getByIndex(chIndex); + if (chan.settings.has_module_settings) { + chan.settings.module_settings.is_muted = !chan.settings.module_settings.is_muted; + nodeDB->saveToDisk(); } - } else if (selected == Freetext) { - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + + // Delete submenu + } else if (selected == 900) { + menuHandler::menuQueue = menuHandler::delete_messages_menu; + screen->runNow(); + + // Delete oldest FIRST (only change) + } else if (selected == DeleteOldest) { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + // Global oldest + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + // Oldest in current channel + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + // Oldest in current DM + messageStore.deleteOldestMessageWithPeer(peer); } - } + + // Delete all messages + } else if (selected == DeleteAll) { + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + #ifdef HAS_I2S - else if (selected == Aloud) { + } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - audioThread->readAloud(msg); - } #endif + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::replyMenu() +{ + enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + // Back + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + // Preset reply + optionsArray[options] = "With Preset"; + optionsEnumArray[options++] = ReplyPreset; + + // Freetext reply (only when keyboard exists) + if (kb_found) { + optionsArray[options] = "With Freetext"; + optionsEnumArray[options++] = ReplyFreetext; + } + + BannerOverlayOptions bannerOptions; + + // Dynamic title based on thread mode + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + bannerOptions.message = "Reply to Channel"; + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + bannerOptions.message = "Reply to DM"; + } else { + // View All + bannerOptions.message = "Reply to Last Msg"; + } + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.InitialSelected = 1; + + bannerOptions.bannerCallback = [](int selected) -> void { + auto mode = graphics::MessageRenderer::getThreadMode(); + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + // Preset reply + if (selected == ReplyPreset) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchWithDestination(peer); + + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + } + } + + return; + } + + // Freetext reply + if (selected == ReplyFreetext) { + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch); + + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + cannedMessageModule->LaunchFreetextWithDestination(peer); + + } else { + // Fallback for last received message + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } + + return; + } + }; + screen->showOverlayBanner(bannerOptions); +} +void menuHandler::deleteMessagesMenu() +{ + enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd }; + + static const char *optionsArray[enumEnd]; + static int optionsEnumArray[enumEnd]; + int options = 0; + + auto mode = graphics::MessageRenderer::getThreadMode(); + + optionsArray[options] = "Back"; + optionsEnumArray[options++] = Back; + + optionsArray[options] = "Delete Oldest"; + optionsEnumArray[options++] = DeleteOldest; + + // If viewing ALL chats → hide “Delete This Chatâ€� + if (mode != graphics::MessageRenderer::ThreadMode::ALL) { + optionsArray[options] = "Delete This Chat"; + optionsEnumArray[options++] = DeleteThis; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Delete All"; + } else { + optionsArray[options] = "Delete All Chats"; + } + optionsEnumArray[options++] = DeleteAll; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Delete Messages"; + + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = options; + bannerOptions.bannerCallback = [mode](int selected) -> void { + int ch = graphics::MessageRenderer::getThreadChannel(); + uint32_t peer = graphics::MessageRenderer::getThreadPeer(); + + if (selected == Back) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + return; + } + + if (selected == DeleteAll) { + LOG_INFO("Deleting all messages"); + messageStore.clearAllMessages(); + graphics::MessageRenderer::clearThreadRegistries(); + graphics::MessageRenderer::clearMessageCache(); + return; + } + + if (selected == DeleteOldest) { + LOG_INFO("Deleting oldest message"); + + if (mode == graphics::MessageRenderer::ThreadMode::ALL) { + messageStore.deleteOldestMessage(); + } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteOldestMessageInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteOldestMessageWithPeer(peer); + } + + return; + } + + // This only appears in non-ALL modes + if (selected == DeleteThis) { + LOG_INFO("Deleting all messages in this thread"); + + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { + messageStore.deleteAllMessagesInChannel(ch); + } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + messageStore.deleteAllMessagesWithPeer(peer); + } + + return; + } + }; + + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageViewModeMenu() +{ + auto encodeChannelId = [](int ch) -> int { return 100 + ch; }; + auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; }; + + static std::vector labels; + static std::vector ids; + static std::vector idToPeer; // DM lookup + + labels.clear(); + ids.clear(); + idToPeer.clear(); + + labels.push_back("Back"); + ids.push_back(-1); + labels.push_back("View All Chats"); + ids.push_back(-2); + + // Channels with messages + for (int ch = 0; ch < 8; ++ch) { + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (!msgs.empty()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(encodeChannelId(ch)); + LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch)); + } + } + + // Registry channels + for (int ch : graphics::MessageRenderer::getSeenChannels()) { + if (ch < 0 || ch >= 8) + continue; + auto msgs = messageStore.getChannelMessages((uint8_t)ch); + if (msgs.empty()) + continue; + int enc = encodeChannelId(ch); + if (std::find(ids.begin(), ids.end(), enc) == ids.end()) { + char buf[40]; + const char *cname = channels.getName(ch); + snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch); + labels.push_back(buf); + ids.push_back(enc); + LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc); + } + } + + // Gather unique peers + auto dms = messageStore.getDirectMessages(); + std::vector uniquePeers; + for (auto &m : dms) { + uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) { + if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end()) + uniquePeers.push_back(peer); + } + std::sort(uniquePeers.begin(), uniquePeers.end()); + + // Encode peers + for (size_t i = 0; i < uniquePeers.size(); ++i) { + uint32_t peer = uniquePeers[i]; + auto node = nodeDB->getMeshNode(peer); + std::string name; + if (node && node->has_user) + name = sanitizeString(node->user.long_name).substr(0, 15); + else { + char buf[20]; + snprintf(buf, sizeof(buf), "Node %08X", peer); + name = buf; + } + labels.push_back("@" + name); + int encPeer = 1000 + (int)idToPeer.size(); + ids.push_back(encPeer); + idToPeer.push_back(peer); + LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer); + } + + // Active ID + int activeId = -2; + auto mode = graphics::MessageRenderer::getThreadMode(); + if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) + activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel()); + else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { + uint32_t cur = graphics::MessageRenderer::getThreadPeer(); + for (size_t i = 0; i < idToPeer.size(); ++i) + if (idToPeer[i] == cur) { + activeId = 1000 + (int)i; + break; + } + } + + LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId); + + // Build banner + static std::vector options; + static std::vector optionIds; + options.clear(); + optionIds.clear(); + + int initialIndex = 0; + for (size_t i = 0; i < labels.size(); i++) { + options.push_back(labels[i].c_str()); + optionIds.push_back(ids[i]); + if (ids[i] == activeId) + initialIndex = (int)i; + } + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Select Conversation"; + bannerOptions.optionsArrayPtr = options.data(); + bannerOptions.optionsEnumPtr = optionIds.data(); + bannerOptions.optionsCount = options.size(); + bannerOptions.InitialSelected = initialIndex; + + bannerOptions.bannerCallback = [=](int selected) -> void { + LOG_DEBUG("messageViewModeMenu: selected=%d", selected); + if (selected == -1) { + menuHandler::menuQueue = menuHandler::message_response_menu; + screen->runNow(); + } else if (selected == -2) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); + } else if (isChannelSel(selected)) { + int ch = selected - 100; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch); + } else if (selected >= 1000) { + int idx = selected - 1000; + if (idx >= 0 && (size_t)idx < idToPeer.size()) { + uint32_t peer = idToPeer[idx]; + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer); + } + } }; screen->showOverlayBanner(bannerOptions); } void menuHandler::homeBaseMenu() { - enum optionsNumbers { Back, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; + enum optionsNumbers { Back, Mute, Backlight, Position, Preset, Freetext, Sleep, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; + if (moduleConfig.external_notification.enabled && externalNotificationModule && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED) { + if (!externalNotificationModule->getMute()) { + optionsArray[options] = "Temporarily Mute"; + } else { + optionsArray[options] = "Unmute"; + } + optionsEnumArray[options++] = Mute; + } #if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN) optionsArray[options] = "Toggle Backlight"; optionsEnumArray[options++] = Backlight; @@ -485,28 +907,23 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Send Node Info"; } optionsEnumArray[options++] = Position; -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "New Preset"; -#else - optionsArray[options] = "New Preset Msg"; -#endif - optionsEnumArray[options++] = Preset; - if (kb_found) { - optionsArray[options] = "New Freetext Msg"; - optionsEnumArray[options++] = Freetext; - } BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Home"; -#else bannerOptions.message = "Home Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Home"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Backlight) { + if (selected == Mute) { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow();) + } + } else if (selected == Backlight) { + screen->setOn(false); #if defined(PIN_EINK_EN) if (uiconfig.screen_brightness == 1) { uiconfig.screen_brightness = 0; @@ -529,8 +946,12 @@ void menuHandler::homeBaseMenu() } else if (selected == Sleep) { screen->setOn(false); } else if (selected == Position) { - InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SEND_PING, .kbchar = 0, .touchX = 0, .touchY = 0}; - inputBroker->injectInputEvent(&event); + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showSimpleBanner("Position\nSent", 3000)); + } else { + IF_SCREEN(screen->showSimpleBanner("Node Info\nSent", 3000)); + } } else if (selected == Preset) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); } else if (selected == Freetext) { @@ -576,27 +997,33 @@ void menuHandler::textMessageBaseMenu() void menuHandler::systemBaseMenu() { - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd }; + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; + optionsArray[options] = "Display Options"; optionsEnumArray[options++] = ScreenOptions; -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "Bluetooth"; -#else - optionsArray[options] = "Bluetooth Toggle"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Bluetooth"; + } else { + optionsArray[options] = "Bluetooth Toggle"; + } optionsEnumArray[options++] = Bluetooth; -#if defined(M5STACK_UNITC6L) - optionsArray[options] = "Power"; -#else - optionsArray[options] = "Reboot/Shutdown"; +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + optionsArray[options] = "WiFi Toggle"; + optionsEnumArray[options++] = WiFiToggle; #endif + + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "Power"; + } else { + optionsArray[options] = "Reboot/Shutdown"; + } optionsEnumArray[options++] = PowerMenu; if (test_enabled) { @@ -605,17 +1032,16 @@ void menuHandler::systemBaseMenu() } BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "System"; -#else bannerOptions.message = "System Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "System"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::notifications_menu; + menuHandler::menuQueue = menuHandler::buzzermodemenupicker; screen->runNow(); } else if (selected == ScreenOptions) { menuHandler::menuQueue = menuHandler::screen_options_menu; @@ -629,6 +1055,11 @@ void menuHandler::systemBaseMenu() } else if (selected == Bluetooth) { menuQueue = bluetooth_toggle_menu; screen->runNow(); +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + } else if (selected == WiFiToggle) { + menuQueue = wifi_toggle_menu; + screen->runNow(); +#endif } else if (selected == Back && !test_enabled) { test_count++; if (test_count > 4) { @@ -641,32 +1072,49 @@ void menuHandler::systemBaseMenu() void menuHandler::favoriteBaseMenu() { - enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[enumEnd] = {"Back", "New Preset"}; -#else - static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; -#endif - static int optionsEnumArray[enumEnd] = {Back, Preset}; - int options = 2; + enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; + + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + // Only show "View Conversation" if a message exists with this node + uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum; + bool hasConversation = false; + for (const auto &m : messageStore.getMessages()) { + if ((m.sender == peer || m.dest == peer)) { + hasConversation = true; + break; + } + } + if (hasConversation) { + optionsArray[options] = "Go To Chat"; + optionsEnumArray[options++] = GoToChat; + } + if (currentResolution == ScreenResolution::UltraLow) { + optionsArray[options] = "New Preset"; + } else { + optionsArray[options] = "New Preset Msg"; + } + optionsEnumArray[options++] = Preset; if (kb_found) { optionsArray[options] = "New Freetext Msg"; optionsEnumArray[options++] = Freetext; } -#if !defined(M5STACK_UNITC6L) - optionsArray[options] = "Trace Route"; - optionsEnumArray[options++] = TraceRoute; -#endif + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + } optionsArray[options] = "Remove Favorite"; optionsEnumArray[options++] = Remove; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Favorites"; -#else bannerOptions.message = "Favorites Action"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Favorites"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = options; @@ -675,6 +1123,17 @@ void menuHandler::favoriteBaseMenu() cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); } else if (selected == Freetext) { cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } + // Handle new Go To Thread action + else if (selected == GoToChat) { + // Switch thread to direct conversation with this node + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, + graphics::UIRenderer::currentFavoriteNodeNum); + + // Manually create and send a UIFrameEvent to trigger the jump + UIFrameEvent evt; + evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { menuHandler::menuQueue = menuHandler::remove_favorite; screen->runNow(); @@ -689,55 +1148,124 @@ void menuHandler::favoriteBaseMenu() void menuHandler::positionBaseMenu() { - enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; + enum class PositionAction { + GpsToggle, + GpsFormat, + CompassMenu, + CompassCalibrate, + GPSSmartPosition, + GPSUpdateInterval, + GPSPositionBroadcast + }; - static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; - static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; - int options = 4; + static const PositionMenuOption baseOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + }; - if (accelerometerThread) { - optionsArray[options] = "Compass Calibrate"; - optionsEnumArray[options++] = CompassCalibrate; - } + static const PositionMenuOption calibrateOptions[] = { + {"Back", OptionsAction::Back}, + {"On/Off Toggle", OptionsAction::Select, static_cast(PositionAction::GpsToggle)}, + {"Format", OptionsAction::Select, static_cast(PositionAction::GpsFormat)}, + {"Smart Position", OptionsAction::Select, static_cast(PositionAction::GPSSmartPosition)}, + {"Update Interval", OptionsAction::Select, static_cast(PositionAction::GPSUpdateInterval)}, + {"Broadcast Interval", OptionsAction::Select, static_cast(PositionAction::GPSPositionBroadcast)}, + {"Compass", OptionsAction::Select, static_cast(PositionAction::CompassMenu)}, + {"Compass Calibrate", OptionsAction::Select, static_cast(PositionAction::CompassCalibrate)}, + }; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Position Action"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.optionsCount = options; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == GPSToggle) { + constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]); + constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]); + static std::array baseLabels{}; + static std::array calibrateLabels{}; + + auto onSelection = [](const PositionMenuOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + return; + } + + if (!option.hasValue) { + return; + } + + auto action = static_cast(option.value); + switch (action) { + case PositionAction::GpsToggle: menuQueue = gps_toggle_menu; screen->runNow(); - } else if (selected == GPSFormat) { + break; + case PositionAction::GpsFormat: menuQueue = gps_format_menu; screen->runNow(); - } else if (selected == CompassMenu) { + break; + case PositionAction::CompassMenu: menuQueue = compass_point_north_menu; screen->runNow(); - } else if (selected == CompassCalibrate) { - accelerometerThread->calibrate(30); + break; + case PositionAction::CompassCalibrate: + if (accelerometerThread) { + accelerometerThread->calibrate(30); + } + break; + case PositionAction::GPSSmartPosition: + menuQueue = gps_smart_position_menu; + screen->runNow(); + break; + case PositionAction::GPSUpdateInterval: + menuQueue = gps_update_interval_menu; + screen->runNow(); + break; + case PositionAction::GPSPositionBroadcast: + menuQueue = gps_position_broadcast_menu; + screen->runNow(); + break; } }; + + BannerOverlayOptions bannerOptions; + if (accelerometerThread) { + bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection); + } else { + bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection); + } + screen->showOverlayBanner(bannerOptions); } void menuHandler::nodeListMenu() { - enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; -#if defined(M5STACK_UNITC6L) - static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"}; -#else - static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; -#endif + enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back"}; + static int optionsEnumArray[enumEnd] = {Back}; + int options = 1; + + optionsArray[options] = "Add Favorite"; + optionsEnumArray[options++] = Favorite; + optionsArray[options] = "Trace Route"; + optionsEnumArray[options++] = TraceRoute; + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Key Verification"; + optionsEnumArray[options++] = Verify; + } + + if (currentResolution != ScreenResolution::UltraLow) { + optionsArray[options] = "Show Long/Short Name"; + optionsEnumArray[options++] = NodeNameLength; + } + optionsArray[options] = "Reset NodeDB"; + optionsEnumArray[options++] = Reset; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Node Action"; bannerOptions.optionsArrayPtr = optionsArray; -#if defined(M5STACK_UNITC6L) - bannerOptions.optionsCount = 3; -#else - bannerOptions.optionsCount = 5; -#endif + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Favorite) { menuQueue = add_favorite; @@ -751,6 +1279,9 @@ void menuHandler::nodeListMenu() } else if (selected == TraceRoute) { menuQueue = trace_route_menu; screen->runNow(); + } else if (selected == NodeNameLength) { + menuHandler::menuQueue = menuHandler::node_name_length_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -758,27 +1289,38 @@ void menuHandler::nodeListMenu() void menuHandler::nodeNameLengthMenu() { - enum OptionsNumbers { Back, Long, Short }; - static const char *optionsArray[] = {"Back", "Long", "Short"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Node Name Length"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Long) { - // Set names to long - LOG_INFO("Setting names to long"); - config.display.use_long_node_name = true; - } else if (selected == Short) { - // Set names to short - LOG_INFO("Setting names to short"); - config.display.use_long_node_name = false; - } else if (selected == Back) { - menuQueue = screen_options_menu; - screen->runNow(); - } + static const NodeNameOption nodeNameOptions[] = { + {"Back", OptionsAction::Back}, + {"Long", OptionsAction::Select, true}, + {"Short", OptionsAction::Select, false}, }; - bannerOptions.InitialSelected = config.display.use_long_node_name == true ? 1 : 2; + + constexpr size_t nodeNameCount = sizeof(nodeNameOptions) / sizeof(nodeNameOptions[0]); + static std::array nodeNameLabels{}; + + auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, + [](const NodeNameOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = node_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.display.use_long_node_name == option.value) { + return; + } + + config.display.use_long_node_name = option.value; + LOG_INFO("Setting names to %s", option.value ? "long" : "short"); + }); + + int initialSelection = config.display.use_long_node_name ? 1 : 2; + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -802,6 +1344,9 @@ void menuHandler::resetNodeDBMenu() LOG_INFO("Initiate node-db reset but keeping favorites"); nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 0) { + menuQueue = node_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -809,131 +1354,392 @@ void menuHandler::resetNodeDBMenu() void menuHandler::compassNorthMenu() { - enum optionsNumbers { Back, Dynamic, Fixed, Freeze }; - static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "North Directions?"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 4; - bannerOptions.InitialSelected = uiconfig.compass_mode + 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Dynamic) { - if (uiconfig.compass_mode != meshtastic_CompassMode_DYNAMIC) { - uiconfig.compass_mode = meshtastic_CompassMode_DYNAMIC; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Fixed) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) { - uiconfig.compass_mode = meshtastic_CompassMode_FIXED_RING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Freeze) { - if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { - uiconfig.compass_mode = meshtastic_CompassMode_FREEZE_HEADING; - saveUIConfig(); - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } else if (selected == Back) { - menuQueue = position_base_menu; - screen->runNow(); - } + static const CompassOption compassOptions[] = { + {"Back", OptionsAction::Back}, + {"Dynamic", OptionsAction::Select, meshtastic_CompassMode_DYNAMIC}, + {"Fixed Ring", OptionsAction::Select, meshtastic_CompassMode_FIXED_RING}, + {"Freeze Heading", OptionsAction::Select, meshtastic_CompassMode_FREEZE_HEADING}, }; + + constexpr size_t compassCount = sizeof(compassOptions) / sizeof(compassOptions[0]); + static std::array compassLabels{}; + + auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, + [](const CompassOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (uiconfig.compass_mode == option.value) { + return; + } + + uiconfig.compass_mode = option.value; + saveUIConfig(); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + }); + + int initialSelection = 0; + for (size_t i = 0; i < compassCount; ++i) { + if (compassOptions[i].hasValue && uiconfig.compass_mode == compassOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } #if !MESHTASTIC_EXCLUDE_GPS void menuHandler::GPSToggleMenu() { - - static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Toggle GPS"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 3; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - playGPSEnableBeep(); - gps->enable(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - playGPSDisableBeep(); - gps->disable(); - service->reloadConfig(SEGMENT_CONFIG); - } else { - menuQueue = position_base_menu; - screen->runNow(); - } + static const GPSToggleOption gpsToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_PositionConfig_GpsMode_DISABLED}, }; - bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; + + constexpr size_t toggleCount = sizeof(gpsToggleOptions) / sizeof(gpsToggleOptions[0]); + static std::array toggleLabels{}; + + auto bannerOptions = + createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = position_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.position.gps_mode == option.value) { + return; + } + + config.position.gps_mode = option.value; + if (option.value == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + playGPSEnableBeep(); + gps->enable(); + } else { + playGPSDisableBeep(); + gps->disable(); + } + service->reloadConfig(SEGMENT_CONFIG); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (gpsToggleOptions[i].hasValue && config.position.gps_mode == gpsToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } void menuHandler::GPSFormatMenu() { + static const GPSFormatOption formatOptionsHigh[] = { + {"Back", OptionsAction::Back}, + {"Decimal Degrees", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"Degrees Minutes Seconds", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"Universal Transverse Mercator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"Military Grid Reference System", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"Open Location Code", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"Ordnance Survey Grid Ref", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"Maidenhead Locator", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; - static const char *optionsArray[] = {"Back", - isHighResolution ? "Decimal Degrees" : "DEC", - isHighResolution ? "Degrees Minutes Seconds" : "DMS", - isHighResolution ? "Universal Transverse Mercator" : "UTM", - isHighResolution ? "Military Grid Reference System" : "MGRS", - isHighResolution ? "Open Location Code" : "OLC", - isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR", - isHighResolution ? "Maidenhead Locator" : "MLS"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "GPS Format"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 8; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 3) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 4) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 5) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 6) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 7) { - uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS; - saveUIConfig(); - service->reloadConfig(SEGMENT_CONFIG); - } else { + static const GPSFormatOption formatOptionsLow[] = { + {"Back", OptionsAction::Back}, + {"DEC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC}, + {"DMS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS}, + {"UTM", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM}, + {"MGRS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS}, + {"OLC", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC}, + {"OSGR", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR}, + {"MLS", OptionsAction::Select, meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS}, + }; + + constexpr size_t formatCount = sizeof(formatOptionsHigh) / sizeof(formatOptionsHigh[0]); + static std::array formatLabelsHigh{}; + static std::array formatLabelsLow{}; + + auto onSelection = [](const GPSFormatOption &option, int) -> void { + if (option.action == OptionsAction::Back) { menuQueue = position_base_menu; screen->runNow(); + return; } + + if (!option.hasValue) { + return; + } + + if (uiconfig.gps_format == option.value) { + return; + } + + uiconfig.gps_format = option.value; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); }; - bannerOptions.InitialSelected = uiconfig.gps_format + 1; + + BannerOverlayOptions bannerOptions; + int initialSelection = 0; + + if (currentResolution == ScreenResolution::High) { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsHigh, formatLabelsHigh, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsHigh[i].hasValue && uiconfig.gps_format == formatOptionsHigh[i].value) { + initialSelection = static_cast(i); + break; + } + } + } else { + bannerOptions = createStaticBannerOptions("GPS Format", formatOptionsLow, formatLabelsLow, onSelection); + for (size_t i = 0; i < formatCount; ++i) { + if (formatOptionsLow[i].hasValue && uiconfig.gps_format == formatOptionsLow[i].value) { + initialSelection = static_cast(i); + break; + } + } + } + + bannerOptions.InitialSelected = initialSelection; screen->showOverlayBanner(bannerOptions); } + +void menuHandler::GPSSmartPositionMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Toggle Smart Position"; + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Smrt Postn"; + } + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_smart_enabled = true; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } else if (selected == 2) { + config.position.position_broadcast_smart_enabled = false; + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + bannerOptions.InitialSelected = config.position.position_broadcast_smart_enabled ? 1 : 2; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSUpdateIntervalMenu() +{ + static const char *optionsArray[] = {"Back", "8 seconds", "20 seconds", "40 seconds", "1 minute", "80 seconds", + "2 minutes", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", + "6 hours", "12 hours", "24 hours", "At Boot Only"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Update Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 16; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.gps_update_interval = 8; + } else if (selected == 2) { + config.position.gps_update_interval = 20; + } else if (selected == 3) { + config.position.gps_update_interval = 40; + } else if (selected == 4) { + config.position.gps_update_interval = 60; + } else if (selected == 5) { + config.position.gps_update_interval = 80; + } else if (selected == 6) { + config.position.gps_update_interval = 120; + } else if (selected == 7) { + config.position.gps_update_interval = 300; + } else if (selected == 8) { + config.position.gps_update_interval = 600; + } else if (selected == 9) { + config.position.gps_update_interval = 900; + } else if (selected == 10) { + config.position.gps_update_interval = 1800; + } else if (selected == 11) { + config.position.gps_update_interval = 3600; + } else if (selected == 12) { + config.position.gps_update_interval = 21600; + } else if (selected == 13) { + config.position.gps_update_interval = 43200; + } else if (selected == 14) { + config.position.gps_update_interval = 86400; + } else if (selected == 15) { + config.position.gps_update_interval = 2147483647; // At Boot Only + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.gps_update_interval == 8) { + bannerOptions.InitialSelected = 1; + } else if (config.position.gps_update_interval == 20) { + bannerOptions.InitialSelected = 2; + } else if (config.position.gps_update_interval == 40) { + bannerOptions.InitialSelected = 3; + } else if (config.position.gps_update_interval == 60) { + bannerOptions.InitialSelected = 4; + } else if (config.position.gps_update_interval == 80) { + bannerOptions.InitialSelected = 5; + } else if (config.position.gps_update_interval == 120) { + bannerOptions.InitialSelected = 6; + } else if (config.position.gps_update_interval == 300) { + bannerOptions.InitialSelected = 7; + } else if (config.position.gps_update_interval == 600) { + bannerOptions.InitialSelected = 8; + } else if (config.position.gps_update_interval == 900) { + bannerOptions.InitialSelected = 9; + } else if (config.position.gps_update_interval == 1800) { + bannerOptions.InitialSelected = 10; + } else if (config.position.gps_update_interval == 3600) { + bannerOptions.InitialSelected = 11; + } else if (config.position.gps_update_interval == 21600) { + bannerOptions.InitialSelected = 12; + } else if (config.position.gps_update_interval == 43200) { + bannerOptions.InitialSelected = 13; + } else if (config.position.gps_update_interval == 86400) { + bannerOptions.InitialSelected = 14; + } else if (config.position.gps_update_interval == 2147483647) { // At Boot Only + bannerOptions.InitialSelected = 15; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::GPSPositionBroadcastMenu() +{ + static const char *optionsArray[] = {"Back", "1 minute", "90 seconds", "5 minutes", "15 minutes", "1 hour", + "2 hours", "3 hours", "4 hours", "5 hours", "6 hours", "12 hours", + "18 hours", "24 hours", "36 hours", "48 hours", "72 hours"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Broadcast Interval"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 17; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == 0) { + menuQueue = position_base_menu; + screen->runNow(); + } else if (selected == 1) { + config.position.position_broadcast_secs = 60; + } else if (selected == 2) { + config.position.position_broadcast_secs = 90; + } else if (selected == 3) { + config.position.position_broadcast_secs = 300; + } else if (selected == 4) { + config.position.position_broadcast_secs = 900; + } else if (selected == 5) { + config.position.position_broadcast_secs = 3600; + } else if (selected == 6) { + config.position.position_broadcast_secs = 7200; + } else if (selected == 7) { + config.position.position_broadcast_secs = 10800; + } else if (selected == 8) { + config.position.position_broadcast_secs = 14400; + } else if (selected == 9) { + config.position.position_broadcast_secs = 18000; + } else if (selected == 10) { + config.position.position_broadcast_secs = 21600; + } else if (selected == 11) { + config.position.position_broadcast_secs = 43200; + } else if (selected == 12) { + config.position.position_broadcast_secs = 64800; + } else if (selected == 13) { + config.position.position_broadcast_secs = 86400; + } else if (selected == 14) { + config.position.position_broadcast_secs = 129600; + } else if (selected == 15) { + config.position.position_broadcast_secs = 172800; + } else if (selected == 16) { + config.position.position_broadcast_secs = 259200; + } + + if (selected != 0) { + saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }; + + if (config.position.position_broadcast_secs == 60) { + bannerOptions.InitialSelected = 1; + } else if (config.position.position_broadcast_secs == 90) { + bannerOptions.InitialSelected = 2; + } else if (config.position.position_broadcast_secs == 300) { + bannerOptions.InitialSelected = 3; + } else if (config.position.position_broadcast_secs == 900) { + bannerOptions.InitialSelected = 4; + } else if (config.position.position_broadcast_secs == 3600) { + bannerOptions.InitialSelected = 5; + } else if (config.position.position_broadcast_secs == 7200) { + bannerOptions.InitialSelected = 6; + } else if (config.position.position_broadcast_secs == 10800) { + bannerOptions.InitialSelected = 7; + } else if (config.position.position_broadcast_secs == 14400) { + bannerOptions.InitialSelected = 8; + } else if (config.position.position_broadcast_secs == 18000) { + bannerOptions.InitialSelected = 9; + } else if (config.position.position_broadcast_secs == 21600) { + bannerOptions.InitialSelected = 10; + } else if (config.position.position_broadcast_secs == 43200) { + bannerOptions.InitialSelected = 11; + } else if (config.position.position_broadcast_secs == 64800) { + bannerOptions.InitialSelected = 12; + } else if (config.position.position_broadcast_secs == 86400) { + bannerOptions.InitialSelected = 13; + } else if (config.position.position_broadcast_secs == 129600) { + bannerOptions.InitialSelected = 14; + } else if (config.position.position_broadcast_secs == 172800) { + bannerOptions.InitialSelected = 15; + } else if (config.position.position_broadcast_secs == 259200) { + bannerOptions.InitialSelected = 16; + } else { + bannerOptions.InitialSelected = 0; + } + screen->showOverlayBanner(bannerOptions); +} + #endif void menuHandler::BluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Bluetooth"; -#else bannerOptions.message = "Toggle Bluetooth"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Bluetooth"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { @@ -950,9 +1756,9 @@ void menuHandler::BluetoothToggleMenu() void menuHandler::BuzzerModeMenu() { - static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"}; + static const char *optionsArray[] = {"All Enabled", "All Disabled", "Notifications", "System Only", "DMs Only"}; BannerOverlayOptions bannerOptions; - bannerOptions.message = "Buzzer Mode"; + bannerOptions.message = "Notification Sounds"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { @@ -1031,78 +1837,63 @@ void menuHandler::switchToMUIMenu() void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) { - static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal", - "Pink", "White"}; - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Select Screen Color"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 10; - bannerOptions.bannerCallback = [display](int selected) -> void { -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT - uint8_t TFT_MESH_r = 0; - uint8_t TFT_MESH_g = 0; - uint8_t TFT_MESH_b = 0; - if (selected == 1) { - LOG_INFO("Setting color to system default or defined variant"); - // Given just before we set all these to zero, we will allow this to go through - } else if (selected == 2) { - LOG_INFO("Setting color to Meshtastic Green"); - TFT_MESH_r = 103; - TFT_MESH_g = 234; - TFT_MESH_b = 148; - } else if (selected == 3) { - LOG_INFO("Setting color to Yellow"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 128; - } else if (selected == 4) { - LOG_INFO("Setting color to Red"); - TFT_MESH_r = 255; - TFT_MESH_g = 64; - TFT_MESH_b = 64; - } else if (selected == 5) { - LOG_INFO("Setting color to Orange"); - TFT_MESH_r = 255; - TFT_MESH_g = 160; - TFT_MESH_b = 20; - } else if (selected == 6) { - LOG_INFO("Setting color to Purple"); - TFT_MESH_r = 204; - TFT_MESH_g = 153; - TFT_MESH_b = 255; - } else if (selected == 7) { - LOG_INFO("Setting color to Teal"); - TFT_MESH_r = 64; - TFT_MESH_g = 224; - TFT_MESH_b = 208; - } else if (selected == 8) { - LOG_INFO("Setting color to Pink"); - TFT_MESH_r = 255; - TFT_MESH_g = 105; - TFT_MESH_b = 180; - } else if (selected == 9) { - LOG_INFO("Setting color to White"); - TFT_MESH_r = 255; - TFT_MESH_g = 255; - TFT_MESH_b = 255; - } else { - menuQueue = system_base_menu; - screen->runNow(); - } + static const ScreenColorOption colorOptions[] = { + {"Back", OptionsAction::Back}, + {"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)}, + {"Meshtastic Green", OptionsAction::Select, ScreenColor(103, 234, 148)}, + {"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)}, + {"Red", OptionsAction::Select, ScreenColor(255, 64, 64)}, + {"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)}, + {"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)}, + {"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)}, + {"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)}, + {"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)}, + {"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)}, + {"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)}, + {"White", OptionsAction::Select, ScreenColor(255, 255, 255)}, + {"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)}, + }; + + constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]); + static std::array colorLabels{}; + + auto bannerOptions = createStaticBannerOptions( + "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = system_base_menu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ + HAS_TFT || defined(HACKADAY_COMMUNICATOR) + const ScreenColor &color = option.value; + if (color.useVariant) { + LOG_INFO("Setting color to system default or defined variant"); + } else { + LOG_INFO("Setting color to %s", option.label); + } + + uint8_t r = color.r; + uint8_t g = color.g; + uint8_t b = color.b; - if (selected != 0) { display->setColor(BLACK); display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); display->setColor(WHITE); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { #ifdef TFT_MESH_OVERRIDE TFT_MESH = TFT_MESH_OVERRIDE; #else TFT_MESH = COLOR565(0x67, 0xEA, 0x94); #endif } else { - TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); + TFT_MESH = COLOR565(r, g, b); } #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) @@ -1110,16 +1901,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) #endif screen->setFrames(graphics::Screen::FOCUS_SYSTEM); - if (TFT_MESH_r == 0 && TFT_MESH_g == 0 && TFT_MESH_b == 0) { + if (color.useVariant || (r == 0 && g == 0 && b == 0)) { uiconfig.screen_rgb_color = 0; } else { - uiconfig.screen_rgb_color = (TFT_MESH_r << 16) | (TFT_MESH_g << 8) | TFT_MESH_b; + uiconfig.screen_rgb_color = + (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); } LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color); saveUIConfig(); - } #endif - }; + }); + + int initialSelection = 0; + if (uiconfig.screen_rgb_color == 0) { + initialSelection = 1; + } else { + uint32_t currentColor = uiconfig.screen_rgb_color; + for (size_t i = 0; i < colorCount; ++i) { + if (!colorOptions[i].hasValue) { + continue; + } + const ScreenColor &color = colorOptions[i].value; + if (color.useVariant) { + continue; + } + uint32_t encoded = + (static_cast(color.r) << 16) | (static_cast(color.g) << 8) | static_cast(color.b); + if (encoded == currentColor) { + initialSelection = static_cast(i); + break; + } + } + } + bannerOptions.InitialSelected = initialSelection; + screen->showOverlayBanner(bannerOptions); } @@ -1127,17 +1942,17 @@ void menuHandler::rebootMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Reboot"; -#else bannerOptions.message = "Reboot Device?"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Reboot"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 1) { IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); + messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { menuQueue = power_menu; @@ -1151,11 +1966,10 @@ void menuHandler::shutdownMenu() { static const char *optionsArray[] = {"Back", "Confirm"}; BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Shutdown"; -#else bannerOptions.message = "Shutdown Device?"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Shutdown"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { @@ -1172,12 +1986,13 @@ void menuHandler::shutdownMenu() void menuHandler::addFavoriteMenu() { -#if defined(M5STACK_UNITC6L) - screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void { -#else - screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { - -#endif + const char *NODE_PICKER_TITLE; + if (currentResolution == ScreenResolution::UltraLow) { + NODE_PICKER_TITLE = "Node Favorite"; + } else { + NODE_PICKER_TITLE = "Node To Favorite"; + } + screen->showNodePicker(NODE_PICKER_TITLE, 30000, [](uint32_t nodenum) -> void { LOG_WARN("Nodenum: %u", nodenum); nodeDB->set_favorite(true, nodenum); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); @@ -1278,43 +2093,28 @@ void menuHandler::wifiBaseMenu() void menuHandler::wifiToggleMenu() { - enum optionsNumbers { Back, Wifi_toggle }; + enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; - static const char *optionsArray[] = {"Back", "Disable"}; + static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; BannerOverlayOptions bannerOptions; - bannerOptions.message = "Disable Wifi and\nEnable Bluetooth?"; + bannerOptions.message = "WiFi Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = 3; + if (config.network.wifi_enabled == true) + bannerOptions.InitialSelected = 2; + else + bannerOptions.InitialSelected = 1; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Wifi_toggle) { + if (selected == Wifi_disable) { config.network.wifi_enabled = false; config.bluetooth.enabled = true; service->reloadConfig(SEGMENT_CONFIG); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }; - screen->showOverlayBanner(bannerOptions); -} - -void menuHandler::notificationsMenu() -{ - enum optionsNumbers { Back, BuzzerActions }; - static const char *optionsArray[] = {"Back", "Buzzer Actions"}; - static int optionsEnumArray[] = {Back, BuzzerActions}; - int options = 2; - - BannerOverlayOptions bannerOptions; - bannerOptions.message = "Notifications"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = options; - bannerOptions.optionsEnumPtr = optionsEnumArray; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == BuzzerActions) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; - screen->runNow(); - } else { - menuQueue = system_base_menu; - screen->runNow(); + } else if (selected == Wifi_enable) { + config.network.wifi_enabled = true; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } }; screen->showOverlayBanner(bannerOptions); @@ -1333,16 +2133,11 @@ void menuHandler::screenOptionsMenu() hasSupportBrightness = false; #endif - enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits }; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; static const char *optionsArray[5] = {"Back"}; static int optionsEnumArray[5] = {Back}; int options = 1; -#if defined(T_DECK) || defined(T_LORA_PAGER) - optionsArray[options] = "Show Long/Short Name"; - optionsEnumArray[options++] = NodeNameLength; -#endif - // Only show brightness for B&W displays if (hasSupportBrightness) { optionsArray[options] = "Brightness"; @@ -1350,12 +2145,13 @@ void menuHandler::screenOptionsMenu() } // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ + HAS_TFT || defined(HACKADAY_COMMUNICATOR) optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif - optionsArray[options] = "Frame Visibility Toggle"; + optionsArray[options] = "Frame Visibility"; optionsEnumArray[options++] = FrameToggles; optionsArray[options] = "Display Units"; @@ -1373,9 +2169,6 @@ void menuHandler::screenOptionsMenu() } else if (selected == ScreenColor) { menuHandler::menuQueue = menuHandler::tftcolormenupicker; screen->runNow(); - } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; - screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -1410,11 +2203,10 @@ void menuHandler::powerMenu() #endif BannerOverlayOptions bannerOptions; -#if defined(M5STACK_UNITC6L) - bannerOptions.message = "Power"; -#else bannerOptions.message = "Reboot / Shutdown"; -#endif + if (currentResolution == ScreenResolution::UltraLow) { + bannerOptions.message = "Power"; + } bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = options; bannerOptions.optionsEnumPtr = optionsEnumArray; @@ -1471,7 +2263,8 @@ void menuHandler::FrameToggles_menu() { enum optionsNumbers { Finish, - nodelist, + nodelist_nodes, + nodelist_location, nodelist_lastheard, nodelist_hopsignal, nodelist_distance, @@ -1492,20 +2285,25 @@ void menuHandler::FrameToggles_menu() static int lastSelectedIndex = 0; #ifndef USE_EINK - optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; - optionsEnumArray[options++] = nodelist; -#endif -#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; + optionsEnumArray[options++] = nodelist_nodes; +#else optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsEnumArray[options++] = nodelist_lastheard; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsEnumArray[options++] = nodelist_hopsignal; +#endif + +#if HAS_GPS +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists"; + optionsEnumArray[options++] = nodelist_location; +#else optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsEnumArray[options++] = nodelist_distance; -#endif -#if HAS_GPS - optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings"; optionsEnumArray[options++] = nodelist_bearings; +#endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; optionsEnumArray[options++] = gps; @@ -1544,8 +2342,12 @@ void menuHandler::FrameToggles_menu() if (selected == Finish) { screen->setFrames(Screen::FOCUS_DEFAULT); - } else if (selected == nodelist) { - screen->toggleFrameVisibility("nodelist"); + } else if (selected == nodelist_nodes) { + screen->toggleFrameVisibility("nodelist_nodes"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_location) { + screen->toggleFrameVisibility("nodelist_location"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); } else if (selected == nodelist_lastheard) { @@ -1661,6 +2463,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case position_base_menu: positionBaseMenu(); break; + case node_base_menu: + nodeListMenu(); + break; #if !MESHTASTIC_EXCLUDE_GPS case gps_toggle_menu: GPSToggleMenu(); @@ -1668,6 +2473,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case gps_format_menu: GPSFormatMenu(); break; + case gps_smart_position_menu: + GPSSmartPositionMenu(); + break; + case gps_update_interval_menu: + GPSUpdateIntervalMenu(); + break; + case gps_position_broadcast_menu: + GPSPositionBroadcastMenu(); + break; #endif case compass_point_north_menu: compassNorthMenu(); @@ -1723,9 +2537,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case bluetooth_toggle_menu: BluetoothToggleMenu(); break; - case notifications_menu: - notificationsMenu(); - break; case screen_options_menu: screenOptionsMenu(); break; @@ -1741,6 +2552,18 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case throttle_message: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; + case message_response_menu: + messageResponseMenu(); + break; + case reply_menu: + replyMenu(); + break; + case delete_messages_menu: + deleteMessagesMenu(); + break; + case message_viewmode_menu: + messageViewModeMenu(); + break; } menuQueue = menu_none; } @@ -1752,4 +2575,4 @@ void menuHandler::saveUIConfig() } // namespace graphics -#endif +#endif \ No newline at end of file diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index a611b7c9d..445513e25 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -19,8 +19,12 @@ class menuHandler clock_face_picker, clock_menu, position_base_menu, + node_base_menu, gps_toggle_menu, gps_format_menu, + gps_smart_position_menu, + gps_update_interval_menu, + gps_position_broadcast_menu, compass_point_north_menu, reset_node_db_menu, buzzermodemenupicker, @@ -35,7 +39,6 @@ class menuHandler number_test, wifi_toggle_menu, bluetooth_toggle_menu, - notifications_menu, screen_options_menu, power_menu, system_base_menu, @@ -43,6 +46,10 @@ class menuHandler key_verification_final_prompt, trace_route_menu, throttle_message, + message_response_menu, + message_viewmode_menu, + reply_menu, + delete_messages_menu, node_name_length_menu, FrameToggles, DisplayUnits @@ -61,6 +68,9 @@ class menuHandler static void TwelveHourPicker(); static void ClockFacePicker(); static void messageResponseMenu(); + static void messageViewModeMenu(); + static void replyMenu(); + static void deleteMessagesMenu(); static void homeBaseMenu(); static void textMessageBaseMenu(); static void systemBaseMenu(); @@ -69,6 +79,9 @@ class menuHandler static void compassNorthMenu(); static void GPSToggleMenu(); static void GPSFormatMenu(); + static void GPSSmartPositionMenu(); + static void GPSUpdateIntervalMenu(); + static void GPSPositionBroadcastMenu(); static void BuzzerModeMenu(); static void switchToMUIMenu(); static void TFTColorPickerMenu(OLEDDisplay *display); @@ -84,7 +97,6 @@ class menuHandler static void numberTest(); static void wifiBaseMenu(); static void wifiToggleMenu(); - static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); @@ -99,5 +111,45 @@ class menuHandler static void BluetoothToggleMenu(); }; +/* Generic Menu Options designations */ +enum class OptionsAction { Back, Select }; + +template struct MenuOption { + const char *label; + OptionsAction action; + bool hasValue; + T value; + + MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) + : label(labelIn), action(actionIn), hasValue(true), value(valueIn) + { + } + + MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {} +}; + +struct ScreenColor { + uint8_t r; + uint8_t g; + uint8_t b; + bool useVariant; + + ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) + : r(rIn), g(gIn), b(bIn), useVariant(variantIn) + { + } +}; + +using RadioPresetOption = MenuOption; +using LoraRegionOption = MenuOption; +using TimezoneOption = MenuOption; +using CompassOption = MenuOption; +using ScreenColorOption = MenuOption; +using GPSToggleOption = MenuOption; +using GPSFormatOption = MenuOption; +using NodeNameOption = MenuOption; +using PositionMenuOption = MenuOption; +using ClockFaceOption = MenuOption; + } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index da6ec7abc..09b798e06 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -1,51 +1,27 @@ -/* -BaseUI - -Developed and Maintained By: -- Ronald Garcia (HarukiToreda) – Lead development and implementation. -- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. -- TonyG (Tropho) – Project management, structural planning, and testing - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - #include "configuration.h" #if HAS_SCREEN #include "MessageRenderer.h" // Core includes +#include "MessageStore.h" #include "NodeDB.h" +#include "UIRenderer.h" #include "configuration.h" #include "gps/RTC.h" +#include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/TimeFormatters.h" #include "graphics/emotes.h" #include "main.h" #include "meshUtils.h" - -// Additional includes for UI rendering -#include "UIRenderer.h" -#include "graphics/TimeFormatters.h" - -// Additional includes for dependencies #include #include // External declarations extern bool hasUnreadMessage; extern meshtastic_DeviceState devicestate; +extern graphics::Screen *screen; using graphics::Emote; using graphics::emotes; @@ -56,17 +32,105 @@ namespace graphics namespace MessageRenderer { -// Simple cache based on text hash -static size_t cachedKey = 0; 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 +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; +uint32_t scrollStartDelay = 0; +uint32_t pauseStart = 0; +bool waitingToReset = false; +bool scrollStarted = false; +static bool didReset = false; + +void scrollUp() +{ + manualScrolling = true; + scrollY -= 12; + if (scrollY < 0) + scrollY = 0; +} + +void scrollDown() +{ + manualScrolling = true; + + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; + + int visibleHeight = screen->getHeight() - (FONT_HEIGHT_SMALL * 2); + int maxScroll = totalHeight - visibleHeight; + if (maxScroll < 0) + maxScroll = 0; + + scrollY += 12; + if (scrollY > maxScroll) + scrollY = maxScroll; +} void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { + std::string renderLine; + for (size_t i = 0; i < line.size();) { + uint8_t c = (uint8_t)line[i]; + size_t len = utf8CharLen(c); + if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { + i += 3; + continue; + } + if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && + ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { + i += 4; + continue; + } + renderLine.append(line, i, len); + i += len; + } int cursorX = x; const int fontHeight = FONT_HEIGHT_SMALL; - // === Step 1: Find tallest emote in the line === + // Step 1: Find tallest emote in the line int maxIconHeight = fontHeight; for (size_t i = 0; i < line.length();) { bool matched = false; @@ -81,25 +145,16 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string } } if (!matched) { - uint8_t c = static_cast(line[i]); - if ((c & 0xE0) == 0xC0) - i += 2; - else if ((c & 0xF0) == 0xE0) - i += 3; - else if ((c & 0xF8) == 0xF0) - i += 4; - else - i += 1; + i += utf8CharLen(static_cast(line[i])); } } - // === Step 2: Baseline alignment === + // Step 2: Baseline alignment int lineHeight = std::max(fontHeight, maxIconHeight); int baselineOffset = (lineHeight - fontHeight) / 2; int fontY = y + baselineOffset; - int fontMidline = fontY + fontHeight / 2; - // === Step 3: Render line in segments === + // Step 3: Render line in segments size_t i = 0; bool inBold = false; @@ -148,10 +203,12 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string // Render the emote (if found) if (matchedEmote && i == nextEmotePos) { - int iconY = fontMidline - matchedEmote->height / 2 - 1; + // Vertically center emote relative to font baseline (not just midline) + int iconY = fontY + (fontHeight - 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); @@ -164,235 +221,471 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string #else cursorX += display->getStringWidth(remaining.c_str()); #endif - break; } } } +// Reset scroll state when new messages arrive +void resetScrollState() +{ + scrollY = 0.0f; + scrollStarted = false; + waitingToReset = false; + scrollStartDelay = millis(); + lastTime = millis(); + manualScrolling = false; + didReset = false; +} + +void nudgeScroll(int8_t direction) +{ + if (direction == 0) + return; + + if (cachedHeights.empty()) { + scrollY = 0.0f; + return; + } + + OLEDDisplay *display = (screen != nullptr) ? screen->getDisplayDevice() : nullptr; + const int displayHeight = display ? display->getHeight() : 64; + const int navHeight = FONT_HEIGHT_SMALL; + const int usableHeight = std::max(0, displayHeight - navHeight); + + int totalHeight = 0; + for (int h : cachedHeights) + totalHeight += h; + + if (totalHeight <= usableHeight) { + scrollY = 0.0f; + return; + } + + const int scrollStop = std::max(0, totalHeight - usableHeight + cachedHeights.back()); + const int step = std::max(FONT_HEIGHT_SMALL, usableHeight / 3); + + float newScroll = scrollY + static_cast(direction) * static_cast(step); + if (newScroll < 0.0f) + newScroll = 0.0f; + if (newScroll > scrollStop) + newScroll = static_cast(scrollStop); + + if (newScroll != scrollY) { + scrollY = newScroll; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = millis(); + lastTime = millis(); + } +} + +// Fully free cached message data from heap +void clearMessageCache() +{ + std::vector().swap(cachedLines); + std::vector().swap(cachedHeights); + + // Reset scroll so we rebuild cleanly next time we enter the screen + resetScrollState(); +} + +// Current thread state +static ThreadMode currentMode = ThreadMode::ALL; +static int currentChannel = -1; +static uint32_t currentPeer = 0; + +// Registry of seen threads for manual toggle +static std::vector seenChannels; +static std::vector seenPeers; + +// Public helper so menus / store can clear stale registries +void clearThreadRegistries() +{ + seenChannels.clear(); + seenPeers.clear(); +} + +// Setter so other code can switch threads +void setThreadMode(ThreadMode mode, int channel /* = -1 */, uint32_t peer /* = 0 */) +{ + currentMode = mode; + currentChannel = channel; + currentPeer = peer; + didReset = false; // force reset when mode changes + + // Track channels we’ve seen + if (mode == ThreadMode::CHANNEL && channel >= 0) { + if (std::find(seenChannels.begin(), seenChannels.end(), channel) == seenChannels.end()) { + seenChannels.push_back(channel); + } + } + + // Track DMs we’ve seen + if (mode == ThreadMode::DIRECT && peer != 0) { + if (std::find(seenPeers.begin(), seenPeers.end(), peer) == seenPeers.end()) { + seenPeers.push_back(peer); + } + } +} + +ThreadMode getThreadMode() +{ + return currentMode; +} + +int getThreadChannel() +{ + return currentChannel; +} + +uint32_t getThreadPeer() +{ + return currentPeer; +} + +// Accessors for menuHandler +const std::vector &getSeenChannels() +{ + return seenChannels; +} +const std::vector &getSeenPeers() +{ + return seenPeers; +} + +static int centerYForRow(int y, int size) +{ + int midY = y + (FONT_HEIGHT_SMALL / 2); + return midY - (size / 2); +} + +// Helpers for drawing status marks (thickened strokes) +static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY + size / 2, x + size / 3, topY + size); + display->drawLine(x, topY + size / 2 + 1, x + size / 3, topY + size + 1); + display->drawLine(x + size / 3, topY + size, x + size, topY); + display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); +} + +static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int topY = centerYForRow(y, size); + display->setColor(WHITE); + display->drawLine(x, topY, x + size, topY + size); + display->drawLine(x, topY + 1, x + size, topY + size + 1); + display->drawLine(x + size, topY, x, topY + size); + display->drawLine(x + size, topY + 1, x, topY + size + 1); +} + +static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) +{ + int r = size / 2; + int centerY = centerYForRow(y, size) + r; + int centerX = x + r; + display->setColor(WHITE); + display->drawCircle(centerX, centerY, r); + display->drawLine(centerX, centerY - 2, centerX, centerY); + display->setPixel(centerX, centerY + 2); + display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); +} + +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; +} + +static void drawMessageScrollbar(OLEDDisplay *display, int visibleHeight, int totalHeight, int scrollOffset, int startY) +{ + if (totalHeight <= visibleHeight) + return; // no scrollbar needed + + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = visibleHeight; + int thumbHeight = std::max(6, (scrollbarHeight * visibleHeight) / totalHeight); + int maxScroll = std::max(1, totalHeight - visibleHeight); + int thumbY = startY + (scrollbarHeight - thumbHeight) * scrollOffset / maxScroll; + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } +} + void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Ensure any boot-relative timestamps are upgraded if RTC is valid + messageStore.upgradeBootRelativeTimestamps(); + + if (!didReset) { + resetScrollState(); + didReset = true; + } + // Clear the unread message indicator when viewing the message hasUnreadMessage = false; - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + // Filter messages based on thread mode + std::deque filtered; + for (const auto &m : messageStore.getLiveMessages()) { + bool include = false; + switch (currentMode) { + case ThreadMode::ALL: + include = true; + break; + case ThreadMode::CHANNEL: + if (m.type == MessageType::BROADCAST && (int)m.channelIndex == currentChannel) + include = true; + break; + case ThreadMode::DIRECT: + if (m.dest != NODENUM_BROADCAST && (m.sender == currentPeer || m.dest == currentPeer)) + include = true; + break; + } + if (include) + filtered.push_back(m); + } display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); -#if defined(M5STACK_UNITC6L) - const int fixedTopHeight = 24; - const int windowX = 0; - const int windowY = fixedTopHeight; - const int windowWidth = 64; - const int windowHeight = SCREEN_HEIGHT - fixedTopHeight; -#else const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; - const int textWidth = SCREEN_WIDTH; + constexpr int LEFT_MARGIN = 2; + constexpr int RIGHT_MARGIN = 2; + constexpr int SCROLLBAR_WIDTH = 3; -#endif - bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - bool isBold = config.display.heading_bold; + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; - // === Set Title + const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; + + // Title string depending on mode + static char titleBuf[32]; const char *titleStr = "Messages"; + switch (currentMode) { + case ThreadMode::ALL: + titleStr = "Messages"; + break; + case ThreadMode::CHANNEL: { + const char *cname = channels.getName(currentChannel); + if (cname && cname[0]) { + snprintf(titleBuf, sizeof(titleBuf), "#%s", cname); + } else { + snprintf(titleBuf, sizeof(titleBuf), "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); + } else { + snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer); + } + titleStr = titleBuf; + break; + } + } - // Check if we have more than an empty message to show - char messageBuf[237]; - snprintf(messageBuf, sizeof(messageBuf), "%s", msg); - if (strlen(messageBuf) == 0) { - // === Header === + if (filtered.empty()) { + // If current conversation is empty go back to ALL view + if (currentMode != ThreadMode::ALL) { + setThreadMode(ThreadMode::ALL); + resetScrollState(); + return; // Next draw will rerun in ALL mode + } + + // Still in ALL mode and no messages at all → show placeholder graphics::drawCommonHeader(display, x, y, titleStr); + didReset = false; const char *messageString = "No messages"; int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); -#if defined(M5STACK_UNITC6L) - display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString); -#else display->drawString(center_text, getTextPositions(display)[2], messageString); -#endif graphics::drawCommonFooter(display, x, y); return; } - // === Header Construction === - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - char headerStr[80]; - const char *sender = "???"; -#if defined(M5STACK_UNITC6L) - if (node && node->has_user) - sender = node->user.short_name; -#else - if (node && node->has_user) { - if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { - sender = node->user.long_name; - } else { - sender = node->user.short_name; - } - } -#endif - uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + // Build lines for filtered messages (newest first) + std::vector allLines; + std::vector isMine; // track alignment + std::vector isHeader; // track header lines + std::vector ackForLine; - if (useTimestamp && minutes >= 15 && daysAgo == 0) { - std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; - if (config.display.use_12h_clock) { - bool isPM = timestampHours >= 12; - timestampHours = timestampHours % 12; - if (timestampHours == 0) - timestampHours = 12; - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, - isPM ? "p" : "a", sender); - } else { - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, - sender); - } - } else { -#if defined(M5STACK_UNITC6L) - snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), - sender); -#else - snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), - sender); -#endif - } -#if defined(M5STACK_UNITC6L) - graphics::drawCommonHeader(display, x, y, titleStr); - int headerY = getTextPositions(display)[1]; - display->drawString(x, headerY, headerStr); - for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) { - display->setPixel(separatorX, fixedTopHeight - 1); - } - cachedLines.clear(); - std::string fullMsg(messageBuf); - std::string currentLine; - for (size_t i = 0; i < fullMsg.size();) { - unsigned char c = fullMsg[i]; - size_t charLen = 1; - if ((c & 0xE0) == 0xC0) - charLen = 2; - else if ((c & 0xF0) == 0xE0) - charLen = 3; - else if ((c & 0xF8) == 0xF0) - charLen = 4; - std::string nextChar = fullMsg.substr(i, charLen); - std::string testLine = currentLine + nextChar; - if (display->getStringWidth(testLine.c_str()) > windowWidth) { - cachedLines.push_back(currentLine); - currentLine = nextChar; - } else { - currentLine = testLine; - } + for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) { + const auto &m = *it; - i += charLen; - } - if (!currentLine.empty()) - cachedLines.push_back(currentLine); - cachedHeights = calculateLineHeights(cachedLines, emotes); - int yOffset = windowY; - int linesDrawn = 0; - for (size_t i = 0; i < cachedLines.size(); ++i) { - if (linesDrawn >= 2) - break; - int lineHeight = cachedHeights[i]; - if (yOffset + lineHeight > windowY + windowHeight) - break; - drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes); - yOffset += lineHeight; - linesDrawn++; - } - screen->forceDisplay(); -#else - uint32_t now = millis(); -#ifndef EXCLUDE_EMOJI - // === Bounce animation setup === - static uint32_t lastBounceTime = 0; - static int bounceY = 0; - const int bounceRange = 2; // Max pixels to bounce up/down - const int bounceInterval = 10; // How quickly to change bounce direction (ms) - - if (now - lastBounceTime >= bounceInterval) { - lastBounceTime = now; - bounceY = (bounceY + 1) % (bounceRange * 2); - } - for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (strcmp(msg, e.label) == 0) { - int headerY = getTextPositions(display)[1]; // same as scrolling header line - display->drawString(x + 3, headerY, headerStr); - if (isInverted && isBold) - display->drawString(x + 4, headerY, headerStr); - - // Draw separator (same as scroll version) - for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { - display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13)); + // Channel / destination labeling + char chanType[32] = ""; + if (currentMode == ThreadMode::ALL) { + if (m.dest == NODENUM_BROADCAST) { + snprintf(chanType, sizeof(chanType), "#%s", channels.getName(m.channelIndex)); + } else { + snprintf(chanType, sizeof(chanType), "(DM)"); } + } - // Center the emote below the header line + separator + nav - int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; - int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; - display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + // Calculate how long ago + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + uint32_t seconds = 0; + bool invalidTime = true; - // Draw header at the end to sort out overlapping elements - graphics::drawCommonHeader(display, x, y, titleStr); - return; + if (m.timestamp > 0 && nowSecs > 0) { + if (nowSecs >= m.timestamp) { + seconds = nowSecs - m.timestamp; + invalidTime = (seconds > 315360000); // >10 years + } else { + uint32_t ahead = m.timestamp - nowSecs; + if (ahead <= 600) { // allow small skew + seconds = 0; + invalidTime = false; + } + } + } else if (m.timestamp > 0 && nowSecs == 0) { + // RTC not valid: only trust boot-relative if same boot + uint32_t bootNow = millis() / 1000; + if (m.isBootRelative && m.timestamp <= bootNow) { + seconds = bootNow - m.timestamp; + invalidTime = false; + } else { + invalidTime = true; // old persisted boot-relative, ignore until healed + } + } + + char timeBuf[16]; + if (invalidTime) { + snprintf(timeBuf, sizeof(timeBuf), "???"); + } else if (seconds < 60) { + snprintf(timeBuf, sizeof(timeBuf), "%us", seconds); + } else if (seconds < 3600) { + snprintf(timeBuf, sizeof(timeBuf), "%um", seconds / 60); + } else if (seconds < 86400) { + snprintf(timeBuf, sizeof(timeBuf), "%uh", seconds / 3600); + } else { + snprintf(timeBuf, sizeof(timeBuf), "%ud", seconds / 86400); + } + + // Build header line for this message + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); + meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); + + char senderBuf[48] = ""; + 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 this is *our own* message, override senderBuf 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); + } + + // Shrink Sender name if needed + int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - + display->getStringWidth(" @...") - 10; + 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, "..."); + } + + // Final header line + char headerStr[96]; + if (mine) { + if (currentMode == ThreadMode::ALL) { + if (strcmp(chanType, "(DM)") == 0) { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf); + } else { + snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType); + } + + // Push header line + allLines.push_back(std::string(headerStr)); + isMine.push_back(mine); + isHeader.push_back(true); + ackForLine.push_back(m.ackStatus); + + const char *msgText = MessageStore::getText(m); + + int wrapWidth = mine ? rightTextWidth : leftTextWidth; + std::vector wrapped = generateLines(display, "", msgText, wrapWidth); + for (auto &ln : wrapped) { + allLines.push_back(ln); + isMine.push_back(mine); + isHeader.push_back(false); + ackForLine.push_back(AckStatus::NONE); } } -#endif - // === Generate the cache key === - size_t currentKey = (size_t)mp.from; - currentKey ^= ((size_t)mp.to << 8); - currentKey ^= ((size_t)mp.rx_time << 16); - currentKey ^= ((size_t)mp.id << 24); - if (cachedKey != currentKey) { - LOG_INFO("Onscreen message scroll cache key needs updating: cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey); + // Cache lines and heights + cachedLines = allLines; + cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader); - // Cache miss - regenerate lines and heights - cachedLines = generateLines(display, headerStr, messageBuf, textWidth); - cachedHeights = calculateLineHeights(cachedLines, emotes); - cachedKey = currentKey; - } else { - // Cache hit but update the header line with current time information - cachedLines[0] = std::string(headerStr); - // The header always has a fixed height since it doesn't contain emotes - // As per calculateLineHeights logic for lines without emotes: - cachedHeights[0] = FONT_HEIGHT_SMALL - 2; - if (cachedHeights[0] < 8) - cachedHeights[0] = 8; // minimum safety - } - - // === Scrolling logic === + // Scrolling logic (unchanged) int totalHeight = 0; - for (size_t i = 1; i < cachedHeights.size(); ++i) { + for (size_t i = 0; i < cachedHeights.size(); ++i) totalHeight += cachedHeights[i]; - } - int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height + int usableScrollHeight = usableHeight; int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); - static float scrollY = 0.0f; - static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; - static bool waitingToReset = false, scrollStarted = false; - - // === Smooth scrolling adjustment === - // You can tweak this divisor to change how smooth it scrolls. - // Lower = smoother, but can feel slow. +#ifndef USE_EINK + uint32_t now = millis(); float delta = (now - lastTime) / 400.0f; lastTime = now; + const float scrollSpeed = 2.0f; - const float scrollSpeed = 2.0f; // pixels per second - - // Delay scrolling start by 2 seconds if (scrollStartDelay == 0) scrollStartDelay = now; if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true; - if (totalHeight > usableScrollHeight) { + if (!manualScrolling && totalHeight > usableScrollHeight) { if (scrollStarted) { if (!waitingToReset) { scrollY += delta * scrollSpeed; @@ -408,29 +701,96 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 scrollStartDelay = lastTime; } } - } else { + } else if (!manualScrolling) { scrollY = 0; } - - int scrollOffset = static_cast(scrollY); - int yOffset = -scrollOffset + getTextPositions(display)[1]; - for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { - display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13)); - } - - // === Render visible lines === - renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold); - - // Draw header at the end to sort out overlapping elements - graphics::drawCommonHeader(display, x, y, titleStr); +#else + // E-Ink: disable autoscroll + scrollY = 0.0f; + waitingToReset = false; + scrollStarted = false; + lastTime = millis(); #endif + + int finalScroll = (int)scrollY; + int yOffset = -finalScroll + getTextPositions(display)[1]; + + // Render visible lines + for (size_t i = 0; i < cachedLines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += cachedHeights[j]; + + if (lineY > -cachedHeights[i] && lineY < scrollBottom) { + if (isHeader[i]) { + + int w = display->getStringWidth(cachedLines[i].c_str()); + int headerX; + if (isMine[i]) { + // push header left to avoid overlap with scrollbar + headerX = SCREEN_WIDTH - w - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (headerX < LEFT_MARGIN) + headerX = LEFT_MARGIN; + } else { + headerX = x; + } + display->drawString(headerX, lineY, cachedLines[i].c_str()); + + // Draw ACK/NACK mark for our own messages + if (isMine[i]) { + int markX = headerX - 10; + int markY = lineY; + if (ackForLine[i] == AckStatus::ACKED) { + // Destination ACK + drawCheckMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::NACKED || ackForLine[i] == AckStatus::TIMEOUT) { + // Failure or timeout + drawXMark(display, markX, markY, 8); + } else if (ackForLine[i] == AckStatus::RELAYED) { + // Relay ACK + drawRelayMark(display, markX, markY, 8); + } + // AckStatus::NONE → show nothing + } + + // Draw underline just under header text + int underlineY = lineY + FONT_HEIGHT_SMALL; + for (int px = 0; px < w; ++px) { + display->setPixel(headerX + px, underlineY); + } + } else { + // Render message line + if (isMine[i]) { + // Calculate actual rendered width including emotes + int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN; + if (rightX < LEFT_MARGIN) + rightX = LEFT_MARGIN; + + drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); + } else { + drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes); + } + } + } + } + int totalContentHeight = totalHeight; + int visibleHeight = usableHeight; + + // Draw scrollbar + drawMessageScrollbar(display, visibleHeight, totalContentHeight, finalScroll, getTextPositions(display)[1]); + graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonFooter(display, x, y); } std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) { std::vector lines; - lines.push_back(std::string(headerStr)); // Header line is always first + + // Only push headerStr if it's not empty (prevents extra blank line after headers) + if (headerStr && headerStr[0] != '\0') { + lines.push_back(std::string(headerStr)); + } std::string line, word; for (int i = 0; messageBuf[i]; ++i) { @@ -453,10 +813,6 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS } else { word += ch; std::string test = line + word; -// Keep these lines for diagnostics -// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch); -// LOG_INFO("Current String: %s", test.c_str()); -// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :) #if defined(OLED_UA) || defined(OLED_RU) uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true); #else @@ -478,28 +834,71 @@ std::vector generateLines(OLEDDisplay *display, const char *headerS return lines; } - -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes) +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec) { + // Tunables for layout control + constexpr int HEADER_UNDERLINE_GAP = 0; // space between underline and first body line + constexpr int HEADER_UNDERLINE_PIX = 1; // underline thickness (1px row drawn) + constexpr int BODY_LINE_LEADING = -4; // default vertical leading for normal body lines + constexpr int MESSAGE_BLOCK_GAP = 4; // gap after a message block before a new header + constexpr int EMOTE_PADDING_ABOVE = 4; // space above emote line (added to line above) + constexpr int EMOTE_PADDING_BELOW = 3; // space below emote line (added to emote line) + std::vector rowHeights; + rowHeights.reserve(lines.size()); - for (const auto &_line : lines) { - int lineHeight = FONT_HEIGHT_SMALL; + for (size_t idx = 0; idx < lines.size(); ++idx) { + const auto &line = lines[idx]; + const int baseHeight = FONT_HEIGHT_SMALL; + + // Detect if THIS line or NEXT line contains an emote bool hasEmote = false; - + int tallestEmote = baseHeight; for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (_line.find(e.label) != std::string::npos) { - lineHeight = std::max(lineHeight, e.height); + if (line.find(emotes[i].label) != std::string::npos) { hasEmote = true; + tallestEmote = std::max(tallestEmote, emotes[i].height); } } - // Apply tighter spacing if no emotes on this line - if (!hasEmote) { - lineHeight -= 2; // reduce by 2px for tighter spacing - if (lineHeight < 8) - lineHeight = 8; // minimum safety + 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; + } + } + } + + int lineHeight = baseHeight; + + if (isHeaderVec[idx]) { + // Header line spacing + lineHeight = baseHeight + HEADER_UNDERLINE_PIX + HEADER_UNDERLINE_GAP; + } else { + // Base spacing for normal lines + int desiredBody = baseHeight + BODY_LINE_LEADING; + + if (hasEmote) { + // Emote line: add overshoot + bottom padding + int overshoot = std::max(0, tallestEmote - baseHeight); + lineHeight = desiredBody + overshoot + EMOTE_PADDING_BELOW; + } else { + // Regular line: no emote → standard spacing + lineHeight = desiredBody; + + // If next line has an emote → add top padding *here* + if (nextHasEmote) { + lineHeight += EMOTE_PADDING_ABOVE; + } + } + + // Add block gap if next is a header + if (idx + 1 < lines.size() && isHeaderVec[idx + 1]) { + lineHeight += MESSAGE_BLOCK_GAP; + } } rowHeights.push_back(lineHeight); @@ -508,22 +907,125 @@ std::vector calculateLineHeights(const std::vector &lines, con return rowHeights; } -void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, - int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold) +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet) { - for (size_t i = 0; i < lines.size(); ++i) { - int lineY = yOffset; - for (size_t j = 0; j < i; ++j) - lineY += rowHeights[j]; - if (lineY > -rowHeights[i] && lineY < scrollBottom) { - if (i == 0 && isInverted) { - display->drawString(x, lineY, lines[i].c_str()); - if (isBold) - display->drawString(x, lineY, lines[i].c_str()); - } else { - drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); + if (packet.from != 0) { + hasUnreadMessage = true; + + // Determine if message belongs to a muted channel + bool isChannelMuted = false; + if (sm.type == MessageType::BROADCAST) { + const meshtastic_Channel channel = channels.getByIndex(packet.channel ? packet.channel : channels.getPrimaryIndex()); + if (channel.settings.has_module_settings && channel.settings.module_settings.is_muted) + isChannelMuted = true; + } + + // 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'; + } + 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, "..."); + } + const char *msgRaw = reinterpret_cast(packet.decoded.payload.bytes); + + char banner[256]; + bool isAlert = false; + + // Check if alert detection is enabled via external notification module + if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || + moduleConfig.external_notification.alert_bell_buzzer) { + for (size_t i = 0; i < packet.decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } } } + + if (isAlert) { + if (longName && longName[0]) + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + else + strcpy(banner, "Alert Received"); + } else { + // Skip muted channels unless it's an alert + if (isChannelMuted) + return; + + if (longName && longName[0]) { + if (currentResolution == ScreenResolution::UltraLow) { + strcpy(banner, "New Message"); + } else { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } + } else + strcpy(banner, "New Message"); + } + + // Append context (which channel or DM) so the banner shows where the message arrived + { + char contextBuf[64] = ""; + if (sm.type == MessageType::BROADCAST) { + const char *cname = channels.getName(sm.channelIndex); + if (cname && cname[0]) + snprintf(contextBuf, sizeof(contextBuf), "in #%s", cname); + else + snprintf(contextBuf, sizeof(contextBuf), "in Ch%d", sm.channelIndex); + } + + if (contextBuf[0]) { + size_t cur = strlen(banner); + if (cur + 1 < sizeof(banner)) { + if (cur > 0 && banner[cur - 1] != '\n') { + banner[cur] = '\n'; + banner[cur + 1] = '\0'; + cur++; + } + strncat(banner, contextBuf, sizeof(banner) - cur - 1); + } + } + } + + // Shorter banner if already in a conversation (Channel or Direct) + bool inThread = (getThreadMode() != ThreadMode::ALL); + + if (shouldWakeOnReceivedMessage()) { + screen->setOn(true); + } + + screen->showSimpleBanner(banner, inThread ? 1000 : 3000); + } + + // Always focus into the correct conversation thread when a message with real text arrives + const char *msgText = MessageStore::getText(sm); + if (msgText && msgText[0] != '\0') { + setThreadFor(sm, packet); + } + + // Reset scroll for a clean start + resetScrollState(); +} + +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) +{ + if (packet.to == 0 || packet.to == NODENUM_BROADCAST) { + setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); + } else { + uint32_t localNode = nodeDB->getNodeNum(); + uint32_t peer = (sm.sender == localNode) ? packet.to : sm.sender; + setThreadMode(ThreadMode::DIRECT, -1, peer); } } diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index c15a699f7..7dec6adec 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -1,7 +1,11 @@ #pragma once +#include "MessageStore.h" // for StoredMessage +#if HAS_SCREEN #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/emotes.h" +#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket +#include #include #include @@ -10,6 +14,27 @@ namespace graphics namespace MessageRenderer { +// Thread filter modes +enum class ThreadMode { ALL, CHANNEL, DIRECT }; + +// Setter for switching thread mode +void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0); + +// Getter for current mode +ThreadMode getThreadMode(); + +// Getter for current channel (valid if mode == CHANNEL) +int getThreadChannel(); + +// Getter for current peer (valid if mode == DIRECT) +uint32_t getThreadPeer(); + +// Registry accessors for menuHandler +const std::vector &getSeenChannels(); +const std::vector &getSeenPeers(); + +void clearThreadRegistries(); + // Text and emote rendering void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); @@ -20,11 +45,27 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); // Function to calculate heights for each line -std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes); +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes, + const std::vector &isHeaderVec); -// Function to render the message content -void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, - int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold); +// Reset scroll state when new messages arrive +void resetScrollState(); + +// Manual scroll control for encoder-style inputs +void nudgeScroll(int8_t direction); + +// Helper to auto-select the correct thread mode from a message +void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet); + +// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset +void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet); + +// Clear Message Line Cache from Message Renderer +void clearMessageCache(); + +void scrollUp(); +void scrollDown(); } // namespace MessageRenderer } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 1a36a6188..e10d8c40a 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -23,7 +23,6 @@ extern graphics::Screen *screen; #if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; -#else #endif namespace graphics { @@ -46,79 +45,119 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t * } // Static variables for dynamic cycling -static NodeListMode currentMode = MODE_LAST_HEARD; +static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; +static ListMode_Location currentMode_Location = MODE_DISTANCE; static int scrollIndex = 0; +// Popup overlay state +static uint32_t popupTime = 0; +static int popupTotal = 0; +static int popupStart = 0; +static int popupEnd = 0; +static int popupPage = 1; +static int popupMaxPage = 1; + +static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible + +// ============================= +// Scrolling Logic +// ============================= +void scrollUp() +{ + if (scrollIndex > 0) + scrollIndex--; + + popupTime = millis(); // show popup +} + +void scrollDown() +{ + scrollIndex++; + popupTime = millis(); +} // ============================= // Utility Functions // ============================= -const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node) +const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) { - const char *name = NULL; - static char nodeName[16] = "?"; - if (config.display.use_long_node_name == true) { - if (node->has_user && strlen(node->user.long_name) > 0) { - name = node->user.long_name; - } else { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); - } - } else { - if (node->has_user && strlen(node->user.short_name) > 0) { - name = node->user.short_name; - } else { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); - } + static char nodeName[25]; // single static buffer we return + nodeName[0] = '\0'; + + auto writeFallbackId = [&] { + std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast(node ? (node->num & 0xFFFF) : 0)); + }; + + // 1) Choose target candidate (long vs short) only if present + const char *raw = nullptr; + if (node && node->has_user) { + raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; } - // Use sanitizeString() function and copy directly into nodeName - std::string sanitized_name = sanitizeString(name ? name : ""); + // 2) Sanitize (empty if raw is null/empty) + std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{}; - if (!sanitized_name.empty()) { - strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1); - nodeName[sizeof(nodeName) - 1] = '\0'; + // 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 { - snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); + // %.*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()); } - if (config.display.use_long_node_name == true) { - int availWidth = (SCREEN_WIDTH / 2) - 65; + // 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; - size_t origLen = strlen(nodeName); - while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { - nodeName[strlen(nodeName) - 1] = '\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 we actually truncated, append "..." (ensure space remains in buffer) - if (strlen(nodeName) < origLen) { - size_t len = strlen(nodeName); - size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0' - if (len > maxLen) { - nodeName[maxLen] = '\0'; - len = maxLen; + // 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'; } - strcat(nodeName, "..."); + std::strcat(nodeName, "..."); } } return nodeName; } -const char *getCurrentModeTitle(int screenWidth) +const char *getCurrentModeTitle_Nodes(int screenWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { case MODE_LAST_HEARD: return "Last Heard"; case MODE_HOP_SIGNAL: #ifdef USE_EINK return "Hops/Sig"; #else - return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; + return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig"; #endif + default: + return "Nodes"; + } +} + +const char *getCurrentModeTitle_Location(int screenWidth) +{ + switch (currentMode_Location) { case MODE_DISTANCE: return "Distance"; + case MODE_BEARING: + return "Bearings"; default: return "Nodes"; } @@ -137,10 +176,8 @@ int calculateMaxScroll(int totalEntries, int visibleRows) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { - int columnWidth = display->getWidth() / 2; - int separatorX = x + columnWidth - 2; for (int y = yStart; y <= yEnd; y += 2) { - display->setPixel(separatorX, y); + display->setPixel(x, y); } } @@ -152,7 +189,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollbarX = display->getWidth() - 2; int scrollbarHeight = display->getHeight() - scrollStartY - 10; int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); - int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); + int perPage = visibleNodeRows * columns; + int maxScroll = std::max(0, (totalEntries - 1) / perPage); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); for (int i = 0; i < thumbHeight; i++) { @@ -167,9 +205,9 @@ 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 timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -188,9 +226,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName); + display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -209,19 +247,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + 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); + const char *nodeName = getSafeNodeName(display, node, columnWidth); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -256,9 +294,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -311,9 +350,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -321,26 +360,24 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } if (strlen(distStr) > 0) { - int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int offset = (currentResolution == ScreenResolution::High) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) int rightEdge = x + columnWidth - offset; int textWidth = display->getStringWidth(distStr); display->drawString(rightEdge - textWidth, y, distStr); } } -void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - switch (currentMode) { + switch (currentMode_Nodes) { case MODE_LAST_HEARD: drawEntryLastHeard(display, node, x, y, columnWidth); break; case MODE_HOP_SIGNAL: drawEntryHopSignal(display, node, x, y, columnWidth); break; - case MODE_DISTANCE: - drawNodeDistance(display, node, x, y, columnWidth); - break; default: break; } @@ -351,15 +388,16 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = + columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - const char *nodeName = getSafeNodeName(display, node); + const char *nodeName = getSafeNodeName(display, node, columnWidth); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -374,7 +412,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 return; bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; @@ -431,11 +469,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t locationScreen = true; else if (strcmp(title, "Distance") == 0) locationScreen = true; -#if defined(M5STACK_UNITC6L) - int columnWidth = display->getWidth(); -#else - int columnWidth = display->getWidth() / 2; -#endif display->clear(); // Draw the battery/time header @@ -444,39 +477,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Space below header y += COMMON_HEADER_HEIGHT; + int totalColumns = 1; // Default to 1 column + + if (config.display.use_long_node_name) { + if (SCREEN_WIDTH <= 240) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 240) { + totalColumns = 2; + } + } else { + if (SCREEN_WIDTH <= 64) { + totalColumns = 1; + } else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) { + totalColumns = 2; + } else { + totalColumns = 3; + } + } + + int columnWidth = display->getWidth() / totalColumns; + int totalEntries = nodeDB->getNumMeshNodes(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int numskipped = 0; int visibleNodeRows = totalRowsAvailable; -#if defined(M5STACK_UNITC6L) - int totalColumns = 1; -#else - int totalColumns = 2; -#endif - int startIndex = scrollIndex * visibleNodeRows * totalColumns; - if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { - startIndex++; // skip own node - } - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + // Build filtered + ordered list + std::vector drawList; + drawList.reserve(totalEntries); + for (int i = 0; i < totalEntries; i++) { + auto *n = nodeDB->getMeshNodeByIndex(i); + + if (!n) + continue; + if (n->num == nodeDB->getNodeNum()) + continue; + if (locationScreen && !n->has_position) + continue; + + drawList.push_back(n->num); + } + totalEntries = drawList.size(); + int perPage = visibleNodeRows * totalColumns; + + int maxScroll = 0; + if (perPage > 0) { + maxScroll = std::max(0, (totalEntries - 1) / perPage); + } + + if (scrollIndex > maxScroll) + scrollIndex = maxScroll; + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; int rowCount = 0; - for (int i = startIndex; i < endIndex; ++i) { - if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) { - numskipped++; - continue; - } + for (int idx = startIndex; idx < endIndex; idx++) { + uint32_t nodeNum = drawList[idx]; + auto *node = nodeDB->getMeshNode(nodeNum); int xPos = x + (col * columnWidth); int yPos = y + yOffset; - renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth); - if (extras) { - extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon); - } + renderer(display, node, xPos, yPos, columnWidth); + + if (extras) + extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; @@ -495,17 +563,73 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // This should correct the scrollbar totalEntries -= numskipped; -#if !defined(M5STACK_UNITC6L) // Draw column separator - if (shownCount > 0) { + if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) { const int firstNodeY = y + 3; - drawColumnSeparator(display, x, firstNodeY, lastNodeY); + for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { + drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY); + } } -#endif const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); graphics::drawCommonFooter(display, x, y); + + // Scroll Popup Overlay + if (millis() - popupTime < POPUP_DURATION_MS) { + popupTotal = totalEntries; + + int perPage = visibleNodeRows * totalColumns; + + popupStart = startIndex + 1; + popupEnd = std::min(startIndex + perPage, totalEntries); + + popupPage = (scrollIndex + 1); + popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Box padding + int padding = 2; + int textW = display->getStringWidth(buf); + int textH = FONT_HEIGHT_SMALL; + int boxWidth = textW + padding * 3; + int boxHeight = textH + padding * 2; + + // Center of usable screen area: + int headerHeight = FONT_HEIGHT_SMALL - 1; + int footerHeight = FONT_HEIGHT_SMALL + 2; + + int usableTop = headerHeight; + int usableBottom = display->getHeight() - footerHeight; + int usableHeight = usableBottom - usableTop; + + // Center point inside usable area + int boxLeft = (display->getWidth() - boxWidth) / 2; + int boxTop = usableTop + (usableHeight - boxHeight) / 2; + + // Draw Box + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); + display->setColor(WHITE); + + // Text + display->drawString(boxLeft + padding, boxTop + padding, buf); + } } // ============================= @@ -513,10 +637,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // ============================= #ifndef USE_EINK -void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// Node list for Last Heard and Hop Signal views +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration - static NodeListMode lastRenderedMode = MODE_COUNT; + static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static unsigned long modeStartTime = 0; unsigned long now = millis(); @@ -529,23 +654,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } #endif // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT) { - currentMode = MODE_LAST_HEARD; + if (lastRenderedMode == MODE_COUNT_NODE) { + currentMode_Nodes = MODE_LAST_HEARD; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode = static_cast((currentMode + 1) % MODE_COUNT); + currentMode_Nodes = static_cast((currentMode_Nodes + 1) % MODE_COUNT_NODE); modeStartTime = now; } // Render screen based on currentMode - const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + const char *title = getCurrentModeTitle_Nodes(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode; + lastRenderedMode = currentMode_Nodes; +} + +// Node list for Distance and Bearings views +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + +#if defined(M5STACK_UNITC6L) + display->clear(); + if (now - lastSwitchTime >= 3000) { + display->display(); + lastSwitchTime = now; + } +#endif + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT_LOCATION) { + currentMode_Location = MODE_DISTANCE; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode_Location = static_cast((currentMode_Location + 1) % MODE_COUNT_LOCATION); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle_Location(display->getWidth()); + + // Render screen based on currentMode_Location + if (currentMode_Location == MODE_DISTANCE) { + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); + } else if (currentMode_Location == MODE_BEARING) { + drawNodeListWithCompasses(display, state, x, y); + } + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode_Location; } #endif @@ -566,14 +733,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ #endif drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } - void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Distance"; drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } #endif - void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { float heading = 0; diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index ea8df8bd9..e212c031b 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -23,8 +23,11 @@ namespace NodeListRenderer typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); -// Node list mode enumeration -enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; +// Node list mode enumeration for Last Heard and Hop Signal views +enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; + +// Node list mode enumeration for Distance and Bearings views +enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 }; // Main node list screen function void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, @@ -35,7 +38,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); -void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); // Extras renderers @@ -46,14 +49,20 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Utility functions -const char *getCurrentModeTitle(int screenWidth); -const char *getSafeNodeName(meshtastic_NodeInfoLite *node); +const char *getCurrentModeTitle_Nodes(int screenWidth); +const char *getCurrentModeTitle_Location(int screenWidth); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); +// Scrolling controls +void scrollUp(); +void scrollDown(); + // Bitmap drawing function void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 26bfe8447..8d76b4592 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if HAS_SCREEN +#if HAS_SCREEN #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" @@ -38,7 +38,7 @@ extern bool hasUnreadMessage; namespace graphics { - +int bannerSignalBars = -1; InputEvent NotificationRenderer::inEvent; int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; @@ -85,9 +85,13 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat void NotificationRenderer::resetBanner() { + notificationTypeEnum previousType = current_notification_type; + alertBannerMessage[0] = '\0'; current_notification_type = notificationTypeEnum::none; + OnScreenKeyboardModule::instance().clearPopup(); + inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.kbchar = 0; curSelected = 0; @@ -100,6 +104,13 @@ void NotificationRenderer::resetBanner() currentNumber = 0; nodeDB->pause_sort(false); + + // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update + // to ensure any messages received during keyboard use are now displayed + if (previousType == notificationTypeEnum::text_input && screen) { + OnScreenKeyboardModule::instance().stop(false); + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } } void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) @@ -163,13 +174,15 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS // modulo to extract uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); // Handle input - if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || + inEvent.inputEvent == INPUT_BROKER_UP_LONG) { if (this_digit == 9) { currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); } else { currentNumber += (pow_of_10(numDigits - curSelected - 1)); } - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || + inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { if (this_digit == 0) { currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); } else { @@ -251,10 +264,10 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta // Handle input if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || - inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { curSelected--; } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || - inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { alertBannerCallback(selectedNodenum); @@ -308,7 +321,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } if (i == curSelected) { selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36); strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3); @@ -368,10 +381,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // Handle input if (alertBannerOptions > 0) { if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || - inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { curSelected--; } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || - inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { curSelected++; } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { if (optionsEnumPtr != nullptr) { @@ -436,7 +449,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { if (i == curSelected) { - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { strncpy(lineBuffer, "> ", 3); strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); @@ -464,7 +477,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay bool is_picker = false; uint16_t lineCount = 0; - // === Layout Configuration === + // Layout Configuration constexpr uint16_t hPadding = 5; constexpr uint16_t vPadding = 2; bool needs_bell = false; @@ -478,13 +491,32 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); + // Track widest line INCLUDING bars (but don't change per-line widths) + uint16_t widestLineWithBars = 0; + while (lines[lineCount] != nullptr) { auto newlinePointer = strchr(lines[lineCount], '\n'); if (newlinePointer) lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first 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); + + // Consider extra width for signal bars on lines that contain "Signal:" + uint16_t potentialWidth = lineWidths[lineCount]; + if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int gap = 6; // space between text and bars + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + potentialWidth += barsWidth; + } + + if (potentialWidth > widestLineWithBars) + widestLineWithBars = potentialWidth; + if (!is_picker) { needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); if (lineWidths[lineCount] > maxWidth) @@ -494,12 +526,16 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay } // count lines + // Ensure box accounts for signal bars if present + if (widestLineWithBars > maxWidth) + maxWidth = widestLineWithBars; + uint16_t boxWidth = hPadding * 2 + maxWidth; -#if defined(M5STACK_UNITC6L) + if (needs_bell) { - if (isHighResolution && boxWidth <= 150) + if ((currentResolution == ScreenResolution::High) && boxWidth <= 150) boxWidth += 26; - if (!isHighResolution && boxWidth <= 100) + if ((currentResolution == ScreenResolution::Low || currentResolution == ScreenResolution::UltraLow) && boxWidth <= 100) boxWidth += 20; } @@ -508,14 +544,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) - boxHeight += (isHighResolution ? 4 : 3); + if (visibleTotalLines == 1) { + boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3; + } int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) - boxWidth += (isHighResolution ? 4 : 2); + if (totalLines > visibleTotalLines) { + boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2; + } int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - + boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; +#if defined(M5STACK_UNITC6L) if (visibleTotalLines == 1) { boxTop += 25; } @@ -526,127 +565,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay if (boxTop < 0) boxTop = 0; } +#endif - // === Draw Box === - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); - display->setColor(WHITE); - int16_t lineY = boxTop + vPadding; - int swingRange = 8; - static int swingOffset = 0; - static bool swingRight = true; - static unsigned long lastSwingTime = 0; - unsigned long now = millis(); - int swingSpeedMs = 10 / (swingRange * 2); - if (now - lastSwingTime >= (unsigned long)swingSpeedMs) { - lastSwingTime = now; - if (swingRight) { - swingOffset++; - if (swingOffset >= swingRange) - swingRight = false; - } else { - swingOffset--; - if (swingOffset <= 0) - swingRight = true; - } - } - for (int i = 0; i < lineCount; i++) { - bool isTitle = (i == 0); - int globalOptionIndex = (i - 1) + firstOptionToShow; - bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected); - - uint16_t visibleWidth = 64 - hPadding * 2; - if (totalLines > visibleTotalLines) - visibleWidth -= 6; - char lineBuffer[lineLengths[i] + 1]; - strncpy(lineBuffer, lines[i], lineLengths[i]); - lineBuffer[lineLengths[i]] = '\0'; - - if (isTitle) { - if (visibleTotalLines == 1) { - display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); - display->setColor(WHITE); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); - } else { - display->setColor(WHITE); - display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight); - display->setColor(BLACK); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer); - display->setColor(WHITE); - if (needs_bell) { - int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2; - display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert); - display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert); - } - } - lineY = boxTop + effectiveLineHeight + 1; - } else if (isSelectedOption) { - display->setColor(WHITE); - display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); - display->setColor(BLACK); - if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) { - int textX = boxLeft + hPadding + swingOffset; - display->drawString(textX, lineY - 1, lineBuffer); - } else { - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer); - } - display->setColor(WHITE); - lineY += effectiveLineHeight; - } else { - display->setColor(BLACK); - display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight); - display->setColor(WHITE); - display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer); - lineY += effectiveLineHeight; - } - } - if (totalLines > visibleTotalLines) { - const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; - uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; - float ratio = (float)visibleTotalLines / totalLines; - uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); - float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines); - uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); - display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); - display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); - } -#else - if (needs_bell) { - if (isHighResolution && boxWidth <= 150) - boxWidth += 26; - if (!isHighResolution && boxWidth <= 100) - boxWidth += 20; - } - - uint16_t screenHeight = display->height(); - uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; - uint8_t visibleTotalLines = std::min(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight); - uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; - uint16_t boxHeight = contentHeight + vPadding * 2; - if (visibleTotalLines == 1) { - boxHeight += (isHighResolution) ? 4 : 3; - } - - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - if (totalLines > visibleTotalLines) { - boxWidth += (isHighResolution) ? 4 : 2; - } - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - - // === Draw Box === + // Draw Box display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); @@ -662,7 +583,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); - // === Draw Content === + // Draw Content int16_t lineY = boxTop + vPadding; for (int i = 0; i < lineCount; i++) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; @@ -691,17 +612,47 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay lineY += (effectiveLineHeight - 2 - background_yOffset); } else { // Pop-up - display->drawString(textX, lineY, lineBuffer); + // If this is the Signal line, center text + bars as one group + bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr); + if (isSignalLine) { + const int totalBars = 5; + const int barWidth = 3; + const int barSpacing = 2; + const int barHeightStep = 2; + const int gap = 6; + + int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true); + int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap; + int totalWidth = textWidth + barsWidth; + int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; + + display->drawString(groupStartX, lineY, lineBuffer); + + int baseX = groupStartX + textWidth + gap; + int baseY = lineY + effectiveLineHeight - 1; + for (int b = 0; b < totalBars; b++) { + int barHeight = (b + 1) * barHeightStep; + int x = baseX + b * (barWidth + barSpacing); + int y = baseY - barHeight; + + if (b < graphics::bannerSignalBars) { + display->fillRect(x, y, barWidth, barHeight); + } else { + display->drawRect(x, y, barWidth, barHeight); + } + } + } else { + display->drawString(textX, lineY, lineBuffer); + } lineY += (effectiveLineHeight); } } - // === Scroll Bar (Thicker, inside box, not over title) === + // Scroll Bar (Thicker, inside box, not over title) if (totalLines > visibleTotalLines) { const uint8_t scrollBarWidth = 5; - int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; - int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; float ratio = (float)visibleTotalLines / totalLines; @@ -712,7 +663,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } -#endif } /// Draw the last text message we received @@ -769,40 +719,8 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat } if (inEvent.inputEvent != INPUT_BROKER_NONE) { - if (inEvent.inputEvent == INPUT_BROKER_UP) { - // high frequency for move cursor left/right than up/down with encoders - extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; - extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; - if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { - virtualKeyboard->moveCursorLeft(); - } else { - virtualKeyboard->moveCursorUp(); - } - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; - extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; - if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { - virtualKeyboard->moveCursorRight(); - } else { - virtualKeyboard->moveCursorDown(); - } - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - virtualKeyboard->moveCursorUp(); - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - virtualKeyboard->moveCursorDown(); - } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - virtualKeyboard->handlePress(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { - virtualKeyboard->handleLongPress(); - } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { + bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); + if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { auto callback = textInputCallback; delete virtualKeyboard; virtualKeyboard = nullptr; @@ -821,12 +739,28 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat inEvent.inputEvent = INPUT_BROKER_NONE; } + // Re-check pointer before drawing to avoid use-after-free and crashes + if (!virtualKeyboard) { + // Ensure we exit text_input state and restore frames + if (current_notification_type == notificationTypeEnum::text_input) { + resetBanner(); + } + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + // If screen is null, do nothing (safe fallback) + return; + } + // Clear the screen to avoid overlapping with underlying frames or overlays display->setColor(BLACK); display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->setColor(WHITE); // Draw the virtual keyboard virtualKeyboard->draw(display, 0, 0); + + // Draw transient popup overlay (if any) managed by OnScreenKeyboardModule + OnScreenKeyboardModule::instance().drawPopupOverlay(display); } else { // If virtualKeyboard is null, reset the banner to avoid getting stuck LOG_INFO("Virtual keyboard is null - resetting banner"); @@ -839,5 +773,12 @@ bool NotificationRenderer::isOverlayBannerShowing() return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); } +void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) +{ + if (!title || !content || current_notification_type != notificationTypeEnum::text_input) + return; + OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); +} + } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index edb069513..e51bfa5ab 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -4,6 +4,7 @@ #include "OLEDDisplayUi.h" #include "graphics/Screen.h" #include "graphics/VirtualKeyboard.h" +#include "modules/OnScreenKeyboardModule.h" #include #include #define MAX_LINES 5 @@ -31,6 +32,7 @@ class NotificationRenderer static bool pauseBanner; static void resetBanner(); + static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index c50fe5cf1..7ce9d5afe 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -6,10 +6,7 @@ #include "NodeListRenderer.h" #include "UIRenderer.h" #include "airtime.h" -#include "configuration.h" #include "gps/GeoCoord.h" -#include "graphics/Screen.h" -#include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TimeFormatters.h" #include "graphics/images.h" @@ -29,6 +26,16 @@ namespace graphics NodeNum UIRenderer::currentFavoriteNodeNum = 0; std::vector graphics::UIRenderer::favoritedNodes; +static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + int yOffset = (currentResolution == ScreenResolution::High) ? -5 : 1; + if (currentResolution == ScreenResolution::High) { + NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); + } +} + void graphics::UIRenderer::rebuildFavoritedNodes() { favoritedNodes.clear(); @@ -56,7 +63,7 @@ extern uint32_t dopThresholds[5]; void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); @@ -76,7 +83,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht } else { snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawString(x + 18, y, textString); } else { display->drawString(x + 11, y, textString); @@ -244,35 +251,36 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, // Draw nodes status void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, - bool show_total, String additional_words) + bool show_total, const char *additional_words) { char usersString[20]; int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str()); + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); if (show_total) { int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str()); + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 3, 8, 8, imgUser); } #else - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 1, 8, 8, imgUser); } #endif - int string_offset = (isHighResolution) ? 9 : 0; + int string_offset = (currentResolution == ScreenResolution::High) ? 9 : 0; display->drawString(x + 10 + string_offset, y - 2, usersString); } @@ -320,11 +328,12 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st int line = 1; // which slot to use next std::string usernameStr; // === 1. Long Name (always try to show first) === -#if defined(M5STACK_UNITC6L) - const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; -#else - const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; -#endif + const char *username; + if (currentResolution == ScreenResolution::UltraLow) { + username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr; + } else { + username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + } if (username) { usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case @@ -500,7 +509,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) - const int iconSize = (isHighResolution) ? 16 : 8; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; @@ -558,11 +567,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); // === Header === -#if defined(M5STACK_UNITC6L) - graphics::drawCommonHeader(display, x, y, "Home"); -#else - graphics::drawCommonHeader(display, x, y, ""); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + graphics::drawCommonHeader(display, x, y, "Home"); + } else { + graphics::drawCommonHeader(display, x, y, ""); + } // === Content below header === @@ -577,15 +586,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta config.display.heading_bold = false; // Display Region and Channel Utilization -#if defined(M5STACK_UNITC6L) - drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); -#else - drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); -#endif + if (currentResolution == ScreenResolution::UltraLow) { + drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } else { + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + } char uptimeStr[32] = ""; -#if !defined(M5STACK_UNITC6L) - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); -#endif + if (currentResolution != ScreenResolution::UltraLow) { + getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + } display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); // === Second Row: Satellites and Voltage === @@ -599,15 +608,8 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (isHighResolution) ? 3 : 1; - if (isHighResolution) { - NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, - imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, - imgSatellite); - } - int xOffset = (isHighResolution) ? 6 : 0; + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); } else { UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); @@ -646,21 +648,22 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (currentResolution == ScreenResolution::High) ? display->getStringWidth(chUtil) + 10 + : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (isHighResolution) ? 100 : 50; + int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50; if (!config.bluetooth.enabled) { #if defined(USE_EINK) - chutil_bar_width = (isHighResolution) ? 50 : 30; + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 50 : 30; #else - chutil_bar_width = (isHighResolution) ? 80 : 40; + chutil_bar_width = (currentResolution == ScreenResolution::High) ? 80 : 40; #endif } - int chutil_bar_height = (isHighResolution) ? 12 : 7; - int extraoffset = (isHighResolution) ? 6 : 3; + int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7; + int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3; if (!config.bluetooth.enabled) { - extraoffset = (isHighResolution) ? 6 : 1; + extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 1; } int chutil_percent = airTime->channelUtilizationPercent(); @@ -720,7 +723,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Fourth & Fifth Rows: Node Identity === int textWidth = 0; int nameX = 0; - int yOffset = (isHighResolution) ? 0 : 5; + int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; std::string longNameStr; if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { @@ -758,7 +761,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // Start Functions to write date/time to the screen // Helper function to check if a year is a leap year -bool isLeapYear(int year) +constexpr bool isLeapYear(int year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } @@ -989,15 +992,8 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (isHighResolution) ? 3 : 1; - if (isHighResolution) { - NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, - imgSatellite_height, imgSatellite, display); - } else { - display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, - imgSatellite); - } - int xOffset = (isHighResolution) ? 6 : 0; + drawSatelliteIcon(display, x, getTextPositions(display)[line]); + int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { // Onboard GPS @@ -1155,7 +1151,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, USERPREFS_OEM_IMAGE_HEIGHT, xbm); @@ -1180,7 +1176,7 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = USERPREFS_OEM_TEXT; - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); } display->setFont(FONT_SMALL); @@ -1224,15 +1220,15 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta lastFrameChangeTime = millis(); } - const int iconSize = isHighResolution ? 16 : 8; - const int spacing = isHighResolution ? 8 : 4; - const int bigOffset = isHighResolution ? 1 : 0; + const int iconSize = (currentResolution == ScreenResolution::High) ? 16 : 8; + const int spacing = (currentResolution == ScreenResolution::High) ? 8 : 4; + const int bigOffset = (currentResolution == ScreenResolution::High) ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); if (totalIcons == 0) return; - const int navPadding = isHighResolution ? 24 : 12; // padding per side + const int navPadding = (currentResolution == ScreenResolution::High) ? 24 : 12; // padding per side int usableWidth = SCREEN_WIDTH - (navPadding * 2); if (usableWidth < iconSize) @@ -1299,7 +1295,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(BLACK); } - if (isHighResolution) { + if (currentResolution == ScreenResolution::High) { NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); } else { display->drawXbm(x, y, iconSize, iconSize, icon); @@ -1314,7 +1310,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta auto drawArrow = [&](bool rightSide) { display->setColor(WHITE); - const int offset = isHighResolution ? 3 : 1; + const int offset = (currentResolution == ScreenResolution::High) ? 3 : 1; const int halfH = rectHeight / 2; const int top = (y - 2) + (rectHeight - halfH) / 2; diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 438d56cc2..6e37b68f2 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -34,7 +34,7 @@ class UIRenderer public: // Common UI elements static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, - int node_offset = 0, bool show_total = true, String additional_words = ""); + int node_offset = 0, bool show_total = true, const char *additional_words = ""); // GPS status functions static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); @@ -43,9 +43,6 @@ class UIRenderer static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); - // Layout and utility functions - static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY); - // Overlay and special screens static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); @@ -83,8 +80,6 @@ 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); - // Message filtering - static bool shouldDrawMessage(const meshtastic_MeshPacket *packet); // 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/emotes.cpp b/src/graphics/emotes.cpp index bed2b7b7c..aa54ef2f1 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -13,41 +13,81 @@ const Emote emotes[] = { {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // 😊 Smiling Eyes - {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // 😀 Grinning Face - {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // 🙂 Slightly Smiling Face - {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // 😉 Winking Face - {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // ðŸ˜� Grinning Smiling Eyes - {"\U0001F60D", Heart_eyes, Heart_eyes_width, Heart_eyes_height}, // ðŸ˜� Heart Eyes + {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes + {"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face + {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face + {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // ðŸ˜� Grinning Smiling Eyes + {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // ðŸ˜� Heart Eyes {"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts // --- Question/Alert --- - {"\u2753", question, question_width, question_height}, // â�“ Question Mark - {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼ï¸� Double Exclamation Mark + {"\u2753", question, question_width, question_height}, // â�“ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼ï¸� Double Exclamation Mark + {"\u26A0\uFE0F", caution, caution_width, caution_height}, // âš ï¸� Warning Sign // --- Laughing Faces --- {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy - {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // 🤣 Rolling on the Floor Laughing - {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // 😆 Smiling Closed Eyes + {"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat - {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width, - Grinning_SmilingEyes2_height}, // 😄 Grinning Face with Smiling Eyes - {"\U0001F62D", Loudly_Crying_Face, Loudly_Crying_Face_width, Loudly_Crying_Face_height}, // 😭 Loudly Crying Face + {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, + grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes + {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face + {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting + {"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses + {"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes + {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // ðŸ‘�ï¸� Eye // --- Gestures and People --- {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand {"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // ✌ï¸� Victory Hand {"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // 🖖 Vulcan Salute - {"\U0001F64F", Praying, Praying_width, Praying_height}, // ðŸ™� Praying Hands + {"\U0001F64F", praying, praying_width, praying_height}, // ðŸ™� Praying Hands + {"\U0001F4AA", strong, strong_width, strong_height}, // 💪 Flexed Biceps + {"\U0001F937", shrug, shrug_width, shrug_height}, // 🤷 Person Shrugging {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones + // --- Symbols --- + {"\u2714\uFE0F", check_mark, check_mark_width, check_mark_height}, // ✔ï¸� Check Mark + {"\u2705", check_mark, check_mark_width, check_mark_height}, // ✅ Check Mark Button + {"\u2611\uFE0F", check_mark, check_mark_width, check_mark_height}, // ☑ï¸� Check Box with Check + {"\U0001F3E0", house, house_width, house_height}, // ðŸ�  House + // --- Weather --- - {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) - {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀ï¸� Sun (with variation selector) - {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧ï¸� Cloud with Rain - {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // â˜�ï¸� Cloud - {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫ï¸� Fog + {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀ï¸� Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧ï¸� Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // â˜�ï¸� Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫ï¸� Fog + {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // â�„ï¸� Snowflake + {"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet + {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡ï¸� Thermometer + {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, + sun_behind_raincloud_height}, // 🌦ï¸� Sun Behind Rain Cloud + {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // â›… Sun Behind Cloud + {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // â›…ï¸� Sun Behind Cloud + {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨ï¸� Cloud with Snow + {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, + cloud_with_lightning_height}, // 🌩ï¸� Cloud with Lightning + {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain + {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, + cloud_with_lightning_rain_height}, // ⛈ï¸� Cloud with Lightning and Rain + {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬ï¸� Wind Face + + // --- Moon Phases --- + {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon + {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon + {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon + {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon + {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon + {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon + {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon + {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon + {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, + first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face // --- Misc Faces --- {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns @@ -67,13 +107,49 @@ const Emote emotes[] = { {"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow // --- Objects --- - {"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo - {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell - {"\U0001F36A", cookie, cookie_width, cookie_height}, // ðŸ�ª Cookie - {"\U0001F525", Fire, Fire_width, Fire_height}, // 🔥 Fire - {"\u2728", Sparkles, Sparkles_width, Sparkles_height}, // ✨ Sparkles - {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳ï¸� Hole - {"\U0001F3B3", bowling, bowling_width, bowling_height} // 🎳 Bowling + {"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // 🔔 Bell + {"\U0001F4CB", clipboard, clipboard_width, clipboard_height}, // 📋 Clipboard + {"\U0001F36A", cookie, cookie_width, cookie_height}, // ðŸ�ª Cookie + {"\U0001F370", shortcake, shortcake_width, shortcake_height}, // ðŸ�° Shortcake + {"\U0001F351", peach, peach_width, peach_height}, // ðŸ�‘ Peach + {"\U0001F983", turkey, turkey_width, turkey_height}, // 🦃 Turkey + {"\U0001F357", turkey_leg, turkey_leg_width, turkey_leg_height}, // ðŸ�— Poultry Leg + {"\U0001F525", fire, fire_width, fire_height}, // 🔥 Fire + {"\u2728", sparkles, sparkles_width, sparkles_height}, // ✨ Sparkles + {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // 🕳ï¸� Hole + {"\U0001F3B3", bowling, bowling_width, bowling_height}, // 🎳 Bowling + + // --- Arrows --- + {"\u2193", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓ Downwards Arrow + {"\u2193\uFE0E", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓︎ Downwards Arrow (text) + {"\u2193\uFE0F", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // ↓ï¸� Downwards Arrow (emoji) + {"\u2199", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙ South West Arrow + {"\u2199\uFE0E", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙︎ South West Arrow (text) + {"\u2199\uFE0F", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // ↙ï¸� South West Arrow (emoji) + {"\u2190", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // â†� Leftwards Arrow + {"\u2190\uFE0E", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // â†�︎ Leftwards Arrow (text) + {"\u2190\uFE0F", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // â†�ï¸� Leftwards Arrow (emoji) + {"\u2196", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖ North West Arrow + {"\u2196\uFE0E", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖︎ North West Arrow (text) + {"\u2196\uFE0F", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // ↖ï¸� North West Arrow (emoji) + {"\u2191", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑ Upwards Arrow + {"\u2191\uFE0E", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑︎ Upwards Arrow (text) + {"\u2191\uFE0F", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // ↑ï¸� Upwards Arrow (emoji) + {"\u2197", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗ North East Arrow + {"\u2197\uFE0E", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗︎ North East Arrow (text) + {"\u2197\uFE0F", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // ↗ï¸� North East Arrow (emoji) + {"\u2192", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // → Rightwards Arrow + {"\u2192\uFE0E", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →︎ Rightwards Arrow (text) + {"\u2192\uFE0F", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // →ï¸� Rightwards Arrow (emoji) + {"\u2198", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘ South East Arrow + {"\u2198\uFE0E", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘︎ South East Arrow (text) + {"\u2198\uFE0F", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // ↘ï¸� South East Arrow (emoji) + + // --- Halloween --- + {"\U0001F383", jack_o_lantern, jack_o_lantern_width, jack_o_lantern_height}, // 🎃 Jack-O-Lantern + {"\U0001F47B", ghost, ghost_width, ghost_height}, // 👻 Ghost + {"\U0001F480", skull, skull_width, skull_height} // 💀 Skull #endif }; @@ -88,23 +164,23 @@ const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00}; -const unsigned char Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, +const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, +const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Slightly_Smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, +const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Winking_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, +const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, +const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; @@ -112,7 +188,7 @@ const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00}; -const unsigned char Heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, +const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; @@ -128,19 +204,19 @@ const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char ROFL[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, +const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Smiling_Closed_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, +const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Grinning_SmilingEyes2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, - 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, - 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; +const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, + 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Loudly_Crying_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, +const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00}; @@ -192,7 +268,7 @@ const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04 0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, +const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C}; @@ -200,11 +276,11 @@ const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; -const unsigned char Praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, +const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC}; -const unsigned char Sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, +const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00}; @@ -227,7 +303,179 @@ const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x0 const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, + 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, + 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F}; + +const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, + 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, + 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31}; + +const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, + 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, + 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D}; + +const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, + 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, + 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F}; + +const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, + 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44, + 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, + 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, + 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00}; + +const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, + 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, + 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00}; + +const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, + 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, + 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F}; + +const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, + 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, + 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, + 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, + 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00}; + +const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, + 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, + 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38}; + +const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, + 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, + 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, + 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, + 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, + 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01}; + +const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21, + 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01}; + +const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, + 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, + 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00}; + +const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, + 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, + 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, + 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, + 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, + 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, + 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, + 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, + 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, + 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, + 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, + 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, + 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, + 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, + 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, + 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00}; + +const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00, + 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80, + 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07}; + +const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, + 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40, + 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03}; + +const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, + 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, + 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07}; + +const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, + 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80, + 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C}; + +const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, + 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, + 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00}; + +const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, + 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, + 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00}; + +const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, + 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, + 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, + 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, + 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, + 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, + 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01}; + +const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, + 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, + 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, + 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, + 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, + 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00}; + +const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, + 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, + 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F}; + +const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, + 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, + 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00}; + +const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, + 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, + 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F}; + +const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, + 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, + 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F}; + +const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, + 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00}; + +const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, + 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, + 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; #endif } // namespace graphics -#endif \ No newline at end of file +#endif diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index b1b2d16da..0637712cc 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -22,33 +22,33 @@ extern const int numEmotes; extern const unsigned char thumbup[] PROGMEM; extern const unsigned char thumbdown[] PROGMEM; -#define Smiling_Eyes_height 16 -#define Smiling_Eyes_width 16 -extern const unsigned char Smiling_Eyes[] PROGMEM; +#define smiling_eyes_height 16 +#define smiling_eyes_width 16 +extern const unsigned char smiling_eyes[] PROGMEM; -#define Grinning_height 16 -#define Grinning_width 16 -extern const unsigned char Grinning[] PROGMEM; +#define grinning_height 16 +#define grinning_width 16 +extern const unsigned char grinning[] PROGMEM; -#define Slightly_Smiling_height 16 -#define Slightly_Smiling_width 16 -extern const unsigned char Slightly_Smiling[] PROGMEM; +#define slightly_smiling_height 16 +#define slightly_smiling_width 16 +extern const unsigned char slightly_smiling[] PROGMEM; -#define Winking_Face_height 16 -#define Winking_Face_width 16 -extern const unsigned char Winking_Face[] PROGMEM; +#define winking_face_height 16 +#define winking_face_width 16 +extern const unsigned char winking_face[] PROGMEM; -#define Grinning_Smiling_Eyes_height 16 -#define Grinning_Smiling_Eyes_width 16 -extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM; +#define grinning_smiling_eyes_height 16 +#define grinning_smiling_eyes_width 16 +extern const unsigned char grinning_smiling_eyes[] PROGMEM; #define heart_smile_height 16 #define heart_smile_width 16 extern const unsigned char heart_smile[] PROGMEM; -#define Heart_eyes_height 16 -#define Heart_eyes_width 16 -extern const unsigned char Heart_eyes[] PROGMEM; +#define heart_eyes_height 16 +#define heart_eyes_width 16 +extern const unsigned char heart_eyes[] PROGMEM; #define question_height 16 #define question_width 16 @@ -62,21 +62,21 @@ extern const unsigned char bang[] PROGMEM; #define haha_width 16 extern const unsigned char haha[] PROGMEM; -#define ROFL_height 16 -#define ROFL_width 16 -extern const unsigned char ROFL[] PROGMEM; +#define rofl_height 16 +#define rofl_width 16 +extern const unsigned char rofl[] PROGMEM; -#define Smiling_Closed_Eyes_height 16 -#define Smiling_Closed_Eyes_width 16 -extern const unsigned char Smiling_Closed_Eyes[] PROGMEM; +#define smiling_closed_eyes_height 16 +#define smiling_closed_eyes_width 16 +extern const unsigned char smiling_closed_eyes[] PROGMEM; -#define Grinning_SmilingEyes2_height 16 -#define Grinning_SmilingEyes2_width 16 -extern const unsigned char Grinning_SmilingEyes2[] PROGMEM; +#define grinning_smiling_eyes_2_height 16 +#define grinning_smiling_eyes_2_width 16 +extern const unsigned char grinning_smiling_eyes_2[] PROGMEM; -#define Loudly_Crying_Face_height 16 -#define Loudly_Crying_Face_width 16 -extern const unsigned char Loudly_Crying_Face[] PROGMEM; +#define loudly_crying_face_height 16 +#define loudly_crying_face_width 16 +extern const unsigned char loudly_crying_face[] PROGMEM; #define wave_icon_height 16 #define wave_icon_width 16 @@ -126,21 +126,21 @@ extern const unsigned char bell_icon[] PROGMEM; #define cookie_height 16 extern const unsigned char cookie[] PROGMEM; -#define Fire_width 16 -#define Fire_height 16 -extern const unsigned char Fire[] PROGMEM; +#define fire_width 16 +#define fire_height 16 +extern const unsigned char fire[] PROGMEM; #define peace_sign_width 16 #define peace_sign_height 16 extern const unsigned char peace_sign[] PROGMEM; -#define Praying_width 16 -#define Praying_height 16 -extern const unsigned char Praying[] PROGMEM; +#define praying_width 16 +#define praying_height 16 +extern const unsigned char praying[] PROGMEM; -#define Sparkles_width 16 -#define Sparkles_height 16 -extern const unsigned char Sparkles[] PROGMEM; +#define sparkles_width 16 +#define sparkles_height 16 +extern const unsigned char sparkles[] PROGMEM; #define clown_width 16 #define clown_height 16 @@ -161,6 +161,178 @@ extern const unsigned char bowling[] PROGMEM; #define vulcan_salute_width 16 #define vulcan_salute_height 16 extern const unsigned char vulcan_salute[] PROGMEM; + +#define jack_o_lantern_width 16 +#define jack_o_lantern_height 16 +extern const unsigned char jack_o_lantern[] PROGMEM; + +#define ghost_width 16 +#define ghost_height 16 +extern const unsigned char ghost[] PROGMEM; + +#define skull_width 16 +#define skull_height 16 +extern const unsigned char skull[] PROGMEM; + +#define vomiting_width 16 +#define vomiting_height 16 +extern const unsigned char vomiting[] PROGMEM; + +#define cool_width 16 +#define cool_height 16 +extern const unsigned char cool[] PROGMEM; + +#define shortcake_width 16 +#define shortcake_height 16 +extern const unsigned char shortcake[] PROGMEM; + +#define caution_width 16 +#define caution_height 16 +extern const unsigned char caution[] PROGMEM; + +#define clipboard_width 16 +#define clipboard_height 16 +extern const unsigned char clipboard[] PROGMEM; + +#define snowflake_width 16 +#define snowflake_height 16 +extern const unsigned char snowflake[] PROGMEM; + +#define drop_width 16 +#define drop_height 16 +extern const unsigned char drop[] PROGMEM; + +#define thermometer_width 16 +#define thermometer_height 16 +extern const unsigned char thermometer[] PROGMEM; + +#define sun_behind_raincloud_width 16 +#define sun_behind_raincloud_height 16 +extern const unsigned char sun_behind_raincloud[] PROGMEM; + +#define sun_behind_cloud_width 16 +#define sun_behind_cloud_height 16 +extern const unsigned char sun_behind_cloud[] PROGMEM; + +#define cloud_with_snow_width 16 +#define cloud_with_snow_height 16 +extern const unsigned char cloud_with_snow[] PROGMEM; + +#define cloud_with_lightning_width 16 +#define cloud_with_lightning_height 16 +extern const unsigned char cloud_with_lightning[] PROGMEM; + +#define cloud_with_lightning_rain_width 16 +#define cloud_with_lightning_rain_height 16 +extern const unsigned char cloud_with_lightning_rain[] PROGMEM; + +#define wind_face_width 16 +#define wind_face_height 16 +extern const unsigned char wind_face[] PROGMEM; + +#define new_moon_width 16 +#define new_moon_height 16 +extern const unsigned char new_moon[] PROGMEM; + +#define waxing_crescent_moon_width 16 +#define waxing_crescent_moon_height 16 +extern const unsigned char waxing_crescent_moon[] PROGMEM; + +#define first_quarter_moon_width 16 +#define first_quarter_moon_height 16 +extern const unsigned char first_quarter_moon[] PROGMEM; + +#define waxing_gibbous_moon_width 16 +#define waxing_gibbous_moon_height 16 +extern const unsigned char waxing_gibbous_moon[] PROGMEM; + +#define full_moon_width 16 +#define full_moon_height 16 +extern const unsigned char full_moon[] PROGMEM; + +#define waning_gibbous_moon_width 16 +#define waning_gibbous_moon_height 16 +extern const unsigned char waning_gibbous_moon[] PROGMEM; + +#define last_quarter_moon_width 16 +#define last_quarter_moon_height 16 +extern const unsigned char last_quarter_moon[] PROGMEM; + +#define waning_crescent_moon_width 16 +#define waning_crescent_moon_height 16 +extern const unsigned char waning_crescent_moon[] PROGMEM; + +#define first_quarter_moon_face_width 16 +#define first_quarter_moon_face_height 16 +extern const unsigned char first_quarter_moon_face[] PROGMEM; + +#define peach_width 16 +#define peach_height 16 +extern const unsigned char peach[] PROGMEM; + +#define turkey_width 16 +#define turkey_height 16 +extern const unsigned char turkey[] PROGMEM; + +#define turkey_leg_width 16 +#define turkey_leg_height 16 +extern const unsigned char turkey_leg[] PROGMEM; + +#define south_west_arrow_width 16 +#define south_west_arrow_height 16 +extern const unsigned char south_west_arrow[] PROGMEM; + +#define south_east_arrow_width 16 +#define south_east_arrow_height 16 +extern const unsigned char south_east_arrow[] PROGMEM; + +#define north_west_arrow_width 16 +#define north_west_arrow_height 16 +extern const unsigned char north_west_arrow[] PROGMEM; + +#define north_east_arrow_width 16 +#define north_east_arrow_height 16 +extern const unsigned char north_east_arrow[] PROGMEM; + +#define downwards_arrow_width 16 +#define downwards_arrow_height 16 +extern const unsigned char downwards_arrow[] PROGMEM; + +#define leftwards_arrow_width 16 +#define leftwards_arrow_height 16 +extern const unsigned char leftwards_arrow[] PROGMEM; + +#define upwards_arrow_width 16 +#define upwards_arrow_height 16 +extern const unsigned char upwards_arrow[] PROGMEM; + +#define rightwards_arrow_width 16 +#define rightwards_arrow_height 16 +extern const unsigned char rightwards_arrow[] PROGMEM; + +#define strong_width 16 +#define strong_height 16 +extern const unsigned char strong[] PROGMEM; + +#define check_mark_width 16 +#define check_mark_height 16 +extern const unsigned char check_mark[] PROGMEM; + +#define house_width 16 +#define house_height 16 +extern const unsigned char house[] PROGMEM; + +#define shrug_width 16 +#define shrug_height 16 +extern const unsigned char shrug[] PROGMEM; + +#define eyes_width 16 +#define eyes_height 16 +extern const unsigned char eyes[] PROGMEM; + +#define eye_width 16 +#define eye_height 16 +extern const unsigned char eye[] PROGMEM; #endif // EXCLUDE_EMOJI -} // namespace graphics \ No newline at end of file +} // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index 8670d78d9..ef9ffef78 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -28,7 +28,7 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 #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) || \ - ARCH_PORTDUINO) && \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; @@ -304,58 +304,6 @@ const uint8_t chirpy[] = { 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; -#define chirpy_width_hirez 76 -#define chirpy_height_hirez 100 -const uint8_t chirpy_hirez[] = { - 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, - 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, - 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, - 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, - 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, - 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, - 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, - 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, - 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, - 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, - 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, - 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, - 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, - 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, - 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, - 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, - 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, - 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, - 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, - 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, - 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, - 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, - 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, - 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, - 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, - 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, - 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, - 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3}; - #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 802186e6e..b35ca5cc0 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -88,8 +88,14 @@ class Applet : public GFX virtual void onForeground() {} virtual void onBackground() {} virtual void onShutdown() {} - virtual void onButtonShortPress() {} // (System Applets only) - virtual void onButtonLongPress() {} // (System Applets only) + virtual void onButtonShortPress() {} + virtual void onButtonLongPress() {} + virtual void onExitShort() {} + virtual void onExitLong() {} + virtual void onNavUp() {} + virtual void onNavDown() {} + virtual void onNavLeft() {} + virtual void onNavRight() {} virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index db7097f3f..6c7a7b491 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -124,7 +124,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) utf32 |= (utf8.at(3) & 0b00111111); break; default: - assert(false); + return 0; } return utf32; diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp new file mode 100644 index 000000000..67ef87f41 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -0,0 +1,205 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AlignStickApplet.h" + +using namespace NicheGraphics; + +InkHUD::AlignStickApplet::AlignStickApplet() +{ + if (!settings->joystick.aligned) + bringToForeground(); +} + +void InkHUD::AlignStickApplet::onRender() +{ + setFont(fontMedium); + printAt(0, 0, "Align Joystick:"); + setFont(fontSmall); + std::string instructions = "Move joystick in the direction indicated"; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); + + // Size of the region in which the joystick graphic should fit + uint16_t joyXLimit = X(0.8); + uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; + if (getTextWidth(instructions) > width()) + contentH += fontSmall.lineHeight(); + uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; + uint16_t joyYLimit = freeY * 0.8; + + // Use the shorter of the two + uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; + + // Center the joystick graphic + uint16_t centerX = X(0.5); + uint16_t centerY = contentH + freeY * 0.5; + + // Draw joystick graphic + drawStick(centerX, centerY, joyWidth); + + setFont(fontSmall); + printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); +} + +// Draw a scalable joystick graphic +void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) +{ + if (width < 9) // too small to draw + return; + + else if (width < 40) { // only draw up arrow + uint16_t chamfer = width < 20 ? 1 : 2; + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); + + } else { // large enough to draw the full thing + uint16_t chamfer = width < 80 ? 1 : 2; + uint16_t stroke = 3; // pixels + uint16_t arrowW = width * 0.22; + uint16_t hollowW = arrowW - stroke * 2; + + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); + + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); + + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); + + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); + } +} + +// Draw a scalable joystick direction arrow +// a right-triangle with blunted tips +/* + _ <--point + ^ / \ + | / \ + size / \ + | / \ + v |_________| + +*/ +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, + uint16_t chamfer, Color color) +{ + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = size - chamferW; + + // Draw arrow + switch (direction) { + case Direction::UP: + fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, + pointY + triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, + pointY + triangleW, color); + break; + case Direction::DOWN: + fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, + pointY - triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, + pointY - triangleW, color); + break; + case Direction::LEFT: + fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, + pointY + chamfer, color); + break; + case Direction::RIGHT: + fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, + pointY + chamfer, color); + break; + } +} + +void InkHUD::AlignStickApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; + + handleInput = true; // Intercept the button input for our applet +} + +void InkHUD::AlignStickApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onButtonLongPress() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onExitLong() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onNavUp() +{ + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onNavDown() +{ + inkhud->rotateJoystick(2); // 180 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onNavLeft() +{ + inkhud->rotateJoystick(3); // 270 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onNavRight() +{ + inkhud->rotateJoystick(1); // 90 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h new file mode 100644 index 000000000..8dba33165 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -0,0 +1,50 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet for manually aligning the joystick with the screen + +should be run at startup if the joystick is enabled +and not aligned to the screen + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/SystemApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class AlignStickApplet : public SystemApplet +{ + public: + AlignStickApplet(); + + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonLongPress() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; + + protected: + enum Direction { + UP, + DOWN, + LEFT, + RIGHT, + }; + + void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index c84ee09e0..debe2b719 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -30,6 +30,7 @@ enum MenuAction { TOGGLE_AUTOSHOW_APPLET, SET_RECENTS, ROTATE, + ALIGN_JOYSTICK, LAYOUT, TOGGLE_BATTERY_ICON, TOGGLE_NOTIFICATIONS, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 09f76ed46..7e7093857 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -178,6 +178,10 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->rotate(); break; + case ALIGN_JOYSTICK: + inkhud->openAlignStick(); + break; + case LAYOUT: // Todo: smarter incrementing of tile count settings->userTiles.count++; @@ -287,14 +291,17 @@ void InkHUD::MenuApplet::showPage(MenuPage page) // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::EXIT; break; case SEND: populateSendPage(); + previousPage = MenuPage::ROOT; break; case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); + previousPage = MenuPage::OPTIONS; break; case OPTIONS: @@ -321,6 +328,8 @@ 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) + items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, @@ -332,20 +341,24 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back( MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::ROOT; break; case APPLETS: populateAppletPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; break; case AUTOSHOW: populateAutoshowPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; break; case RECENTS: populateRecentsPage(); + previousPage = MenuPage::OPTIONS; break; case EXIT: @@ -479,12 +492,21 @@ void InkHUD::MenuApplet::onButtonShortPress() // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Move menu cursor to next entry, then update - if (cursorShown) - cursor = (cursor + 1) % items.size(); - else - cursorShown = true; - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + if (!settings->joystick.enabled) { + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } } void InkHUD::MenuApplet::onButtonLongPress() @@ -504,6 +526,62 @@ void InkHUD::MenuApplet::onButtonLongPress() requestUpdate(Drivers::EInk::UpdateTypes::FAST); } +void InkHUD::MenuApplet::onExitShort() +{ + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavUp() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to previous entry, then update + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + + if (!cursorShown) + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavDown() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavLeft() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onNavRight() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + // Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu void InkHUD::MenuApplet::populateAppletPage() { @@ -796,4 +874,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources() cm.recipientItems.clear(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 8f9280e6f..4f9f92227 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -27,6 +27,11 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; + void onExitShort() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; void onRender() override; void show(Tile *t); // Open the menu, onto a user tile @@ -52,6 +57,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage currentPage = MenuPage::ROOT; + MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) @@ -97,4 +103,4 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index ae0836d19..2ea9c7fe0 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -153,6 +153,42 @@ void InkHUD::NotificationApplet::onButtonLongPress() inkhud->forceUpdate(EInk::UpdateTypes::FULL); } +void InkHUD::NotificationApplet::onExitShort() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onExitLong() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onNavUp() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onNavDown() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onNavLeft() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onNavRight() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Called internally when we first get a "notifiable event", and then again before render, // in case autoshow swapped which applet was displayed diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 66df784b4..16ea13407 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -31,6 +31,12 @@ class NotificationApplet : public SystemApplet void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; + void onExitShort() override; + void onExitLong() override; + void onNavUp() override; + void onNavDown() override; + void onNavLeft() override; + void onNavRight() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index ade44ab65..a9d579873 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -112,12 +112,21 @@ void InkHUD::TipsApplet::onRender() setFont(fontSmall); int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); - cursorY += fontSmall.lineHeight() * 1.5; + if (!settings->joystick.enabled) { + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: next"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: select / open menu"); + } else { + printAt(0, cursorY, "Joystick"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- open menu / select"); + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "Exit Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- switch tile / close menu"); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; @@ -127,8 +136,13 @@ void InkHUD::TipsApplet::onRender() printAt(0, 0, "Tip: Rotation"); setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + if (!settings->joystick.enabled) { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + } else { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); @@ -232,4 +246,10 @@ void InkHUD::TipsApplet::onButtonShortPress() requestUpdate(); } +// Functions the same as the user button in this instance +void InkHUD::TipsApplet::onExitShort() +{ + onButtonShortPress(); +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index db88585e9..159e6f58f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -36,6 +36,7 @@ class TipsApplet : public SystemApplet void onForeground() override; void onBackground() override; void onButtonShortPress() override; + void onExitShort() override; protected: void renderWelcome(); // Very first screen of tutorial diff --git a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp index 88bed998d..ad0f9fc47 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.cpp @@ -1,6 +1,7 @@ #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./PositionsApplet.h" +#include "NodeDB.h" using namespace NicheGraphics; @@ -49,8 +50,8 @@ ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPack if (!hasPosition) return ProcessMessage::CONTINUE; - bool hasHopsAway = (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start); // From NodeDB::updateFrom - uint8_t hopsAway = mp.hop_start - mp.hop_limit; + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; // Determine if the position packet would change anything on-screen // ----------------------------------------------------------------- diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index cdda1638d..5382d2391 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -55,10 +55,15 @@ void InkHUD::Events::onButtonShort() } // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) + // or open menu if joystick is enabled + if (consumer) { consumer->onButtonShortPress(); - else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module - inkhud->nextApplet(); + } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } void InkHUD::Events::onButtonLong() @@ -83,6 +88,156 @@ void InkHUD::Events::onButtonLong() inkhud->openMenu(); } +void InkHUD::Events::onExitShort() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is change tiles + if (consumer) + consumer->onExitShort(); + else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module + inkhud->nextTile(); + } +} + +void InkHUD::Events::onExitLong() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Slightly longer than playChirp + playBoop(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onExitLong(); + } +} + +void InkHUD::Events::onNavUp() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavUp(); + } +} + +void InkHUD::Events::onNavDown() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + if (consumer) + consumer->onNavDown(); + } +} + +void InkHUD::Events::onNavLeft() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavLeft(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->prevApplet(); + } +} + +void InkHUD::Events::onNavRight() +{ + if (settings->joystick.enabled) { + // Audio feedback (via buzzer) + // Short tone + playChirp(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + + // Check which system applet wants to handle the button press (if any) + SystemApplet *consumer = nullptr; + for (SystemApplet *sa : inkhud->systemApplets) { + if (sa->handleInput) { + consumer = sa; + break; + } + } + + // If no system applet is handling input, default behavior instead is to cycle applets + if (consumer) + consumer->onNavRight(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->nextApplet(); + } +} + // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now int InkHUD::Events::beforeDeepSleep(void *unused) diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index df68f368c..664ca19f0 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -29,6 +29,12 @@ class Events void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press + void onExitShort(); // Exit button: short press + void onExitLong(); // Exit button: long press + void onNavUp(); // Navigate up + void onNavDown(); // Navigate down + void onNavLeft(); // Navigate left + void onNavRight(); // Navigate right int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 90b6718e0..9f05ae5bb 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -80,6 +80,94 @@ void InkHUD::InkHUD::longpress() events->onButtonLong(); } +// Call this when your exit button gets a short press +void InkHUD::InkHUD::exitShort() +{ + events->onExitShort(); +} + +// Call this when your exit button gets a long press +void InkHUD::InkHUD::exitLong() +{ + events->onExitLong(); +} + +// Call this when your joystick gets an up input +void InkHUD::InkHUD::navUp() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavLeft(); + break; + case 2: // 180 deg + events->onNavDown(); + break; + case 3: // 270 deg + events->onNavRight(); + break; + default: // 0 deg + events->onNavUp(); + break; + } +} + +// Call this when your joystick gets a down input +void InkHUD::InkHUD::navDown() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavRight(); + break; + case 2: // 180 deg + events->onNavUp(); + break; + case 3: // 270 deg + events->onNavLeft(); + break; + default: // 0 deg + events->onNavDown(); + break; + } +} + +// Call this when your joystick gets a left input +void InkHUD::InkHUD::navLeft() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavDown(); + break; + case 2: // 180 deg + events->onNavRight(); + break; + case 3: // 270 deg + events->onNavUp(); + break; + default: // 0 deg + events->onNavLeft(); + break; + } +} + +// Call this when your joystick gets a right input +void InkHUD::InkHUD::navRight() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onNavUp(); + break; + case 2: // 180 deg + events->onNavLeft(); + break; + case 3: // 270 deg + events->onNavDown(); + break; + default: // 0 deg + events->onNavRight(); + break; + } +} + // Cycle the next user applet to the foreground // Only activated applets are cycled // If user has a multi-applet layout, the applets will cycle on the "focused tile" @@ -88,6 +176,14 @@ void InkHUD::InkHUD::nextApplet() windowManager->nextApplet(); } +// Cycle the previous user applet to the foreground +// Only activated applets are cycled +// If user has a multi-applet layout, the applets will cycle on the "focused tile" +void InkHUD::InkHUD::prevApplet() +{ + windowManager->prevApplet(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() @@ -95,6 +191,12 @@ void InkHUD::InkHUD::openMenu() windowManager->openMenu(); } +// Bring AlignStick applet to the foreground +void InkHUD::InkHUD::openAlignStick() +{ + windowManager->openAlignStick(); +} + // In layouts where multiple applets are shown at once, change which tile is focused // The focused tile in the one which cycles applets on button short press, and displays menu on long press void InkHUD::InkHUD::nextTile() @@ -102,12 +204,26 @@ void InkHUD::InkHUD::nextTile() windowManager->nextTile(); } +// In layouts where multiple applets are shown at once, change which tile is focused +// The focused tile in the one which cycles applets on button short press, and displays menu on long press +void InkHUD::InkHUD::prevTile() +{ + windowManager->prevTile(); +} + // Rotate the display image by 90 degrees void InkHUD::InkHUD::rotate() { windowManager->rotate(); } +// rotate the joystick in 90 degree increments +void InkHUD::InkHUD::rotateJoystick(uint8_t angle) +{ + persistence->settings.joystick.alignment += angle; + persistence->settings.joystick.alignment %= 4; +} + // Show / hide the battery indicator in top-right void InkHUD::InkHUD::toggleBatteryIcon() { diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 13839ea22..7325d8262 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -55,15 +55,25 @@ class InkHUD void shortpress(); void longpress(); + void exitShort(); + void exitLong(); + void navUp(); + void navDown(); + void navLeft(); + void navRight(); // Trigger UI changes // - called by various InkHUD components // - suitable(?) for use by aux button, connected in variant nicheGraphics.h void nextApplet(); + void prevApplet(); void openMenu(); + void openAlignStick(); void nextTile(); + void prevTile(); void rotate(); + void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); // Updating the display diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index b85274c87..5054b7234 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -29,7 +29,7 @@ class Persistence // Used to invalidate old settings, if needed // Version 0 is reserved for testing, and will always load defaults - static constexpr uint32_t SETTINGS_VERSION = 2; + static constexpr uint32_t SETTINGS_VERSION = 3; struct Settings { struct Meta { @@ -96,6 +96,19 @@ class Persistence bool safeShutdownSeen = false; } tips; + // Joystick settings for enabling and aligning to the screen + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; + + // gets set to true when AlignStick applet is completed + bool aligned = false; + + // Rotation of the joystick + // Multiples of 90 degrees clockwise + uint8_t alignment = 0; + } joystick; + // Rotation of the display // Multiples of 90 degrees clockwise // Most commonly: rotation is 0 when flex connector is oriented below display diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index 80984f399..b985f9f77 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -8,4 +8,5 @@ build_flags = -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = - https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX \ No newline at end of file + # TODO renovate + https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index c883e9a29..0548de1eb 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -2,6 +2,7 @@ #include "./WindowManager.h" +#include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" @@ -98,6 +99,38 @@ void InkHUD::WindowManager::nextTile() userTiles.at(settings->userTiles.focused)->requestHighlight(); } +// Focus on a different tile but decrement index +void InkHUD::WindowManager::prevTile() +{ + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } + + // Swap to next tile + if (settings->userTiles.focused == 0) + settings->userTiles.focused = settings->userTiles.count - 1; + else + settings->userTiles.focused--; + + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); + + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); + + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::WindowManager::openMenu() @@ -106,6 +139,15 @@ void InkHUD::WindowManager::openMenu() menu->show(userTiles.at(settings->userTiles.focused)); } +// Bring the AlignStick applet to the foreground +void InkHUD::WindowManager::openAlignStick() +{ + if (settings->joystick.enabled) { + AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); + alignStick->bringToForeground(); + } +} + // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() @@ -155,6 +197,59 @@ void InkHUD::WindowManager::nextApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// On the currently focussed tile: cycle to the previous available user applet +// Applets available for this must be activated, and not already displayed on another tile +void InkHUD::WindowManager::prevApplet() +{ + Tile *t = userTiles.at(settings->userTiles.focused); + + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; + + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } + } + + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); + + // Iterate forward through the WindowManager::applets, looking for the previous valid applet + Applet *prevValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = 0; + if (i > appletIndex) + newAppletIndex = inkhud->userApplets.size() + appletIndex - i; + else + newAppletIndex = (appletIndex - i); + Applet *a = inkhud->userApplets.at(newAppletIndex); + + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + prevValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } + } + + // Confirm that we found another applet + if (!prevValidApplet) + return; + + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(prevValidApplet); + prevValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST +} + // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { @@ -338,6 +433,8 @@ 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) + addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("Menu", new MenuApplet, nullptr); @@ -360,6 +457,8 @@ 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) + inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 4d1aedf1b..5def48f8c 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -28,8 +28,11 @@ class WindowManager // - call these to make stuff change void nextTile(); + void prevTile(); void openMenu(); + void openAlignStick(); void nextApplet(); + void prevApplet(); void rotate(); void toggleBatteryIcon(); diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index e7821299e..aa23f065f 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -273,7 +273,7 @@ _(Example shows only config required by InkHUD. This is not a complete `env` def extends = esp32s3_base, inkhud ; or nrf52840_base, etc build_src_filter = -${esp32_base.build_src_filter} +${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = @@ -756,12 +756,12 @@ This mapping of emoji to control characters is fairly arbitrary. Selection was i | `0x03` | 🙂 | | `0x04` | 😆 | | `0x05` | 👋 | -| `0x06` | ☀ | +| `0x06` | ☀ | | ~~`0x07`~~ | (bell char, unused) | | `0x08` | 🌧 | -| `0x09` | â˜� | +| `0x09` | â˜� | | ~~`0x0A`~~ | (line feed, unused) | -| `0x0B` | ♥ | +| `0x0B` | ♥ | | `0x0C` | 💩 | | ~~`0x0D`~~ | (carriage return, unused) | | `0x0E` | 🔔 | diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp new file mode 100644 index 000000000..287fb943f --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -0,0 +1,523 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./TwoButtonExtended.h" + +#include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin +#include "PowerFSM.h" +#include "sleep.h" + +using namespace NicheGraphics::Inputs; + +TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); + joystick[Direction::UP] = SimpleButton(); + joystick[Direction::DOWN] = SimpleButton(); + joystick[Direction::LEFT] = SimpleButton(); + joystick[Direction::RIGHT] = SimpleButton(); +} + +// Get access to (or create) the singleton instance of this class +// Accessible inside the ISRs, even though we maybe shouldn't +TwoButtonExtended *TwoButtonExtended::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; + + return singletonInstance; +} + +// Begin receiving button input +// We probably need to do this after sleep, as well as at boot +void TwoButtonExtended::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); + + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::UP].pin != 0xFF) + attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::DOWN].pin != 0xFF) + attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::LEFT].pin != 0xFF) + attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::RIGHT].pin != 0xFF) + attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, + joystickActiveLogic == LOW ? FALLING : RISING); +} + +// Stop receiving button input, and run custom sleep code +// Called before device sleeps. This might be power-off, or just ESP32 light sleep +// Some devices will want to attach interrupts here, for the user button to wake from sleep +void TwoButtonExtended::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); + + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); + + if (joystick[Direction::UP].pin != 0xFF) + detachInterrupt(joystick[Direction::UP].pin); + + if (joystick[Direction::DOWN].pin != 0xFF) + detachInterrupt(joystick[Direction::DOWN].pin); + + if (joystick[Direction::LEFT].pin != 0xFF) + detachInterrupt(joystick[Direction::LEFT].pin); + + if (joystick[Direction::RIGHT].pin != 0xFF) + detachInterrupt(joystick[Direction::RIGHT].pin); +} + +// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings +// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. +// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. +uint8_t TwoButtonExtended::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset + + // Use default pin for variant, if no better source +#ifdef BUTTON_PIN + pin = BUTTON_PIN; +#endif + + // From userPrefs.jsonc, if set +#ifdef USERPREFS_BUTTON_PIN + pin = USERPREFS_BUTTON_PIN; +#endif + + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; + + return pin; +} + +// Configures the wiring and logic of either button +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) +{ + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } + } + + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; + + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + +// Configures the wiring and logic of the joystick buttons +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) +{ + if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || + joystick[Direction::RIGHT].pin == rPin) { + LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); + return; + } + + joystick[Direction::UP].pin = uPin; + joystick[Direction::DOWN].pin = dPin; + joystick[Direction::LEFT].pin = lPin; + joystick[Direction::RIGHT].pin = rPin; + joystickActiveLogic = LOW; + + pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); + 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); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; +} + +void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) +{ + joystickDebounceLength = debounceMs; +} + +// Set what should happen when a button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; +} + +// Set what should happen when a button becomes unpressed +// Use this to implement a "While held" behavior +void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; +} + +// Set what should happen when a "short press" event has occurred +void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) +{ + assert(whichButton < 2); + buttons[whichButton].onPress = onPress; +} + +// Set what should happen when a "long press" event has fired +// Note: this will occur while the button is still held +void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; +} + +// Set what should happen when a joystick button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) +{ + joystick[Direction::UP].onDown = uDown; + joystick[Direction::DOWN].onDown = dDown; + joystick[Direction::LEFT].onDown = lDown; + joystick[Direction::RIGHT].onDown = rDown; +} + +// Set what should happen when a joystick button becomes unpressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) +{ + joystick[Direction::UP].onUp = uUp; + joystick[Direction::DOWN].onUp = dUp; + joystick[Direction::LEFT].onUp = lUp; + joystick[Direction::RIGHT].onUp = rUp; +} + +// Set what should happen when a "press" event has fired +// Note: this will occur while the joystick button is still held +void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) +{ + joystick[Direction::UP].onPress = uPress; + joystick[Direction::DOWN].onPress = dPress; + 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() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the secondary button +// Wakes our button thread +void TwoButtonExtended::isrSecondary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the joystick buttons +// Also wakes our button thread +void TwoButtonExtended::isrJoystickUp() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::UP].state == State::REST) { + b->joystick[Direction::UP].state = State::IRQ; + b->joystick[Direction::UP].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickDown() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::DOWN].state == State::REST) { + b->joystick[Direction::DOWN].state = State::IRQ; + b->joystick[Direction::DOWN].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickLeft() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::LEFT].state == State::REST) { + b->joystick[Direction::LEFT].state = State::IRQ; + b->joystick[Direction::LEFT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickRight() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::RIGHT].state == State::REST) { + b->joystick[Direction::RIGHT].state = State::IRQ; + b->joystick[Direction::RIGHT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Concise method to start our button thread +// Follows an ISR, listening for button release +void TwoButtonExtended::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } +} + +// Concise method to stop our button thread +// Called when we no longer need to poll for button release +void TwoButtonExtended::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } + + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; + joystick[Direction::UP].state = REST; + joystick[Direction::DOWN].state = REST; + joystick[Direction::LEFT].state = REST; + joystick[Direction::RIGHT].state = REST; +} + +// Our button thread +// Started by an IRQ, on either button +// Polls for button releases +// Stops when both buttons released +int32_t TwoButtonExtended::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); + + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; + + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onPress(); // Run callback: press + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; + } + + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // Check all the joystick directions + for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { + switch (joystick[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) + joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as press + case POLLING_UNFIRED: { + uint32_t length = millis() - joystick[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) + joystick[i].state = State::REST; // Mark that the button has reset + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= joystickDebounceLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + joystick[i].state = State::POLLING_FIRED; + joystick[i].onPress(); + } + } + break; + } + + // Button still held after press + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].state = State::REST; + joystick[i].onUp(); // Callback: release of hold + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // If all buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; +} + +#ifdef ARCH_ESP32 + +// Detach our class' interrupts before lightsleep +// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press +int TwoButtonExtended::beforeLightSleep(void *unused) +{ + stop(); + return 0; // Indicates success +} + +// Reconfigure our interrupts +// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep +int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); + + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); + + return 0; // Indicates success +} + +#endif + +#endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h new file mode 100644 index 000000000..23fd78a2a --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -0,0 +1,136 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics input source + +Short and Long press for up to two buttons +Interrupt driven + +*/ + +/* + +This expansion adds support for four more buttons +These buttons are single-action only, no long press +Interrupt driven + +*/ + +#pragma once + +#include "configuration.h" + +#include "assert.h" +#include "functional" + +#ifdef ARCH_ESP32 +#include "esp_sleep.h" // For light-sleep handling +#endif + +#include "Observer.h" + +namespace NicheGraphics::Inputs +{ + +class TwoButtonExtended : protected concurrency::OSThread +{ + public: + typedef std::function Callback; + + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + + static TwoButtonExtended *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + 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 setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setJoystickDebounce(uint32_t debounceMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + 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); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Joystick Directions + enum Direction { UP = 0, DOWN, LEFT, RIGHT }; + + // Data used for direction (single-action) buttons + class SimpleButton + { + public: + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down + + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onPress = noop; + }; + + // Data used for double-action buttons + class Button : public SimpleButton + { + public: + // Per-button extended config + bool activeLogic = LOW; // Active LOW by default. + uint32_t debounceLength = 50; // Minimum length for shortpress in ms + uint32_t longpressLength = 500; // Time until longpress in ms + + // Per-button event callbacks + std::function onLongPress = noop; + }; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButtonExtended::afterLightSleep); +#endif + + int32_t runOnce() override; // Timer method. Polls for button release + + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release + + static void isrPrimary(); // User Button ISR + static void isrSecondary(); // optional aux button or joystick center + static void isrJoystickUp(); + static void isrJoystickDown(); + static void isrJoystickLeft(); + static void isrJoystickRight(); + + TwoButtonExtended(); // Constructor made private: force use of Button::instance() + + // Info about both buttons + Button buttons[2]; + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms + SimpleButton joystick[4]; +}; + +}; // namespace NicheGraphics::Inputs + +#endif diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp new file mode 100644 index 000000000..87c8a24ae --- /dev/null +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -0,0 +1,217 @@ +#if defined(HACKADAY_COMMUNICATOR) + +#include "HackadayCommunicatorKeyboard.h" +#include "main.h" + +#define _TCA8418_COLS 10 +#define _TCA8418_ROWS 8 +#define _TCA8418_NUM_KEYS 80 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierRightShiftKey = 30; +constexpr uint8_t modifierRightShift = 0b0001; +constexpr uint8_t modifierLeftShiftKey = 76; // keynum -1 +constexpr uint8_t modifierLeftShift = 0b0001; +// constexpr uint8_t modifierSymKey = 42; +// constexpr uint8_t modifierSym = 0b0010; + +// Num chars per key, Modulus for rotating through characters +static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = { + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0, +}; + +static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, + {}, + {'+'}, + {'9'}, + {'8'}, + {'7'}, + {'2'}, + {'3'}, + {'4'}, + {'5'}, + {Key::ESC}, + {'q', 'Q'}, + {'w', 'W'}, + {'e', 'E'}, + {'r', 'R'}, + {'t', 'T'}, + {'y', 'Y'}, + {'u', 'U'}, + {'i', 'I'}, + {'o', 'O'}, + {Key::TAB}, + {'a', 'A'}, + {'s', 'S'}, + {'d', 'D'}, + {'f', 'F'}, + {'g', 'G'}, + {'h', 'H'}, + {'j', 'J'}, + {'k', 'K'}, + {'l', 'L'}, + {}, + {'z', 'Z'}, + {'x', 'X'}, + {'c', 'C'}, + {'v', 'V'}, + {'b', 'B'}, + {'n', 'N'}, + {'m', 'M'}, + {',', '<'}, + {'.', '>'}, + {}, + {}, + {}, + {'\\'}, + {' '}, + {}, + {Key::RIGHT}, + {Key::DOWN}, + {Key::LEFT}, + {}, + {}, + {}, + {'-'}, + {'6', '^'}, + {'5', '%'}, + {'4', '$'}, + {'[', '{'}, + {']', '}'}, + {'p', 'P'}, + {}, + {}, + {}, + {'*'}, + {'3', '#'}, + {'2', '@'}, + {'1', '!'}, + {Key::SELECT}, + {'\'', '"'}, + {';', ':'}, + {}, + {}, + {}, + {'/', '?'}, + {'='}, + {'.', '>'}, + {'0', ')'}, + {}, + {Key::UP}, + {Key::BSP}, + {}}; + +HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard() + : 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 HackadayCommunicatorKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + enableInterrupts(); +} + +// handle multi-key presses (shift and alt) +void HackadayCommunicatorKeyboard::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 HackadayCommunicatorKeyboard::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 HackadayCommunicatorKeyboard::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 (HackadayCommunicatorTapMod[last_key]) + queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierLeftShiftKey) { + modifierFlag ^= modifierLeftShift; + } +} + +bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierLeftShiftKey); +} + +#endif \ No newline at end of file diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h new file mode 100644 index 000000000..8316bed72 --- /dev/null +++ b/src/input/HackadayCommunicatorKeyboard.h @@ -0,0 +1,26 @@ +#include "TCA8418KeyboardBase.h" + +class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase +{ + public: + HackadayCommunicatorKeyboard(); + void reset(void); + void trigger(void) override; + virtual ~HackadayCommunicatorKeyboard() {} + + 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; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + 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.h b/src/input/InputBroker.h index 36328ca64..c55d7fa53 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -3,6 +3,12 @@ #include "Observer.h" #include "freertosinc.h" +#ifdef InputBrokerDebug +#define LOG_INPUT(...) LOG_DEBUG(__VA_ARGS__) +#else +#define LOG_INPUT(...) +#endif + enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, @@ -47,6 +53,7 @@ typedef struct _InputEvent { class InputPollable { public: + virtual ~InputPollable() = default; virtual void pollOnce() = 0; }; diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index 7b43fa256..cc1222595 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -3,6 +3,9 @@ #include "RotaryEncoderImpl.h" #include "InputBroker.h" #include "RotaryEncoder.h" +#ifdef ARCH_ESP32 +#include "sleep.h" +#endif #define ORIGIN_NAME "RotaryEncoder" @@ -11,6 +14,20 @@ RotaryEncoderImpl *rotaryEncoderImpl; RotaryEncoderImpl::RotaryEncoderImpl() { rotary = nullptr; +#ifdef ARCH_ESP32 + isFirstInit = true; +#endif +} + +RotaryEncoderImpl::~RotaryEncoderImpl() +{ + LOG_DEBUG("RotaryEncoderImpl destructor"); + detachRotaryEncoderInterrupts(); + + if (rotary != nullptr) { + delete rotary; + rotary = nullptr; + } } bool RotaryEncoderImpl::init() @@ -25,15 +42,22 @@ bool RotaryEncoderImpl::init() eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); - rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, - moduleConfig.canned_message.inputbroker_pin_press); - rotary->resetButton(); + if (rotary == nullptr) { + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + } - interruptInstance = this; - auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); - attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + attachRotaryEncoderInterrupts(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + if (isFirstInit) { + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); + isFirstInit = false; + } +#endif LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, @@ -71,6 +95,50 @@ void RotaryEncoderImpl::pollOnce() } } +void RotaryEncoderImpl::detachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl detach button interrupts"); + if (interruptInstance == this) { + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b); + detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press); + interruptInstance = nullptr; + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already detached"); + } +} + +void RotaryEncoderImpl::attachRotaryEncoderInterrupts() +{ + LOG_DEBUG("RotaryEncoderImpl attach button interrupts"); + if (rotary != nullptr && interruptInstance == nullptr) { + rotary->resetButton(); + + interruptInstance = this; + auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); }; + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE); + attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE); + } else { + LOG_WARN("RotaryEncoderImpl: interrupts already attached"); + } +} + +#ifdef ARCH_ESP32 + +int RotaryEncoderImpl::beforeLightSleep(void *unused) +{ + detachRotaryEncoderInterrupts(); + return 0; // Indicates success; +} + +int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachRotaryEncoderInterrupts(); + return 0; // Indicates success; +} +#endif + RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance; #endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h index 6f8e9fe5f..ec8a064bd 100644 --- a/src/input/RotaryEncoderImpl.h +++ b/src/input/RotaryEncoderImpl.h @@ -8,12 +8,18 @@ class RotaryEncoder; -class RotaryEncoderImpl : public InputPollable +class RotaryEncoderImpl final : public InputPollable { public: RotaryEncoderImpl(); - bool init(void); + ~RotaryEncoderImpl() override; + bool init(); virtual void pollOnce() override; + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif protected: static RotaryEncoderImpl *interruptInstance; @@ -23,6 +29,21 @@ class RotaryEncoderImpl : public InputPollable input_broker_event eventPressed = INPUT_BROKER_NONE; RotaryEncoder *rotary; + + private: +#ifdef ARCH_ESP32 + bool isFirstInit; +#endif + void detachRotaryEncoderInterrupts(); + void attachRotaryEncoderInterrupts(); + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep); +#endif }; extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 12cbc36fb..1da2ea008 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -27,7 +27,9 @@ bool RotaryEncoderInterruptImpl1::init() RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif return true; } diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 2df1ace70..a5d2c614f 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -2,6 +2,8 @@ #include "configuration.h" #include +SerialKeyboard *globalSerialKeyboard = nullptr; + #ifdef INPUTBROKER_SERIAL_TYPE #define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file @@ -25,6 +27,8 @@ unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w', SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) { this->_originName = name; + + globalSerialKeyboard = this; } void SerialKeyboard::erase() @@ -85,9 +89,21 @@ int32_t SerialKeyboard::runOnce() e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT if (!(shiftRegister2 & (1 << 3))) { - e.inputEvent = INPUT_BROKER_UP; + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_LEFT; + } } else if (!(shiftRegister2 & (1 << 2))) { - e.inputEvent = INPUT_BROKER_RIGHT; + if (shift > 0) { + e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED + e.kbchar = 0x09; // TAB + shift = 0; // reset shift after TAB + } else { + e.inputEvent = INPUT_BROKER_RIGHT; + } e.kbchar = 0; } else if (!(shiftRegister2 & (1 << 1))) { e.inputEvent = INPUT_BROKER_SELECT; diff --git a/src/input/SerialKeyboard.h b/src/input/SerialKeyboard.h index 1480c4d58..f25eb2630 100644 --- a/src/input/SerialKeyboard.h +++ b/src/input/SerialKeyboard.h @@ -8,6 +8,8 @@ class SerialKeyboard : public Observable, public concurrency public: explicit SerialKeyboard(const char *name); + uint8_t getShift() const { return shift; } + protected: virtual int32_t runOnce() override; void erase(); @@ -22,4 +24,6 @@ class SerialKeyboard : public Observable, public concurrency int lastKeyPressed = 13; int quickPress = 0; unsigned long lastPressTime = 0; -}; \ No newline at end of file +}; + +extern SerialKeyboard *globalSerialKeyboard; \ No newline at end of file diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index 4ddaf7064..bbd07e199 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -45,7 +45,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, pinPress); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif this->setInterval(100); } @@ -88,6 +90,50 @@ int32_t TrackballInterruptBase::runOnce() } } + if (directionDetected && directionStartTime > 0) { + uint32_t directionDuration = millis() - directionStartTime; + uint8_t directionPressedNow = 0; + directionInterval++; + + if (!digitalRead(_pinUp)) { + directionPressedNow = TB_ACTION_UP; + } else if (!digitalRead(_pinDown)) { + directionPressedNow = TB_ACTION_DOWN; + } else if (!digitalRead(_pinLeft)) { + directionPressedNow = TB_ACTION_LEFT; + } else if (!digitalRead(_pinRight)) { + directionPressedNow = TB_ACTION_RIGHT; + } + + const uint8_t DIRECTION_REPEAT_THRESHOLD = 3; + + if (directionPressedNow == TB_ACTION_NONE) { + // Reset state + directionDetected = false; + directionStartTime = 0; + directionInterval = 0; + this->action = TB_ACTION_NONE; + } else if (directionDuration >= LONG_PRESS_DURATION && directionInterval >= DIRECTION_REPEAT_THRESHOLD) { + // repeat event when long press these direction. + switch (directionPressedNow) { + case TB_ACTION_UP: + e.inputEvent = this->_eventUp; + break; + case TB_ACTION_DOWN: + e.inputEvent = this->_eventDown; + break; + case TB_ACTION_LEFT: + e.inputEvent = this->_eventLeft; + break; + case TB_ACTION_RIGHT: + e.inputEvent = this->_eventRight; + break; + } + + directionInterval = 0; + } + } + #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball if (this->action == TB_ACTION_PRESSED && !pressDetected) { // Start long press detection @@ -113,17 +159,22 @@ int32_t TrackballInterruptBase::runOnce() pressDetected = true; pressStartTime = millis(); // Don't send event yet, wait to see if it's a long press - } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { - // LOG_DEBUG("Trackball event UP"); + } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) { - // LOG_DEBUG("Trackball event DOWN"); + // send event first,will automatically trigger every 50ms * 3 after 500ms + } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) { - // LOG_DEBUG("Trackball event LEFT"); + } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) { - // LOG_DEBUG("Trackball event RIGHT"); + } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight) && !directionDetected) { + directionDetected = true; + directionStartTime = millis(); e.inputEvent = this->_eventRight; } #endif diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 76a99f33d..67d4ee449 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -49,10 +49,14 @@ class TrackballInterruptBase : public Observable, public con // Long press detection for press button uint32_t pressStartTime = 0; + uint32_t directionStartTime = 0; + uint8_t directionInterval = 0; bool pressDetected = false; + bool directionDetected = false; uint32_t lastLongPressEventTime = 0; + uint32_t lastDirectionPressEventTime = 0; static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; // ms - interval between repeated long press events private: input_broker_event _eventDown = INPUT_BROKER_NONE; diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index ae84efdaf..2b9d38c83 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -3,6 +3,14 @@ #include "InputBroker.h" #include "mesh/NodeDB.h" +#ifndef UPDOWN_LONG_PRESS_DURATION +#define UPDOWN_LONG_PRESS_DURATION 300 +#endif + +#ifndef UPDOWN_LONG_PRESS_REPEAT_INTERVAL +#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 300 +#endif + class UpDownInterruptBase : public Observable, public concurrency::OSThread { public: @@ -40,8 +48,8 @@ class UpDownInterruptBase : public Observable, public concur uint32_t lastPressLongEventTime = 0; uint32_t lastUpLongEventTime = 0; uint32_t lastDownLongEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 300; - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; + static const uint32_t LONG_PRESS_DURATION = UPDOWN_LONG_PRESS_DURATION; + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = UPDOWN_LONG_PRESS_REPEAT_INTERVAL; private: uint8_t _pinDown = 0; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 9b0b1f39e..906dcd2a8 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -29,7 +29,9 @@ bool UpDownInterruptImpl1::init() eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; +#endif return true; } diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 0ed2df116..12d0822f6 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(HACKADAY_COMMUNICATOR) +#include "HackadayCommunicatorKeyboard.h" #else #include "TCA8418Keyboard.h" #endif @@ -20,6 +22,8 @@ KbI2cBase::KbI2cBase(const char *name) TCAKeyboard(*(new TDeckProKeyboard())) #elif defined(T_LORA_PAGER) TCAKeyboard(*(new TLoraPagerKeyboard())) +#elif defined(HACKADAY_COMMUNICATOR) + TCAKeyboard(*(new HackadayCommunicatorKeyboard())) #else TCAKeyboard(*(new TCA8418Keyboard())) #endif @@ -328,7 +332,7 @@ int32_t KbI2cBase::runOnce() break; } if (e.inputEvent != INPUT_BROKER_NONE) { - LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); + // LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); this->notifyObservers(&e); } TCAKeyboard.trigger(); @@ -485,8 +489,6 @@ int32_t KbI2cBase::runOnce() case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x91: // fn+t case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - - case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST // just pass those unmodified diff --git a/src/main.cpp b/src/main.cpp index 61975063a..da58930b2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -394,7 +394,10 @@ void setup() io.pinMode(EXPANDS_GPIO_EN, OUTPUT); io.digitalWrite(EXPANDS_GPIO_EN, HIGH); io.pinMode(EXPANDS_SD_PULLEN, INPUT); +#elif defined(HACKADAY_COMMUNICATOR) + pinMode(KB_INT, INPUT); #endif + concurrency::hasBeenSetup = true; #if ARCH_PORTDUINO SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); @@ -425,10 +428,17 @@ void setup() #endif #if ARCH_PORTDUINO + RTCQuality ourQuality = RTCQualityDevice; + + std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); + if (timeCommandResult[0] == '1') { + ourQuality = RTCQualityNTP; + } + struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityDevice, &tv); + perhapsSetRTC(ourQuality, &tv); #endif powerMonInit(); @@ -436,6 +446,13 @@ void setup() LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); +#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) +#ifndef SENSECAP_INDICATOR + // use PSRAM for malloc calls > 256 bytes + heap_caps_malloc_extmem_enable(256); +#endif +#endif + #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO)); @@ -878,7 +895,7 @@ void setup() #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(USE_SPISSD1306) + defined(USE_SPISSD1306) || defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) && @@ -1155,7 +1172,7 @@ void setup() // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \ - defined(USE_SPISSD1306) + defined(USE_ST7796) || defined(USE_SPISSD1306) || defined(HACKADAY_COMMUNICATOR) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) @@ -1169,11 +1186,6 @@ void setup() #endif #endif -#ifdef PIN_PWR_DELAY_MS - // This may be required to give the peripherals time to power up. - delay(PIN_PWR_DELAY_MS); -#endif - #ifdef ARCH_PORTDUINO // as one can't use a function pointer to the class constructor: auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, @@ -1448,8 +1460,10 @@ void setup() #endif #if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) +#ifndef HAS_PHYSICAL_KEYBOARD osk_found = true; #endif +#endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index b53f552fa..a3cc7791c 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -96,6 +96,8 @@ class Channels bool setDefaultPresetCryptoForHash(ChannelHash channelHash); + int16_t getHash(ChannelIndex i) { return hashes[i]; } + private: /** Given a channel index, change to use the crypto key specified by that index * @@ -113,8 +115,6 @@ class Channels */ int16_t generateHash(ChannelIndex channelNum); - int16_t getHash(ChannelIndex i) { return hashes[i]; } - /** * Validate a channel, fixing any errors as needed */ diff --git a/src/mesh/Default.h b/src/mesh/Default.h index 218d8d0fb..a60e3af9b 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -57,14 +57,7 @@ class Default // Note: Kept as uint32_t to match the public API parameter type static float congestionScalingCoefficient(uint32_t numOnlineNodes) { - // Increase frequency of broadcasts for small networks regardless of preset - if (numOnlineNodes <= 10) { - return 0.6; - } else if (numOnlineNodes <= 20) { - return 0.7; - } else if (numOnlineNodes <= 30) { - return 0.8; - } else if (numOnlineNodes <= 40) { + if (numOnlineNodes <= 40) { return 1.0; } else { float throttlingFactor = 0.075; diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 032be241b..78602a9ec 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -4,6 +4,7 @@ #include "configuration.h" #include "mesh-pb-constants.h" #include "meshUtils.h" +#include "modules/TextMessageModule.h" #if !MESHTASTIC_EXCLUDE_TRACEROUTE #include "modules/TraceRouteModule.h" #endif @@ -35,6 +36,10 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) return true; // we handled it, so stop processing } + if (!seenRecently && !wasUpgraded && textMessageModule) { + seenRecently = textMessageModule->recentlySeen(p->id); + } + if (seenRecently) { printPacket("Ignore dupe incoming msg", p); rxDupe++; @@ -124,6 +129,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p) if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) { iface->clampToLateRebroadcastWindow(getFrom(p), p->id); } + if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB && + nodeDB->isFromOrToFavoritedNode(*p)) { + iface->clampToLateRebroadcastWindow(getFrom(p), p->id); + } } bool FloodingRouter::isRebroadcaster() diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index c5748a560..83b64a873 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -195,7 +195,7 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) // but opted NOT TO. Because it is not a good idea to let remote nodes 'probe' to find out which PSKs were "good" vs // bad. routingModule->sendAckNak(meshtastic_Routing_Error_NO_RESPONSE, getFrom(&mp), mp.id, mp.channel, - routingModule->getHopLimitForResponse(mp.hop_start, mp.hop_limit)); + routingModule->getHopLimitForResponse(mp)); } } @@ -235,7 +235,7 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) assert(p->which_payload_variant == meshtastic_MeshPacket_decoded_tag); // Should already be set by now p->to = getFrom(&to); // Make sure that if we are sending to the local node, we use our local node addr, not 0 p->channel = to.channel; // Use the same channel that the request came in on - p->hop_limit = routingModule->getHopLimitForResponse(to.hop_start, to.hop_limit); + p->hop_limit = routingModule->getHopLimitForResponse(to); // No need for an ack if we are just delivering locally (it just generates an ignored ack) p->want_ack = (to.from != 0) ? to.want_ack : false; diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index eda3f8881..63f401d18 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -44,6 +44,7 @@ struct UIFrameEvent { REDRAW_ONLY, // Don't change which frames are show, just redraw, asap REGENERATE_FRAMESET, // Regenerate (change? add? remove?) screen frames, honoring requestFocus() REGENERATE_FRAMESET_BACKGROUND, // Regenerate screen frames, Attempt to remain on the same frame throughout + SWITCH_TO_TEXTMESSAGE // Jump directly to the Text Message screen } action = REDRAW_ONLY; // We might want to pass additional data inside this struct at some point @@ -225,4 +226,4 @@ class MeshModule /** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet * This ensures that if the request packet was sent reliably, the reply is sent that way as well. */ -void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); \ No newline at end of file +void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to); diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 1b2af082d..c1b3839bb 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -7,10 +7,12 @@ #include "../concurrency/Periodic.h" #include "BluetoothCommon.h" // needed for updateBatteryLevel, FIXME, eventually when we pull mesh out into a lib we shouldn't be whacking bluetooth from here #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "RTC.h" #include "TypeConversions.h" +#include "graphics/draw/MessageRenderer.h" #include "main.h" #include "mesh-pb-constants.h" #include "meshUtils.h" @@ -93,11 +95,8 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp) } else if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && !nodeDB->getMeshNode(mp->from)->has_user && nodeInfoModule && !isPreferredRebroadcaster && !nodeDB->isFull()) { if (airTime->isTxAllowedChannelUtil(true)) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - auto hopStart = mp->hop_start; - auto hopLimit = mp->hop_limit; - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit + 2) { + const int8_t hopsUsed = getHopsAway(*mp, config.lora.hop_limit); + if (hopsUsed > (int32_t)(config.lora.hop_limit + 2)) { LOG_DEBUG("Skip send NodeInfo: %d hops away is too far away", hopsUsed); } else { LOG_INFO("Heard new node on ch. %d, send NodeInfo and ask for response", mp->channel); @@ -192,8 +191,14 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p) p.id = generatePacketId(); // If the phone didn't supply one, then pick one p.rx_time = getValidTime(RTCQualityFromNet); // Record the time the packet arrived from the phone - // (so we update our nodedb for the local node) + IF_SCREEN(if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && p.decoded.payload.size > 0 && + p.to != NODENUM_BROADCAST && p.to != 0) // DM only + { + perhapsDecode(&p); + const StoredMessage &sm = messageStore.addFromPacket(p); + graphics::MessageRenderer::handleNewMessage(nullptr, sm, p); // notify UI + }) // Send the packet into the mesh DEBUG_HEAP_BEFORE; auto a = packetPool.allocCopy(p); @@ -276,6 +281,10 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies) if (nodeDB->hasValidPosition(node)) { #if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS if (positionModule) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position ping; no fresh position since boot"); + return false; + } LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel); positionModule->sendOurPosition(dest, wantReplies, node->channel); return true; diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index afdb4d096..5230e5b85 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -64,7 +64,7 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) perhapsRebroadcast(p); } } else { - bool isRepeated = p->hop_start > 0 && p->hop_start == p->hop_limit; + bool isRepeated = getHopsAway(*p) == 0; // If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again if (isRepeated) { if (!findInTxQueue(p->from, p->id)) { @@ -101,8 +101,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to); bool weWereSoleRelayer = false; bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer); - if ((weWereRelayer && wasAlreadyRelayer) || - (p->hop_start != 0 && p->hop_start == p->hop_limit && weWereSoleRelayer)) { + if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) { if (origTx->next_hop != p->relay_node) { // Not already set LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from, p->relay_node, wasAlreadyRelayer, weWereSoleRelayer); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ff76baaa1..3c408f01f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -664,7 +664,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \ + defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); @@ -804,11 +805,16 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 500; moduleConfig.external_notification.nag_timeout = 2; #endif -#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) - // Default to RAK led pin 2 (blue) +#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) || \ + defined(ELECROW_ThinkNode_M6) + // Default to PIN_LED2 for external notification output (LED color depends on device variant) moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = PIN_LED2; +#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3) + moduleConfig.external_notification.active = false; +#else moduleConfig.external_notification.active = true; +#endif moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; @@ -1038,6 +1044,7 @@ void NodeDB::clearLocalPosition() node->position.altitude = 0; node->position.time = 0; setLocalPosition(meshtastic_Position_init_default); + localPositionUpdatedSinceBoot = false; } void NodeDB::cleanupMeshDB() @@ -1542,6 +1549,23 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p) return delta; } +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 + // 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. For encrypted packets, this returns + // defaultIfUnknown when hop_start is 0. + if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)) + return defaultIfUnknown; // Cannot reliably determine the number of hops. + + // Guard against invalid values. + if (p.hop_start < p.hop_limit) + return defaultIfUnknown; + + return p.hop_start - p.hop_limit; +} + #define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline size_t NodeDB::getNumOnlineMeshNodes(bool localOnly) @@ -1794,9 +1818,10 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT // If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway - if (mp.hop_start != 0 && mp.hop_limit <= mp.hop_start) { + const int8_t hopsAway = getHopsAway(mp); + if (hopsAway >= 0) { info->has_hops_away = true; - info->hops_away = mp.hop_start - mp.hop_limit; + info->hops_away = hopsAway; } sortMeshDB(); } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 306acc0a5..817e31617 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -110,6 +110,10 @@ uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n); /// Given a packet, return how many seconds in the past (vs now) it was received uint32_t sinceReceived(const meshtastic_MeshPacket *p); +/// Given a packet, return the number of hops used to reach this node. +/// Returns defaultIfUnknown if the number of hops couldn't be determined. +int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown = -1); + enum LoadFileResult { // Successfully opened the file LOAD_SUCCESS = 1, @@ -279,9 +283,13 @@ class NodeDB LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i, position.time, position.timestamp); localPosition = position; + if (position.latitude_i != 0 || position.longitude_i != 0) { + localPositionUpdatedSinceBoot = true; + } } bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; } #if !defined(MESHTASTIC_EXCLUDE_PKI) bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest); @@ -301,6 +309,7 @@ class NodeDB private: bool duplicateWarned = false; + bool localPositionUpdatedSinceBoot = false; uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually uint32_t lastSort = 0; // When last sorted the nodeDB diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 3c0da4494..6660549eb 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -171,7 +171,8 @@ 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, true), + 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), /* Nepal @@ -245,7 +246,8 @@ uint32_t RadioInterface::getPacketTime(const meshtastic_MeshPacket *p, bool rece /** The delay to use for retransmitting dropped packets */ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) { - size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded); + size_t numbytes =p->which_payload_variant == meshtastic_MeshPacket_decoded_tag ? + pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded) : p->encrypted.size+MESHTASTIC_HEADER_LENGTH; uint32_t packetAirtime = getPacketTime(numbytes + sizeof(PacketHeader)); // Make sure enough time has elapsed for this packet to be sent and an ACK is received. // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d", packetAirtime, slotTimeMsec); @@ -296,11 +298,6 @@ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) return true; } - // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early - if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) { - return nodeDB->isFromOrToFavoritedNode(*p); - } - return false; } @@ -503,6 +500,11 @@ void RadioInterface::applyModemConfig() cr = 5; sf = 10; break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO: + bw = (myRegion->wideLora) ? 1625.0 : 500; + cr = 8; + sf = 11; + break; default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal. bw = (myRegion->wideLora) ? 812.5 : 250; cr = 5; @@ -519,6 +521,10 @@ void RadioInterface::applyModemConfig() sf = 12; break; } + 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; cr = loraConfig.coding_rate; @@ -539,13 +545,26 @@ void RadioInterface::applyModemConfig() } if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) { - static const char *err_string = "Regional frequency range is smaller than bandwidth. Fall back to default preset"; - LOG_ERROR(err_string); + 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; - sprintf(cn->message, err_string); + snprintf(cn->message, sizeof(cn->message), "%s", err_string); service->sendClientNotification(cn); // Set to default modem preset diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 00066a7a3..2b9b17183 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -1,6 +1,7 @@ #include "ReliableRouter.h" #include "Default.h" #include "MeshTypes.h" +#include "NodeDB.h" #include "configuration.h" #include "memGet.h" #include "mesh-pb-constants.h" @@ -108,12 +109,12 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas // If this packet should always be ACKed reliably with want_ack back to the original sender, make sure we // do that unconditionally. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit), true); + routingModule->getHopLimitForResponse(*p), true); } else if (!p->decoded.request_id && !p->decoded.reply_id) { // If it's not an ACK or a reply, send an ACK. sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); - } else if ((p->hop_start > 0 && p->hop_start == p->hop_limit) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + routingModule->getHopLimitForResponse(*p)); + } else if ((getHopsAway(*p) == 0) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { // If we received the packet directly from the original sender, send a 0-hop ACK since the original sender // won't overhear any implicit ACKs. If we received the packet via NextHopRouter, also send a 0-hop ACK to // stop the immediate relayer's retransmissions. @@ -123,11 +124,11 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } else { // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + routingModule->getHopLimitForResponse(*p)); } } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions @@ -150,7 +151,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0; // We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records - if (ackId || nakId) { + if ((ackId || nakId) && + // Implicit ACKs from MQTT should not stop retransmissions + !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) { LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId); if (ackId) { stopRetransmission(p->to, ackId); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 05f47d7f4..a3861521a 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -37,8 +37,8 @@ static MemoryDynamic dynamicPool; Allocator &packetPool = dynamicPool; -#elif defined(ARCH_STM32WL) -// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically. +#elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) +// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. // For now, make it dynamic again. #define MAX_PACKETS \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ @@ -81,8 +81,7 @@ Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRA bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) { // First hop MUST always decrement to prevent retry issues - bool isFirstHop = (p->hop_start != 0 && p->hop_start == p->hop_limit); - if (isFirstHop) { + if (getHopsAway(*p) == 0) { return true; // Always decrement on first hop } @@ -114,7 +113,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p) // Check 3: role check (moderate cost - multiple comparisons) if (!IS_ONE_OF(node->user.role, meshtastic_Config_DeviceConfig_Role_ROUTER, - meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { continue; } @@ -526,6 +525,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) #elif ARCH_PORTDUINO 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.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) { + JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl; + } } #endif return DecodeState::DECODE_SUCCESS; @@ -688,7 +691,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) // Store a copy of encrypted packet for MQTT DEBUG_HEAP_BEFORE; - meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p); + p_encrypted = packetPool.allocCopy(*p); DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted); // Take those raw bytes and convert them back into a well structured protobuf we can understand @@ -726,7 +729,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_ROUTING_APP, meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_ADMIN_APP, meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_KEY_VERIFICATION_APP, meshtastic_PortNum_WAYPOINT_APP, - meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP)) { + meshtastic_PortNum_STORE_FORWARD_APP, meshtastic_PortNum_TRACEROUTE_APP, + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP)) { LOG_DEBUG("Ignore packet on non-standard portnum for CORE_PORTNUMS_ONLY"); cancelSending(p->from, p->id); skipHandle = true; @@ -741,19 +745,24 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) MeshModule::callModules(*p, src); #if !MESHTASTIC_EXCLUDE_MQTT - // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not to - // us (because we would be able to decrypt it) - if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && - !isBroadcast(p->to) && !isToUs(p)) - 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) - mqtt->onSend(*p_encrypted, *p, p->channel); + if (p_encrypted == nullptr) { + LOG_WARN("p_encrypted is null, skipping MQTT publish"); + } else { + // Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not + // to us (because we would be able to decrypt it) + if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 && + !isBroadcast(p->to) && !isToUs(p)) + 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) + mqtt->onSend(*p_encrypted, *p, p->channel); + } #endif } packetPool.release(p_encrypted); // Release the encrypted packet + p_encrypted = nullptr; } void Router::perhapsHandleReceived(meshtastic_MeshPacket *p) diff --git a/src/mesh/Router.h b/src/mesh/Router.h index 10a3771a7..dbe6f4f39 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -91,6 +91,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory before us */ uint32_t rxDupe = 0, txRelayCanceled = 0; + // pointer to the encrypted packet + meshtastic_MeshPacket *p_encrypted = nullptr; + protected: friend class RoutingModule; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e1f07a32b..2d7996a13 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -62,6 +62,11 @@ template bool SX126xInterface::init() digitalWrite(LORA_PA_TX_EN, LOW); #endif +#ifdef RF95_FAN_EN + digitalWrite(RF95_FAN_EN, HIGH); + pinMode(RF95_FAN_EN, OUTPUT); +#endif + #if ARCH_PORTDUINO tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { @@ -85,6 +90,13 @@ template bool SX126xInterface::init() power = -9; int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); + +#ifdef SX126X_PA_RAMP_US + // Set custom PA ramp time for boards requiring longer stabilization (e.g., T-Beam 1W needs >800us) + if (res == RADIOLIB_ERR_NONE) { + lora.setPaRampTime(SX126X_PA_RAMP_US); + } +#endif // \todo Display actual typename of the adapter, not just `SX126x` LOG_INFO("SX126x init result %d", res); if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 4c4d0e3d1..ccece50d3 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -33,3 +33,5 @@ PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index a542cf29c..b3edf7d43 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -16,6 +16,16 @@ #endif /* Enum definitions */ +/* Firmware update mode for OTA updates */ +typedef enum _meshtastic_OTAMode { + /* Do not reboot into OTA mode */ + meshtastic_OTAMode_NO_REBOOT_OTA = 0, + /* Reboot into OTA mode for BLE firmware update */ + meshtastic_OTAMode_OTA_BLE = 1, + /* Reboot into OTA mode for WiFi firmware update */ + meshtastic_OTAMode_OTA_WIFI = 2 +} meshtastic_OTAMode; + /* TODO: REPLACE */ typedef enum _meshtastic_AdminMessage_ConfigType { /* TODO: REPLACE */ @@ -258,10 +268,13 @@ typedef struct _meshtastic_AdminMessage { meshtastic_SharedContact add_contact; /* Initiate or respond to a key verification request */ meshtastic_KeyVerificationAdmin key_verification; + /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */ + meshtastic_OTAMode reboot_ota_mode; /* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */ int32_t factory_reset_device; /* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) - Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */ + Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. + Deprecated in favor of reboot_ota_mode in 2.7.17 */ int32_t reboot_ota_seconds; /* This message is only supported for the simulator Portduino build. If received the simulator will exit successfully. */ @@ -288,6 +301,10 @@ extern "C" { #endif /* Helper constants for enums */ +#define _meshtastic_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA +#define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI +#define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1)) + #define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG #define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) @@ -309,6 +326,7 @@ extern "C" { #define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation #define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation +#define meshtastic_AdminMessage_payload_variant_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode @@ -396,6 +414,7 @@ extern "C" { #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 #define meshtastic_AdminMessage_key_verification_tag 67 +#define meshtastic_AdminMessage_reboot_ota_mode_tag 68 #define meshtastic_AdminMessage_factory_reset_device_tag 94 #define meshtastic_AdminMessage_reboot_ota_seconds_tag 95 #define meshtastic_AdminMessage_exit_simulator_tag 96 @@ -454,6 +473,7 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_ed X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \ +X(a, STATIC, ONEOF, UENUM, (payload_variant,reboot_ota_mode,reboot_ota_mode), 68) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \ diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 327568316..d4ef5bee4 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -293,7 +293,8 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { /* Long Range - Fast */ meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0, - /* Long Range - Slow */ + /* Long Range - Slow + Deprecated in 2.7: Unpopular slow preset. */ meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1, /* Very Long Range - Slow Deprecated in 2.5: Works only with txco and is unusably slow */ @@ -311,7 +312,10 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { /* Short Range - Turbo This is the fastest preset and the only one with 500kHz bandwidth. It is not legal to use in all regions due to this wider bandwidth. */ - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8 + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8, + /* Long Range - Turbo + This preset performs similarly to LongFast, but with 500Khz bandwidth. */ + meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { @@ -689,8 +693,8 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST -#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO -#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1)) +#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_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 9966e52f8..d8eee1203 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2) PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO) +PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2) + + PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) @@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU + + diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 46de1dee0..75a490550 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -237,8 +237,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_T_ETH_ELITE = 91, /* Heltec HRI-3621 industrial probe */ meshtastic_HardwareModel_HELTEC_SENSOR_HUB = 92, - /* Reserved Fried Chicken ID for future use */ - meshtastic_HardwareModel_RESERVED_FRIED_CHICKEN = 93, + /* Muzi Works Muzi-Base device */ + meshtastic_HardwareModel_MUZI_BASE = 93, /* Heltec Magnetic Power Bank with Meshtastic compatible */ meshtastic_HardwareModel_HELTEC_MESH_POCKET = 94, /* Seeed Solar Node */ @@ -478,6 +478,22 @@ typedef enum _meshtastic_Routing_Error { meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38 } meshtastic_Routing_Error; +/* Enum of message types */ +typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type { + /* Send an announcement of the canonical tip of a chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0, + /* Query whether a specific link is on the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1, + /* Request the next link in the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3, + /* Provide a link to add to the chain */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4, + /* If we must fragment, send the first half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5, + /* If we must fragment, send the second half */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6 +} meshtastic_StoreForwardPlusPlus_SFPP_message_type; + /* The priority of this message for sending. Higher priorities are sent first (when managing the transmit queue). This field is never sent over the air, it is only used internally inside of a local device node. @@ -782,6 +798,34 @@ typedef struct _meshtastic_KeyVerification { meshtastic_KeyVerification_hash2_t hash2; } meshtastic_KeyVerification; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t; +typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t; +/* The actual over-the-mesh message doing store and forward++ */ +typedef struct _meshtastic_StoreForwardPlusPlus { + /* Which message type is this */ + meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type; + /* The hash of the specific message */ + meshtastic_StoreForwardPlusPlus_message_hash_t message_hash; + /* The hash of a link on a chain */ + meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash; + /* the root hash of a chain */ + meshtastic_StoreForwardPlusPlus_root_hash_t root_hash; + /* The encrypted bytes from a message */ + meshtastic_StoreForwardPlusPlus_message_t message; + /* Message ID of the contained message */ + uint32_t encapsulated_id; + /* Destination of the contained message */ + uint32_t encapsulated_to; + /* Sender of the contained message */ + uint32_t encapsulated_from; + /* The receive time of the message in question */ + uint32_t encapsulated_rxtime; + /* Used in a LINK_REQUEST to specify the message X spots back from head */ + uint32_t chain_count; +} meshtastic_StoreForwardPlusPlus; + /* Waypoint message, used to share arbitrary locations across the mesh */ typedef struct _meshtastic_Waypoint { /* Id of the waypoint */ @@ -1310,6 +1354,10 @@ extern "C" { #define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED #define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1)) +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF +#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1)) + #define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET #define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX #define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1)) @@ -1338,6 +1386,8 @@ extern "C" { #define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type + #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority @@ -1380,6 +1430,7 @@ extern "C" { #define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} #define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}} +#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} @@ -1411,6 +1462,7 @@ extern "C" { #define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} #define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} #define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}} +#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN} @@ -1489,6 +1541,16 @@ extern "C" { #define meshtastic_KeyVerification_nonce_tag 1 #define meshtastic_KeyVerification_hash1_tag 2 #define meshtastic_KeyVerification_hash2_tag 3 +#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1 +#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2 +#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3 +#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4 +#define meshtastic_StoreForwardPlusPlus_message_tag 5 +#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6 +#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7 +#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8 +#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9 +#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10 #define meshtastic_Waypoint_id_tag 1 #define meshtastic_Waypoint_latitude_i_tag 2 #define meshtastic_Waypoint_longitude_i_tag 3 @@ -1705,6 +1767,20 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3) #define meshtastic_KeyVerification_CALLBACK NULL #define meshtastic_KeyVerification_DEFAULT NULL +#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \ +X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \ +X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \ +X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \ +X(a, STATIC, SINGULAR, BYTES, message, 5) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \ +X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9) \ +X(a, STATIC, SINGULAR, UINT32, chain_count, 10) +#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL +#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL + #define meshtastic_Waypoint_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, id, 1) \ X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ @@ -1980,6 +2056,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg; extern const pb_msgdesc_t meshtastic_Routing_msg; extern const pb_msgdesc_t meshtastic_Data_msg; extern const pb_msgdesc_t meshtastic_KeyVerification_msg; +extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg; @@ -2013,6 +2090,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Data_fields &meshtastic_Data_msg #define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg +#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg #define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg @@ -2069,6 +2147,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_QueueStatus_size 23 #define meshtastic_RouteDiscovery_size 256 #define meshtastic_Routing_size 259 +#define meshtastic_StoreForwardPlusPlus_size 377 #define meshtastic_ToRadio_size 504 #define meshtastic_User_size 115 #define meshtastic_Waypoint_size 165 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 67adc60cc..6b89c6a37 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -86,6 +86,11 @@ typedef enum _meshtastic_PortNum { /* Paxcounter lib included in the firmware ENCODING: protobuf */ meshtastic_PortNum_PAXCOUNTER_APP = 34, + /* Store and Forward++ module included in the firmware + ENCODING: protobuf + This module is specifically for Native Linux nodes, and provides a Git-style + chain of messages. */ + meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35, /* Provides a hardware serial interface to send and receive from the Meshtastic network. Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 7784a473f..74dc17d38 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -417,6 +417,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta } case meshtastic_AdminMessage_enter_dfu_mode_request_tag: { LOG_INFO("Client requesting to enter DFU mode"); +#if HAS_SCREEN + IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0)); +#endif #if defined(ARCH_NRF52) || defined(ARCH_RP2040) enterDfuMode(); #endif diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f435f6060..8d1ba6346 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -7,19 +7,24 @@ #include "Channels.h" #include "FSCommon.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "SPILock.h" #include "buzz.h" #include "detect/ScanI2C.h" +#include "gps/RTC.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" #include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" +#include "input/SerialKeyboard.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control +extern MessageStore messageStore; #if HAS_TRACKBALL #include "input/TrackballInterruptImpl1.h" #endif @@ -40,8 +45,76 @@ // 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; +} extern ScanI2C::DeviceAddress cardkb_found; -extern bool graphics::isMuted; extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; @@ -71,18 +144,16 @@ CannedMessageModule::CannedMessageModule() void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) { - // Use the requested destination, unless it's "broadcast" and we have a previous node/channel - if (newDest == NODENUM_BROADCAST && lastDestSet) { - newDest = lastDest; - newChannel = lastChannel; - } + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() + dest = newDest; channel = newChannel; + lastDest = dest; lastChannel = channel; lastDestSet = true; - // Rest of function unchanged... // Upon activation, highlight "[Select Destination]" int selectDestination = 0; for (int i = 0; i < messagesCount; ++i) { @@ -99,6 +170,8 @@ void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChan UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchWithDestination dest=0x%08x ch=%d", dest, channel); } void CannedMessageModule::LaunchRepeatDestination() @@ -112,13 +185,12 @@ void CannedMessageModule::LaunchRepeatDestination() void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) { - // Use the requested destination, unless it's "broadcast" and we have a previous node/channel - if (newDest == NODENUM_BROADCAST && lastDestSet) { - newDest = lastDest; - newChannel = lastChannel; - } + // Do NOT override explicit broadcast replies + // Only reuse lastDest in LaunchRepeatDestination() + dest = newDest; channel = newChannel; + lastDest = dest; lastChannel = channel; lastDestSet = true; @@ -128,6 +200,8 @@ void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; notifyObservers(&e); + + LOG_DEBUG("[CannedMessage] LaunchFreetextWithDestination dest=0x%08x ch=%d", dest, channel); } static bool returnToCannedList = false; @@ -149,7 +223,7 @@ int CannedMessageModule::splitConfiguredMessages() String canned_messages = cannedMessageModuleConfig.messages; // Copy all message parts into the buffer - strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); + strncpy(this->messageBuffer, canned_messages.c_str(), sizeof(this->messageBuffer)); // Temporary array to allow for insertion const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; @@ -166,16 +240,16 @@ int CannedMessageModule::splitConfiguredMessages() #endif // First message always starts at buffer start - tempMessages[tempCount++] = this->messageStore; - int upTo = strlen(this->messageStore) - 1; + tempMessages[tempCount++] = this->messageBuffer; + int upTo = strlen(this->messageBuffer) - 1; // Walk buffer, splitting on '|' while (i < upTo) { - if (this->messageStore[i] == '|') { - this->messageStore[i] = '\0'; // End previous message + if (this->messageBuffer[i] == '|') { + this->messageBuffer[i] = '\0'; // End previous message if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) break; - tempMessages[tempCount++] = (this->messageStore + i + 1); + tempMessages[tempCount++] = (this->messageBuffer + i + 1); } i += 1; } @@ -193,25 +267,23 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (graphics::isHighResolution) { + if (graphics::currentResolution == graphics::ScreenResolution::High) { if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); + display->drawStringf(x, y, buffer, "To: #%s", channels.getName(this->channel)); } else { - display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); } } else { if (this->dest == NODENUM_BROADCAST) { - display->drawStringf(x, y, buffer, "To: Broadc@%.5s", channels.getName(this->channel)); + display->drawStringf(x, y, buffer, "To: #%.20s", channels.getName(this->channel)); } else { - display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + display->drawStringf(x, y, buffer, "To: @%s", getNodeName(this->dest)); } } } void CannedMessageModule::resetSearch() { - LOG_INFO("Resetting search, restoring full destination list"); - int previousDestIndex = destIndex; searchQuery = ""; @@ -273,6 +345,10 @@ void CannedMessageModule::updateDestinationSelectionList() } } + meshtastic_MeshPacket *p = allocDataPacket(); + p->pki_encrypted = true; + p->channel = 0; + // Populate active channels std::vector seenChannels; seenChannels.reserve(channels.getNumChannels()); @@ -284,15 +360,6 @@ void CannedMessageModule::updateDestinationSelectionList() } } - /* As the nodeDB is sorted, can skip this step - // Sort by favorite, then last heard - std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { - if (a.node->is_favorite != b.node->is_favorite) - return a.node->is_favorite > b.node->is_favorite; - return a.lastHeard < b.lastHeard; - }); - */ - scrollIndex = 0; // Show first result at the top destIndex = 0; // Highlight the first entry if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { @@ -360,16 +427,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) return handleEmotePickerInput(event); case CANNED_MESSAGE_RUN_STATE_INACTIVE: - if (isSelect) { - return 0; // Main button press no longer runs through powerFSM - } - // Let LEFT/RIGHT pass through so frame navigation works - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) { - break; - } - // Handle UP/DOWN: activate canned message list! - if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN || - event->inputEvent == INPUT_BROKER_ALT_LONG) { + if (event->inputEvent == INPUT_BROKER_ALT_LONG) { LaunchWithDestination(NODENUM_BROADCAST); return 1; } @@ -383,6 +441,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) // Immediately process the input in the new state (freetext) return handleFreeTextInput(event); } + return 0; break; // (Other states can be added here as needed) @@ -573,7 +632,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) return false; - // === Handle Cancel key: go inactive, clear UI state === + // 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; @@ -602,7 +661,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo } else if (isSelect) { const char *current = messages[currentMessageIndex]; - // === [Select Destination] triggers destination selection UI === + // [Select Destination] triggers destination selection UI if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; @@ -613,7 +672,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo return true; } - // === [Exit] returns to the main/inactive screen === + // [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; @@ -627,7 +686,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo return true; } - // === [Free Text] triggers the free text input (virtual keyboard) === + // [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; @@ -642,9 +701,9 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (osk_found && screen) { char headerBuffer[64]; if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); + snprintf(headerBuffer, sizeof(headerBuffer), "To: #%s", channels.getName(this->channel)); } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); + snprintf(headerBuffer, sizeof(headerBuffer), "To: @%s", getNodeName(this->dest)); } screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { if (!text.empty()) { @@ -693,20 +752,12 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { } else { #if CANNED_MESSAGE_ADD_CONFIRMATION - // Show confirmation dialog before sending canned message - NodeNum destNode = dest; - ChannelIndex chan = channel; - graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() { - this->sendText(destNode, chan, current, false); - payload = runState; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - currentMessageIndex = -1; - - // Notify UI to regenerate frame set and redraw - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); + const int savedIndex = currentMessageIndex; + graphics::menuHandler::showConfirmationBanner("Send message?", [this, savedIndex]() { + this->currentMessageIndex = savedIndex; + this->payload = this->runState; + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + this->setIntervalFromNow(0); }); #else payload = runState; @@ -805,7 +856,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) } #endif // USE_VIRTUAL_KEYBOARD - // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + // 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; @@ -836,6 +887,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) if (event->inputEvent == INPUT_BROKER_BACK && this->freetext.length() > 0) { payload = 0x08; lastTouchMillis = millis(); + requestFocus(); runOnce(); return true; } @@ -844,6 +896,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) if (event->inputEvent == INPUT_BROKER_LEFT) { payload = INPUT_BROKER_LEFT; lastTouchMillis = millis(); + requestFocus(); runOnce(); return true; } @@ -851,6 +904,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) if (event->inputEvent == INPUT_BROKER_RIGHT) { payload = INPUT_BROKER_RIGHT; lastTouchMillis = millis(); + requestFocus(); runOnce(); return true; } @@ -946,57 +1000,110 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha lastDest = dest; lastChannel = channel; lastDestSet = true; - // === Prepare packet === + meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; + p->decoded.dest = dest; // Mirror picker: NODENUM_BROADCAST or node->num - // Save destination for ACK/NACK UI fallback this->lastSentNode = dest; this->incoming = dest; - // Copy message payload + // Manually find the node by number to check PKI capability + meshtastic_NodeInfoLite *node = nullptr; + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num == dest) { + node = n; + break; + } + } + + NodeNum myNodeNum = nodeDB->getNodeNum(); + if (node && node->num != myNodeNum && node->has_user && node->user.public_key.size == 32) { + p->pki_encrypted = true; + p->channel = 0; // force PKI + } + + // Track this packet’s request ID for matching ACKs + this->lastRequestId = p->id; + + // Copy payload p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - // Optionally add bell character if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell - p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; } - // Mark as waiting for ACK to trigger ACK/NACK screen this->waitingForAck = true; - // Log outgoing message - LOG_INFO("Send message id=%u, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - - if (p->to != 0xffffffff) { - // Only add as favorite if our role is NOT CLIENT_BASE - if (config.device.role != 12) { - LOG_INFO("Proactively adding %x as favorite node", p->to); - nodeDB->set_favorite(true, p->to); - } else { - LOG_DEBUG("Not favoriting node %x as we are CLIENT_BASE role", p->to); - } - - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - p->pki_encrypted = true; - p->channel = 0; - } - - // Send to mesh and phone (even if no phone connected, to track ACKs) + // Send to mesh (PKI-encrypted if conditions above matched) service->sendToMesh(p, RX_SRC_LOCAL, true); - // === Simulate local message to clear unread UI === + // Show banner immediately if (screen) { - meshtastic_MeshPacket simulatedPacket = {}; - simulatedPacket.from = 0; // Local device - screen->handleTextMessage(&simulatedPacket); + graphics::BannerOverlayOptions opts; + opts.message = "Sending..."; + opts.durationMs = 2000; + screen->showOverlayBanner(opts); } + + // Save outgoing message + StoredMessage sm; + + // Always use our local time, consistent with other paths + uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); + sm.timestamp = (nowSecs > 0) ? nowSecs : millis() / 1000; + sm.isBootRelative = (nowSecs == 0); + + sm.sender = nodeDB->getNodeNum(); // us + sm.channelIndex = channel; + size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); + sm.textOffset = MessageStore::storeText(message, len); + sm.textLength = len; + + // Classify broadcast vs DM + if (dest == NODENUM_BROADCAST) { + sm.dest = NODENUM_BROADCAST; + sm.type = MessageType::BROADCAST; + } 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) { + 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); + } + } + sm.ackStatus = AckStatus::NONE; + + messageStore.addLiveMessage(std::move(sm)); + + // Auto-switch thread view on outgoing message + if (sm.type == MessageType::BROADCAST) { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, sm.channelIndex); + } else { + graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, sm.dest); + } + playComboTune(); + + this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + this->payload = wantReplies ? 1 : 0; + requestFocus(); + + // Tell Screen to switch to TextMessage frame via UIFrameEvent + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + notifyObservers(&e); } + int32_t CannedMessageModule::runOnce() { if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { @@ -1014,11 +1121,9 @@ int32_t CannedMessageModule::runOnce() // Clean up virtual keyboard if needed when going inactive if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { LOG_INFO("Performing delayed virtual keyboard cleanup"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); } - temporaryMessage = ""; return INT32_MAX; } @@ -1032,9 +1137,7 @@ int32_t CannedMessageModule::runOnce() // Clean up virtual keyboard after sending if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard after message send"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } @@ -1062,7 +1165,6 @@ int32_t CannedMessageModule::runOnce() (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; @@ -1071,15 +1173,11 @@ 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 happens after virtual keyboard message sending is complete - LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - this->notifyObservers(&e); + return INT32_MAX; } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -1092,9 +1190,7 @@ int32_t CannedMessageModule::runOnce() // Clean up virtual keyboard if it exists during timeout if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } @@ -1107,7 +1203,21 @@ int32_t CannedMessageModule::runOnce() } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + + // Clean up state but *don’t* deactivate yet + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } @@ -1121,37 +1231,59 @@ int32_t CannedMessageModule::runOnce() return INT32_MAX; } else { sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); + + // Clean up state + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Tell Screen to jump straight to the TextMessage frame + UIFrameEvent e; + e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; + this->notifyObservers(&e); + + // Now deactivate this module + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + return INT32_MAX; // don’t fall back into canned list } - this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } + // fallback clean-up if nothing above returned this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); - return 2000; + + // Immediately stop, don’t linger on canned screen + return INT32_MAX; } // Highlight [Select Destination] initially when entering the message list else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - int selectDestination = 0; - for (int i = 0; i < this->messagesCount; ++i) { - if (strcmp(this->messages[i], "[Select Destination]") == 0) { - selectDestination = i; - break; + // Only auto-highlight [Select Destination] if we’re ACTIVELY browsing, + // not when coming back from a sent message. + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + int selectDestination = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") == 0) { + selectDestination = i; + break; + } } + this->currentMessageIndex = selectDestination; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; } - this->currentMessageIndex = selectDestination; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { @@ -1159,7 +1291,6 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { @@ -1209,7 +1340,7 @@ int32_t CannedMessageModule::runOnce() this->freetext.substring(this->cursor); } this->cursor++; - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; this->freetext = this->freetext.substring(0, maxChars); @@ -1265,7 +1396,10 @@ const char *CannedMessageModule::getNodeName(NodeNum node) bool CannedMessageModule::shouldDraw() { - return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE); + // Only allow drawing when we're in an interactive UI state. + return (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || + this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER); } // Has the user defined any canned messages? @@ -1292,16 +1426,6 @@ int CannedMessageModule::getPrevIndex() return this->currentMessageIndex - 1; } } -void CannedMessageModule::showTemporaryMessage(const String &message) -{ - temporaryMessage = message; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - notifyObservers(&e); - runState = CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION; - // run this loop again in 2 seconds, next iteration will clear the display - setIntervalFromNow(2000); -} #if defined(USE_VIRTUAL_KEYBOARD) @@ -1524,7 +1648,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // === Header === + // Header int titleY = 2; String titleText = "Select Destination"; titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; @@ -1532,7 +1656,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O display->drawString(display->getWidth() / 2, titleY, titleText); display->setTextAlignment(TEXT_ALIGN_LEFT); - // === List Items === + // List Items int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); int numActiveChannels = this->activeChannelIndices.size(); int totalEntries = numActiveChannels + this->filteredNodes.size(); @@ -1541,7 +1665,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (this->visibleRows < 1) this->visibleRows = 1; - // === Clamp scrolling === + // Clamp scrolling if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; if (scrollIndex < 0) @@ -1559,26 +1683,44 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O // Draw Channels First if (itemIndex < numActiveChannels) { uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + snprintf(entryText, sizeof(entryText), "#%s", channels.getName(channelIndex)); } // 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'; + } + 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, "..."); + } + + // 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 (node->is_favorite) { -#if defined(M5STACK_UNITC6L) - snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name); - } else { + if (display->getWidth() <= 64) { snprintf(entryText, sizeof(entryText), "%s", node->user.short_name); } -#else - snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); - } else { - snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); - } -#endif } } } @@ -1586,18 +1728,19 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) strcpy(entryText, "?"); - // === Highlight background (if selected) === + // Highlight background (if selected) if (itemIndex == destIndex) { int scrollPadding = 8; // Reserve space for scrollbar display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); display->setColor(BLACK); } - // === Draw entry text === + // Draw entry text display->drawString(xOffset + 2, yOffset, entryText); display->setColor(WHITE); - // === Draw key icon (after highlight) === + // Draw key icon (after highlight) + /* if (itemIndex >= numActiveChannels) { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { @@ -1615,6 +1758,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O } } } + */ } // Scrollbar @@ -1651,6 +1795,9 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; int numEmotes = graphics::numEmotes; + // keep member variable in sync + this->visibleRows = _visibleRows; + // Clamp highlight index if (emotePickerIndex < 0) emotePickerIndex = 0; @@ -1672,7 +1819,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla // Draw emote rows display->setTextAlignment(TEXT_ALIGN_LEFT); - for (int vis = 0; vis < visibleRows; ++vis) { + for (int vis = 0; vis < _visibleRows; ++vis) { int emoteIdx = topIndex + vis; if (emoteIdx >= numEmotes) break; @@ -1699,11 +1846,11 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla } // Draw scrollbar if needed - if (numEmotes > visibleRows) { - int scrollbarHeight = visibleRows * rowHeight; + if (numEmotes > _visibleRows) { + int scrollbarHeight = _visibleRows * rowHeight; int scrollTrackX = display->getWidth() - 6; display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); - int scrollBarLen = std::max(6, (scrollbarHeight * visibleRows) / numEmotes); + int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); } @@ -1716,104 +1863,25 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // === Draw temporary message if available === - if (temporaryMessage.length() != 0) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame - LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str()); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage); - return; + // Never draw if state is outside our UI modes + if (!(runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER)) { + return; // bail if not in a UI state that should render } - // === Emote Picker Screen === + // Emote Picker Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here return; } - // === Destination Selection === + // Destination Selection if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { drawDestinationSelectionScreen(display, state, x, y); return; } - // === ACK/NACK Screen === - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { - requestFocus(); - EINK_ADD_FRAMEFLAG(display, COSMETIC); - display->setTextAlignment(TEXT_ALIGN_CENTER); - -#ifdef USE_EINK - display->setFont(FONT_SMALL); - int yOffset = y + 10; -#else - display->setFont(FONT_MEDIUM); -#if defined(M5STACK_UNITC6L) - int yOffset = y; -#else - int yOffset = y + 10; -#endif -#endif - - // --- Delivery Status Message --- - if (this->ack) { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel)); - } else if (this->lastAckHopLimit > this->lastAckHopStart) { - snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", this->lastAckHopLimit - this->lastAckHopStart, - getNodeName(this->incoming)); - } else { - snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming)); - } - } else { - snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); - } - - // Draw delivery message and compute y-offset after text height - int lineCount = 1; - for (const char *ptr = buffer; *ptr; ptr++) { - if (*ptr == '\n') - lineCount++; - } - - display->drawString(display->getWidth() / 2 + x, yOffset, buffer); -#if defined(M5STACK_UNITC6L) - yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding -#else - yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding -#endif -#ifndef USE_EINK - // --- SNR + RSSI Compact Line --- - if (this->ack) { - display->setFont(FONT_SMALL); -#if defined(M5STACK_UNITC6L) - snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi); -#else - snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); -#endif - display->drawString(display->getWidth() / 2 + x, yOffset, buffer); - } -#endif - - return; - } - - // === Sending Screen === - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - EINK_ADD_FRAMEFLAG(display, COSMETIC); - requestFocus(); -#ifdef USE_EINK - display->setFont(FONT_SMALL); -#else - display->setFont(FONT_MEDIUM); -#endif - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); - return; - } - - // === Disabled Screen === + // Disabled Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -1821,7 +1889,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } - // === Free Text Input Screen === + // Free Text Input Screen if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) @@ -1834,10 +1902,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // --- Draw node/channel header at the top --- + // Draw node/channel header at the top drawHeader(display, x, y, buffer); - // --- Char count right-aligned --- + // Char count right-aligned if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); @@ -1845,58 +1913,98 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } - // --- Draw Free Text input with multi-emote support and proper line wrapping --- +#if INPUTBROKER_SERIAL_TYPE == 1 + // Chatter Modifier key mode label (right side) + { + uint8_t mode = globalSerialKeyboard ? globalSerialKeyboard->getShift() : 0; + const char *label = (mode == 0) ? "a" : (mode == 1) ? "A" : "#"; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int16_t th = FONT_HEIGHT_SMALL; + const int16_t tw = display->getStringWidth(label); + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = tw + padX * 2; + const int16_t bh = th + padY * 2; + + const int16_t bx = x + display->getWidth() - bw - 2; + const int16_t by = y + display->getHeight() - bh - 2; + + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - r * 2, bh); + display->fillRect(bx, by + r, r, bh - r * 2); + display->fillRect(bx + bw - r, by + r, r, bh - r * 2); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + } + + // LEFT-SIDE DESTINATION-HINT BOX (“Dest: Shift + â—„â€�) + { + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char *label = "Dest: Shift + "; + int16_t labelW = display->getStringWidth(label); + + // triangle size visually matches glyph height, not full line height + const int triH = FONT_HEIGHT_SMALL - 3; + const int triW = triH * 0.7; + + const int16_t padX = 3; + const int16_t padY = 2; + const int16_t r = 3; + + const int16_t bw = labelW + triW + padX * 2 + 2; + const int16_t bh = FONT_HEIGHT_SMALL + padY * 2; + + const int16_t bx = x + 2; + const int16_t by = y + display->getHeight() - bh - 2; + + // Rounded white box + display->setColor(WHITE); + display->fillRect(bx + r, by, bw - (r * 2), bh); + display->fillRect(bx, by + r, r, bh - (r * 2)); + display->fillRect(bx + bw - r, by + r, r, bh - (r * 2)); + display->fillCircle(bx + r, by + r, r); + display->fillCircle(bx + bw - r - 1, by + r, r); + display->fillCircle(bx + r, by + bh - r - 1, r); + display->fillCircle(bx + bw - r - 1, by + bh - r - 1, r); + + // Draw text + display->setColor(BLACK); + display->drawString(bx + padX, by + padY, label); + + // Perfectly center triangle on text baseline + int16_t tx = bx + padX + labelW; + int16_t ty = by + padY + (FONT_HEIGHT_SMALL / 2) - (triH / 2) - 1; // -1 for optical centering + + // â—„ Left-pointing triangle + display->fillTriangle(tx + triW, ty, // top-right + tx, ty + triH / 2, // left center + tx + triW, ty + triH // bottom-right + ); + } +#endif + // Draw Free Text input with multi-emote support and proper line wrapping display->setColor(WHITE); { int inputY = 0 + y + FONT_HEIGHT_SMALL; String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); // Tokenize input into (isEmote, token) pairs - std::vector> tokens; const char *msg = msgWithCursor.c_str(); - 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; - } - } - } + std::vector> tokens = tokenizeMessageWithEmotes(msg); - // ===== Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) ===== + // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed) std::vector>> lines; std::vector> currentLine; int lineWidth = 0; @@ -1921,7 +2029,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } else { // Text: split by words and wrap inside word if needed String text = token.second; - pos = 0; + int pos = 0; while (pos < static_cast(text.length())) { // Find next space (or end) int spacePos = text.indexOf(' ', pos); @@ -1967,18 +2075,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int nextX = x; for (const auto &token : line) { if (token.first) { - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; - } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; - display->drawXbm(nextX, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, yLine, rowHeight, token.second); } else { display->drawString(nextX, yLine, token.second); nextX += display->getStringWidth(token.second); @@ -1991,12 +2089,12 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } - // === Canned Messages List === + // Canned Messages List if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // ====== Precompute per-row heights based on emotes (centered if present) ====== + // Precompute per-row heights based on emotes (centered if present) const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; @@ -2016,7 +2114,7 @@ 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 --- + // 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; @@ -2034,7 +2132,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); } - // --- Draw all message rows with multi-emote support --- + // Draw all message rows with multi-emote support int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; @@ -2043,52 +2141,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int rowHeight = rowHeights[vis]; bool _highlight = (msgIdx == currentMessageIndex); - // --- Multi-emote tokenization --- - std::vector> tokens; // (isEmote, token) - int pos = 0; - int msgLen = strlen(msg); - while (pos < msgLen) { - const graphics::Emote *foundEmote = nullptr; - int foundLen = 0; - - // Look for any emote label at this pos (prefer longest match) - 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[0] == 0) - 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; - } - } - } - // --- End multi-emote tokenization --- + // Multi-emote tokenization + std::vector> tokens = tokenizeMessageWithEmotes(msg); // Vertically center based on rowHeight int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -2109,19 +2163,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Draw all tokens left to right for (const auto &token : tokens) { if (token.first) { - // Emote - const graphics::Emote *emote = nullptr; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - emote = &graphics::emotes[j]; - break; - } - } - if (emote) { - int emoteYOffset = (rowHeight - emote->height) / 2; - display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); - nextX += emote->width + 2; - } + // Emote rendering centralized in helper + renderEmote(display, nextX, lineY, rowHeight, token.second); } else { // Text display->drawString(nextX, lineY + textYOffset, token.second); @@ -2148,43 +2191,166 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } +// Return SNR limit based on modem preset +static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset) +{ + switch (preset) { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE: + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + return -6.0f; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + return -5.5f; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: + return -4.5f; + default: + return -6.0f; + } +} + +// Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI +static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars) +{ + // 5-bar logic: strength inside Good/Fair/Bad category + if (snr > snrLimit && rssi > -10) { + bars = 5; // very strong good + return "Good"; + } else if (snr > snrLimit && rssi > -20) { + bars = 4; // normal good + return "Good"; + } else if (snr > 0 && rssi > -50) { + bars = 3; // weaker good (on edge of fair) + return "Good"; + } else if (snr > -10 && rssi > -100) { + bars = 2; // fair + return "Fair"; + } else { + bars = 1; // bad + return "Bad"; + } +} + ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { - if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { + // Only process routing ACK/NACK packets that are responses to our own outbound + if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck && mp.to == nodeDB->getNodeNum() && + mp.decoded.request_id == this->lastRequestId) // only ACKs for our last sent packet + { if (mp.decoded.request_id != 0) { - // Trigger screen refresh for ACK/NACK feedback - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - requestFocus(); - this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - // Decode the routing response meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - // Track hop metadata - this->lastAckWasRelayed = (mp.hop_limit != mp.hop_start); - this->lastAckHopStart = mp.hop_start; - this->lastAckHopLimit = mp.hop_limit; - - // Determine ACK status + // Determine ACK/NACK status bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); bool isFromDest = (mp.from == this->lastSentNode); bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); // Identify the responding node if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { - this->incoming = mp.from; // Relayed by another node + this->incoming = mp.from; // relayed by another node } else { - this->incoming = this->lastSentNode; // Direct reply + this->incoming = this->lastSentNode; // direct reply } - // Final ACK confirmation logic - this->ack = isAck && (wasBroadcast || isFromDest); + // Final ACK/NACK logic + if (wasBroadcast) { + // Any ACK counts for broadcast + this->ack = isAck; + waitingForAck = false; + } else if (isFromDest) { + // Only ACK from destination counts as final + this->ack = isAck; + waitingForAck = false; + } else if (isAck) { + // Relay ACK → mark as RELAYED, still no final ACK + this->ack = false; + waitingForAck = false; + } else { + // Explicit failure + this->ack = false; + waitingForAck = false; + } - waitingForAck = false; - this->notifyObservers(&e); - setIntervalFromNow(3000); // Time to show ACK/NACK screen + // Update last sent StoredMessage with ACK/NACK/RELAYED result + if (!messageStore.getMessages().empty()) { + StoredMessage &last = const_cast(messageStore.getMessages().back()); + if (last.sender == nodeDB->getNodeNum()) { // only update our own messages + if (wasBroadcast && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (isFromDest && isAck) { + last.ackStatus = AckStatus::ACKED; + } else if (!isFromDest && isAck) { + last.ackStatus = AckStatus::RELAYED; + } else { + last.ackStatus = AckStatus::NACKED; + } + } + } + + // Capture radio metrics + this->lastRxRssi = mp.rx_rssi; + this->lastRxSnr = mp.rx_snr; + + // Show overlay banner + if (screen) { + auto *display = screen->getDisplayDevice(); + graphics::BannerOverlayOptions opts; + static char buf[128]; + + const char *channelName = channels.getName(this->channel); + const char *src = getNodeName(this->incoming); + char nodeName[48]; + strncpy(nodeName, src, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + + int availWidth = + display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); + if (availWidth < 0) + availWidth = 0; + + size_t origLen = strlen(nodeName); + while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { + nodeName[strlen(nodeName) - 1] = '\0'; + } + if (strlen(nodeName) < origLen) { + strcat(nodeName, "..."); + } + + // Calculate signal quality and bars based on preset, SNR, and RSSI + float snrLimit = getSnrLimit(config.lora.modem_preset); + int bars = 0; + const char *qualityLabel = getSignalGrade(this->lastRxSnr, this->lastRxRssi, snrLimit, bars); + + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message sent to\n#%s\n\nSignal: %s", + (channelName && channelName[0]) ? channelName : "unknown", qualityLabel); + } else { + snprintf(buf, sizeof(buf), "DM sent to\n@%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } + } else if (isAck && !isFromDest) { + // Relay ACK banner + snprintf(buf, sizeof(buf), "DM Relayed\n(Status Unknown)\n%s\n\nSignal: %s", + (nodeName && nodeName[0]) ? nodeName : "unknown", qualityLabel); + } else { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buf, sizeof(buf), "Message failed to\n#%s", + (channelName && channelName[0]) ? channelName : "unknown"); + } else { + snprintf(buf, sizeof(buf), "DM failed to\n@%s", (nodeName && nodeName[0]) ? nodeName : "unknown"); + } + } + + opts.message = buf; + opts.durationMs = 3000; + graphics::bannerSignalBars = bars; // tell banner renderer how many bars to draw + screen->showOverlayBanner(opts); // this triggers drawNotificationBox() + } } } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 5b0481ac7..3d7c09d87 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -75,7 +75,6 @@ class CannedMessageModule : public SinglePortModule, public ObservableisPlaying()) - audioThread->stop(); + audioThread->stop(); #endif // Turn off all outputs LOG_INFO("Turning off setExternalStates"); @@ -320,7 +319,7 @@ void ExternalNotificationModule::stopNow() externalTurnedOn[i] = 0; } setIntervalFromNow(0); -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) drv.stop(); #endif @@ -542,6 +541,19 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP (!isBroadcast(mp.to) && isToUs(&mp))) { // Buzz if buzzer mode is not in DIRECT_MSG_ONLY or is DM to us isNagging = true; +#ifdef T_LORA_PAGER + if (canBuzz()) { + 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_pwm && !moduleConfig.external_notification.use_i2s_as_buzzer) { setExternalState(2, true); } else { diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 827524fc3..63392f7e4 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -181,25 +181,25 @@ void setupModules() // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { -#ifndef T_LORA_PAGER - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } -#elif defined(T_LORA_PAGER) +#if defined(T_LORA_PAGER) // use a special FSM based rotary encoder version for T-LoRa Pager rotaryEncoderImpl = new RotaryEncoderImpl(); if (!rotaryEncoderImpl->init()) { delete rotaryEncoderImpl; rotaryEncoderImpl = nullptr; } -#else +#elif defined(INPUTDRIVER_ENCODER_TYPE) && (INPUTDRIVER_ENCODER_TYPE == 2) upDownInterruptImpl1 = new UpDownInterruptImpl1(); if (!upDownInterruptImpl1->init()) { delete upDownInterruptImpl1; upDownInterruptImpl1 = nullptr; } +#else + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } #endif cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); @@ -217,7 +217,7 @@ void setupModules() } #endif // HAS_BUTTON #if ARCH_PORTDUINO - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") { seesawRotary = new SeesawRotary("SeesawRotary"); if (!seesawRotary->init()) { delete seesawRotary; diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index 936a7b44a..2cd8ec5ed 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -170,7 +170,7 @@ bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, } else { LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)"); } - } else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) { + } else if (getHopsAway(mp) == 0) { LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr); // If the hopLimit is the same as hopStart, then it is a neighbor getOrCreateNeighbor(mp.from, mp.from, 0, diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index aaab019d6..7db8b66cc 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -7,17 +7,41 @@ #include "configuration.h" #include "main.h" #include +#include + +#ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS +#define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60) +#endif NodeInfoModule *nodeInfoModule; +static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS; + bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr) { + suppressReplyForCurrentRequest = false; + if (mp.from == nodeDB->getNodeNum()) { LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from); return false; } auto p = *pptr; + + if (mp.decoded.want_response) { + const NodeNum sender = getFrom(&mp); + const uint32_t now = mp.rx_time ? mp.rx_time : getTime(); + auto it = lastNodeInfoSeen.find(sender); + if (it != lastNodeInfoSeen.end()) { + uint32_t sinceLast = now >= it->second ? now - it->second : 0; + if (sinceLast < NodeInfoReplySuppressSeconds) { + suppressReplyForCurrentRequest = true; + } + } + lastNodeInfoSeen[sender] = now; + pruneLastNodeInfoCache(); + } + if (p.is_licensed != owner.is_licensed) { LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!"); return true; @@ -42,6 +66,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes service->sendToPhone(packetCopy); } + pruneLastNodeInfoCache(); + // LOG_DEBUG("did handleReceived"); return false; // Let others look at this message also if they want } @@ -68,9 +94,11 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha if (p) { // Check whether we didn't ignore it p->to = dest; - p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && + bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && wantReplies; + + p->decoded.want_response = requestWantResponse; if (_shorterTimeout) p->priority = meshtastic_MeshPacket_Priority_DEFAULT; else @@ -89,6 +117,13 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha meshtastic_MeshPacket *NodeInfoModule::allocReply() { + if (suppressReplyForCurrentRequest) { + LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago"); + ignoreRequest = true; + suppressReplyForCurrentRequest = false; + return NULL; + } + if (!airTime->isTxAllowedChannelUtil(false)) { ignoreRequest = true; // Mark it as ignored for MeshModule LOG_DEBUG("Skip send NodeInfo > 40%% ch. util"); @@ -125,6 +160,29 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() } } +void NodeInfoModule::pruneLastNodeInfoCache() +{ + if (!nodeDB || !nodeDB->meshNodes) + return; + + const size_t maxEntries = nodeDB->meshNodes->size(); + + for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) { + if (!nodeDB->getMeshNode(it->first)) { + it = lastNodeInfoSeen.erase(it); + } else { + ++it; + } + } + + while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) { + auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(), + [](const std::pair &lhs, + const std::pair &rhs) { return lhs.second < rhs.second; }); + lastNodeInfoSeen.erase(oldestIt); + } +} + NodeInfoModule::NodeInfoModule() : ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo") { diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index 572b81700..d16fbeac2 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -1,5 +1,6 @@ #pragma once #include "ProtobufModule.h" +#include /** * NodeInfo module for sending/receiving NodeInfos into the mesh @@ -43,6 +44,10 @@ class NodeInfoModule : public ProtobufModule, private concurren private: uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh bool shorterTimeout = false; + bool suppressReplyForCurrentRequest = false; + std::map lastNodeInfoSeen; + + void pruneLastNodeInfoCache(); }; extern NodeInfoModule *nodeInfoModule; diff --git a/src/modules/OnScreenKeyboardModule.cpp b/src/modules/OnScreenKeyboardModule.cpp new file mode 100644 index 000000000..e75d926bf --- /dev/null +++ b/src/modules/OnScreenKeyboardModule.cpp @@ -0,0 +1,272 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/SharedUIDisplay.h" +#include "graphics/draw/NotificationRenderer.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" +#include "modules/OnScreenKeyboardModule.h" +#include +#include + +namespace graphics +{ + +OnScreenKeyboardModule &OnScreenKeyboardModule::instance() +{ + static OnScreenKeyboardModule inst; + return inst; +} + +OnScreenKeyboardModule::~OnScreenKeyboardModule() +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } +} + +void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t durationMs, + std::function cb) +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + keyboard = new VirtualKeyboard(); + callback = cb; + if (header) + keyboard->setHeader(header); + if (initialText) + keyboard->setInputText(initialText); + + // Route VK submission/cancel events back into the module + keyboard->setCallback([this](const std::string &text) { + if (text.empty()) { + this->onCancel(); + } else { + this->onSubmit(text); + } + }); + + // Maintain legacy compatibility hooks + NotificationRenderer::virtualKeyboard = keyboard; + NotificationRenderer::textInputCallback = callback; +} + +void OnScreenKeyboardModule::stop(bool callEmptyCallback) +{ + auto cb = callback; + callback = nullptr; + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + // Keep NotificationRenderer legacy pointers in sync + NotificationRenderer::virtualKeyboard = nullptr; + NotificationRenderer::textInputCallback = nullptr; + clearPopup(); + if (callEmptyCallback && cb) + cb(""); +} + +void OnScreenKeyboardModule::handleInput(const InputEvent &event) +{ + if (!keyboard) + return; + + if (processVirtualKeyboardInput(event, keyboard)) + return; + + if (event.inputEvent == INPUT_BROKER_CANCEL) + onCancel(); +} + +bool OnScreenKeyboardModule::processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *targetKeyboard) +{ + if (!targetKeyboard) + return false; + + switch (event.inputEvent) { + case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: + targetKeyboard->moveCursorUp(); + return true; + case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: + targetKeyboard->moveCursorDown(); + return true; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_ALT_PRESS: + targetKeyboard->moveCursorLeft(); + return true; + case INPUT_BROKER_RIGHT: + case INPUT_BROKER_USER_PRESS: + targetKeyboard->moveCursorRight(); + return true; + case INPUT_BROKER_SELECT: + targetKeyboard->handlePress(); + return true; + case INPUT_BROKER_SELECT_LONG: + targetKeyboard->handleLongPress(); + return true; + default: + return false; + } +} + +bool OnScreenKeyboardModule::draw(OLEDDisplay *display) +{ + if (!keyboard) + return false; + + // Timeout + if (keyboard->isTimedOut()) { + onCancel(); + return false; + } + + // Clear full screen behind keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + keyboard->draw(display, 0, 0); + + // Draw popup overlay if needed + drawPopup(display); + return true; +} + +void OnScreenKeyboardModule::onSubmit(const std::string &text) +{ + auto cb = callback; + stop(false); + if (cb) + cb(text); +} + +void OnScreenKeyboardModule::onCancel() +{ + stop(true); +} + +void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) +{ + if (!title || !content) + return; + strncpy(popupTitle, title, sizeof(popupTitle) - 1); + popupTitle[sizeof(popupTitle) - 1] = '\0'; + strncpy(popupMessage, content, sizeof(popupMessage) - 1); + popupMessage[sizeof(popupMessage) - 1] = '\0'; + popupUntil = millis() + durationMs; + popupVisible = true; +} + +void OnScreenKeyboardModule::clearPopup() +{ + popupTitle[0] = '\0'; + popupMessage[0] = '\0'; + popupUntil = 0; + popupVisible = false; +} + +void OnScreenKeyboardModule::drawPopupOverlay(OLEDDisplay *display) +{ + // Only render the popup overlay (without drawing the keyboard) + drawPopup(display); +} + +void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) +{ + if (!popupVisible) + return; + if (millis() > popupUntil || popupMessage[0] == '\0') { + popupVisible = false; + return; + } + + // Build lines and leverage NotificationRenderer inverted box drawing for consistent style + constexpr uint16_t maxContentLines = 3; + const bool hasTitle = popupTitle[0] != '\0'; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const uint16_t maxWrapWidth = display->width() - 40; + + auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { + std::vector wrapped; + std::string current; + std::string word; + const char *p = text; + while (*p && wrapped.size() < maxContentLines) { + while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { + if (*p == '\n') { + if (!current.empty()) { + wrapped.push_back(current); + current.clear(); + if (wrapped.size() >= maxContentLines) + break; + } + } + ++p; + } + if (!*p || wrapped.size() >= maxContentLines) + break; + word.clear(); + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') + word += *p++; + if (word.empty()) + continue; + std::string test = current.empty() ? word : (current + " " + word); + uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); + if (w <= availableWidth) + current = test; + else { + if (!current.empty()) { + wrapped.push_back(current); + current = word; + if (wrapped.size() >= maxContentLines) + break; + } else { + current = word; + while (current.size() > 1 && + display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) + current.pop_back(); + } + } + } + if (!current.empty() && wrapped.size() < maxContentLines) + wrapped.push_back(current); + return wrapped; + }; + + std::vector allLines; + if (hasTitle) + allLines.emplace_back(popupTitle); + + char buf[sizeof(popupMessage)]; + strncpy(buf, popupMessage, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + char *paragraph = strtok(buf, "\n"); + while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { + auto wrapped = wrapText(paragraph, maxWrapWidth); + for (const auto &ln : wrapped) { + if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) + break; + allLines.push_back(ln); + } + paragraph = strtok(nullptr, "\n"); + } + + std::vector ptrs; + for (const auto &ln : allLines) + ptrs.push_back(ln.c_str()); + ptrs.push_back(nullptr); + + // Use the standard notification box drawing from NotificationRenderer + NotificationRenderer::drawNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); +} + +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/modules/OnScreenKeyboardModule.h b/src/modules/OnScreenKeyboardModule.h new file mode 100644 index 000000000..f86b71ec3 --- /dev/null +++ b/src/modules/OnScreenKeyboardModule.h @@ -0,0 +1,55 @@ +#pragma once + +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/Screen.h" // InputEvent +#include "graphics/VirtualKeyboard.h" +#include +#include +#include + +namespace graphics +{ +class OnScreenKeyboardModule +{ + public: + static OnScreenKeyboardModule &instance(); + + void start(const char *header, const char *initialText, uint32_t durationMs, + std::function callback); + + void stop(bool callEmptyCallback); + + void handleInput(const InputEvent &event); + static bool processVirtualKeyboardInput(const InputEvent &event, VirtualKeyboard *keyboard); + bool draw(OLEDDisplay *display); + + void showPopup(const char *title, const char *content, uint32_t durationMs); + void clearPopup(); + // Draw only the popup overlay (used when legacy virtualKeyboard draws the keyboard) + void drawPopupOverlay(OLEDDisplay *display); + + private: + OnScreenKeyboardModule() = default; + ~OnScreenKeyboardModule(); + OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; + OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; + + void onSubmit(const std::string &text); + void onCancel(); + + void drawPopup(OLEDDisplay *display); + + VirtualKeyboard *keyboard = nullptr; + std::function callback; + + char popupTitle[64] = {0}; + char popupMessage[256] = {0}; + uint32_t popupUntil = 0; + bool popupVisible = false; +}; + +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 8b6a9f19c..f7116e701 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -45,8 +45,12 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes { auto p = *pptr; - // If inbound message is a replay (or spoof!) of our own messages, we shouldn't process - // (why use second-hand sources for our own data?) + const auto transport = mp.transport_mechanism; + if (isFromUs(&mp) && !IS_ONE_OF(transport, meshtastic_MeshPacket_TransportMechanism_TRANSPORT_INTERNAL, + meshtastic_MeshPacket_TransportMechanism_TRANSPORT_API)) { + LOG_WARN("Ignoring packet supposedly from us over external transport"); + return true; + } // FIXME this can in fact happen with packets sent from EUD (src=RX_SRC_USER) // to set fixed location, EUD-GPS location or just the time (see also issue #900) @@ -345,6 +349,11 @@ void PositionModule::sendOurPosition() void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) { + if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) { + LOG_DEBUG("Skip position send; no fresh position since boot"); + return; + } + // cancel any not yet sent (now stale) position packets if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal) service->cancelSending(prevPacketId); @@ -416,8 +425,14 @@ int32_t PositionModule::runOnce() return RUNONCE_INTERVAL; } + bool waitingForFreshPosition = (lastGpsSend == 0) && !config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot(); + if (lastGpsSend == 0 || msSinceLastSend >= intervalMs) { - if (nodeDB->hasValidPosition(node)) { + if (waitingForFreshPosition) { +#ifdef GPS_DEBUG + LOG_DEBUG("Skip initial position send; no fresh position since boot"); +#endif + } else if (nodeDB->hasValidPosition(node)) { lastGpsSend = now; lastGpsLatitude = node->position.latitude_i; @@ -472,19 +487,53 @@ void PositionModule::sendLostAndFoundText() delete[] message; } +// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision +static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t precisionBits, int32_t &outLat, int32_t &outLon) +{ + if (precisionBits > 0 && precisionBits < 32) { + // Build mask for top 'precisionBits' bits of a 32-bit unsigned field + const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits)); + // Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but + // the bitmask logic used previously operated as unsigned—preserve that behavior by + // casting to uint32_t for masking, then back to int32_t. + uint32_t lat_u = static_cast(inLat) & mask; + uint32_t lon_u = static_cast(inLon) & mask; + + // Add the "center of cell" offset used elsewhere: + // The code previously added (1 << (31 - precision)) to produce the middle of the possible location. + uint32_t center_offset = (1u << (31 - precisionBits)); + lat_u += center_offset; + lon_u += center_offset; + + outLat = static_cast(lat_u); + outLon = static_cast(lon_u); + } else { + // full precision: return input unchanged + outLat = inLat; + outLon = inLon; + } +} + struct SmartPosition PositionModule::getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition) { - // The minimum distance to travel before we are able to send a new position packet. const uint32_t distanceTravelThreshold = Default::getConfiguredOrDefault(config.position.broadcast_smart_minimum_distance, 100); - // Determine the distance in meters between two points on the globe - float distanceTraveledSinceLastSend = GeoCoord::latLongToMeter( - lastGpsLatitude * 1e-7, lastGpsLongitude * 1e-7, currentPosition.latitude_i * 1e-7, currentPosition.longitude_i * 1e-7); + int32_t lastLatImprecise, lastLonImprecise; + int32_t currentLatImprecise, currentLonImprecise; - return SmartPosition{.distanceTraveled = abs(distanceTraveledSinceLastSend), + computeImpreciseLatLon(lastGpsLatitude, lastGpsLongitude, precision, lastLatImprecise, lastLonImprecise); + computeImpreciseLatLon(currentPosition.latitude_i, currentPosition.longitude_i, precision, currentLatImprecise, + currentLonImprecise); + + float distMeters = GeoCoord::latLongToMeter(lastLatImprecise * 1e-7, lastLonImprecise * 1e-7, currentLatImprecise * 1e-7, + currentLonImprecise * 1e-7); + + float distanceTraveled = fabsf(distMeters); + + return SmartPosition{.distanceTraveled = distanceTraveled, .distanceThreshold = distanceTravelThreshold, - .hasTraveledOverThreshold = abs(distanceTraveledSinceLastSend) >= distanceTravelThreshold}; + .hasTraveledOverThreshold = distanceTraveled >= distanceTravelThreshold}; } void PositionModule::handleNewPosition() diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 4a2415058..32e499531 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -69,7 +69,7 @@ class PositionModule : public ProtobufModule, private concu // In event mode we want to prevent excessive position broadcasts // we set the minimum interval to 5m const uint32_t minimumTimeThreshold = - max(300000, Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); + max(uint32_t(300000), Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30)); #else const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index 05173983c..e9e1fc786 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -58,12 +58,11 @@ void RoutingModule::sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketI router->sendLocal(p); // we sometimes send directly to the local node } -uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit) +uint8_t RoutingModule::getHopLimitForResponse(const meshtastic_MeshPacket &mp) { - if (hopStart != 0) { - // Hops used by the request. If somebody in between running modified firmware modified it, ignore it - uint8_t hopsUsed = hopStart < hopLimit ? config.lora.hop_limit : hopStart - hopLimit; - if (hopsUsed > config.lora.hop_limit) { + const int8_t hopsUsed = getHopsAway(mp); + if (hopsUsed >= 0) { + if (hopsUsed > (int32_t)(config.lora.hop_limit)) { // In event mode, we never want to send packets with more than our default 3 hops. #if !(EVENTMODE) // This falls through to the default. return hopsUsed; // If the request used more hops than the limit, use the same amount of hops @@ -75,6 +74,12 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } +meshtastic_MeshPacket *RoutingModule::allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit) +{ + return MeshModule::allocAckNak(err, to, idFrom, chIndex, hopLimit); +} + RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index a4e0679d0..2ac42f447 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -16,8 +16,11 @@ class RoutingModule : public ProtobufModule virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0, bool ackWantsAck = false); + meshtastic_MeshPacket *allocAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, + uint8_t hopLimit = 0); + // Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response - uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit); + uint8_t getHopLimitForResponse(const meshtastic_MeshPacket &mp); protected: friend class Router; diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index fc9ed310e..8738c16ca 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -20,13 +20,17 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) switch (arg->getStatusType()) { case STATUS_TYPE_POWER: { meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; - if (powerStatus->getHasUSB()) { + if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { power_state = charging; if (powerStatus->getBatteryChargePercent() >= 100) { power_state = charged; } } else { - power_state = discharging; + if (powerStatus->getBatteryChargePercent() > 5) { + power_state = discharging; + } else { + power_state = critical; + } } break; } @@ -58,16 +62,33 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) int32_t StatusLEDModule::runOnce() { + my_interval = 1000; if (power_state == charging) { CHARGE_LED_state = !CHARGE_LED_state; } else if (power_state == charged) { CHARGE_LED_state = LED_STATE_ON; + } else if (power_state == critical) { + if (POWER_LED_starttime + 30000 < millis() && !doing_fast_blink) { + doing_fast_blink = true; + POWER_LED_starttime = millis(); + } + if (doing_fast_blink) { + PAIRING_LED_state = LED_STATE_OFF; + CHARGE_LED_state = !CHARGE_LED_state; + my_interval = 250; + if (POWER_LED_starttime + 2000 < millis()) { + doing_fast_blink = false; + } + } else { + CHARGE_LED_state = LED_STATE_OFF; + } + } else { CHARGE_LED_state = LED_STATE_OFF; } - if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis()) { + if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { PAIRING_LED_state = LED_STATE_OFF; } else if (ble_state == unpaired) { if (slowTrack) { diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index d9e3a4f33..d90ff718c 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -31,8 +31,10 @@ class StatusLEDModule : private concurrency::OSThread bool PAIRING_LED_state = LED_STATE_OFF; uint32_t PAIRING_LED_starttime = 0; + uint32_t POWER_LED_starttime = 0; + bool doing_fast_blink = false; - enum PowerState { discharging, charging, charged }; + enum PowerState { discharging, charging, charged, critical }; PowerState power_state = discharging; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index dc5d8b41f..1da756366 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -1,9 +1,13 @@ #include "SystemCommandsModule.h" +#include "input/InputBroker.h" #include "meshUtils.h" + #if HAS_SCREEN +#include "MessageStore.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #endif + #include "GPS.h" #include "MeshService.h" #include "Module.h" @@ -22,15 +26,12 @@ SystemCommandsModule::SystemCommandsModule() int SystemCommandsModule::handleInputEvent(const InputEvent *event) { - LOG_INFO("Input event %u! kb %u", event->inputEvent, event->kbchar); + LOG_INPUT("SystemCommands Input event %u! kb %u", event->inputEvent, event->kbchar); // System commands (all others fall through) switch (event->kbchar) { // Fn key symbols case INPUT_BROKER_MSG_FN_SYMBOL_ON: - IF_SCREEN(screen->setFunctionSymbol("Fn")); - return 0; case INPUT_BROKER_MSG_FN_SYMBOL_OFF: - IF_SCREEN(screen->removeFunctionSymbol("Fn")); return 0; // Brightness case INPUT_BROKER_MSG_BRIGHTNESS_UP: @@ -44,10 +45,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) // Mute case INPUT_BROKER_MSG_MUTE_TOGGLE: if (moduleConfig.external_notification.enabled && externalNotificationModule) { - bool isMuted = externalNotificationModule->getMute(); - externalNotificationModule->setMute(!isMuted); - IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow(); - screen->showSimpleBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + externalNotificationModule->setMute(!externalNotificationModule->getMute()); + IF_SCREEN(if (!externalNotificationModule->getMute()) externalNotificationModule->stopNow(); screen->showSimpleBanner( + externalNotificationModule->getMute() ? "Notifications\nDisabled" : "Notifications\nEnabled", 3000);) } return 0; // Bluetooth @@ -77,6 +77,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) case INPUT_BROKER_MSG_REBOOT: IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); nodeDB->saveToDisk(); +#if HAS_SCREEN + messageStore.saveToFlash(); +#endif rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; return true; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 29e815092..843d7b8d5 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -53,7 +53,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "Sensor/LTR390UVSensor.h" #endif -#if __has_include() +#if __has_include(MESHTASTIC_BME680_HEADER) #include "Sensor/BME680Sensor.h" #endif @@ -214,7 +214,7 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) #if __has_include() addSensor(i2cScanner, ScanI2C::DeviceType::LTR390UV); #endif -#if __has_include() +#if __has_include(MESHTASTIC_BME680_HEADER) addSensor(i2cScanner, ScanI2C::DeviceType::BME_680); #endif #if __has_include() @@ -378,7 +378,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index 215e49c7a..572f0281a 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -136,12 +136,12 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { char heartStr[32]; - snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %u bpm", lastMeasurement.variant.health_metrics.heart_bpm); display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { char spo2Str[32]; - snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + snprintf(spo2Str, sizeof(spo2Str), "spO2: %u %%", lastMeasurement.variant.health_metrics.spO2); display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 29dd1def8..9047c7cd4 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -117,7 +117,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s int line = 1; // === Set Title - const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power"; + const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 95f3dc5f0..22330ca75 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "BME680Sensor.h" @@ -10,6 +10,7 @@ BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 int32_t BME680Sensor::runOnce() { if (!bme680.run()) { @@ -17,10 +18,13 @@ int32_t BME680Sensor::runOnce() } return 35; } +#endif // defined(MESHTASTIC_BME680_BSEC2_SUPPORTED) bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) { status = 0; + +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 if (!bme680.begin(dev->address.address, *bus)) checkStatus("begin"); @@ -42,12 +46,25 @@ bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) if (status == 0) LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status); +#else + bme680 = makeBME680(bus); + + if (!bme680->begin(dev->address.address)) { + LOG_ERROR("Init sensor: %s failed at begin()", sensorName); + return status; + } + + status = 1; + +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED + initI2CSensor(); return status; } bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) { +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0) return false; @@ -65,9 +82,27 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) // Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms) measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal; updateState(); +#else + if (!bme680->performReading()) { + LOG_ERROR("BME680Sensor::getMetrics: performReading failed"); + return false; + } + + measurement->variant.environment_metrics.has_temperature = true; + measurement->variant.environment_metrics.has_relative_humidity = true; + measurement->variant.environment_metrics.has_barometric_pressure = true; + measurement->variant.environment_metrics.has_gas_resistance = true; + + measurement->variant.environment_metrics.temperature = bme680->readTemperature(); + measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity(); + measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F; + measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0; + +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED return true; } +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 void BME680Sensor::loadState() { #ifdef FSCom @@ -144,5 +179,6 @@ void BME680Sensor::checkStatus(const char *functionName) else if (bme680.sensor.status > BME68X_OK) LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index f4ead95f7..9bef56e1e 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -1,23 +1,40 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER) #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "TelemetrySensor.h" + +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 +#include #include +#else +#include +#include +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED #define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis() +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 const uint8_t bsec_config[] = { #include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt" }; - +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED class BME680Sensor : public TelemetrySensor { private: +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 Bsec2 bme680; +#else + using BME680Ptr = std::unique_ptr; + + static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique(bus); } + + BME680Ptr bme680; +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED protected: +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 const char *bsecConfigFileName = "/prefs/bsec.dat"; uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0}; uint8_t accuracy = 0; @@ -34,10 +51,13 @@ class BME680Sensor : public TelemetrySensor void loadState(); void updateState(); void checkStatus(const char *functionName); +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED public: BME680Sensor(); +#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1 virtual int32_t runOnce() override; +#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; }; diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index aee359158..d94701c6b 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -1,10 +1,14 @@ #include "TextMessageModule.h" #include "MeshService.h" +#include "MessageStore.h" #include "NodeDB.h" #include "PowerFSM.h" #include "buzz.h" #include "configuration.h" #include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/draw/MessageRenderer.h" +#include "main.h" TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) @@ -13,16 +17,30 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp auto &p = mp.decoded; LOG_INFO("Received text msg from=0x%0x, id=0x%x, msg=%.*s", mp.from, mp.id, p.payload.size, p.payload.bytes); #endif + // add packet ID to the rolling list of packets + textPacketList[textPacketListIndex] = mp.id; + textPacketListIndex = (textPacketListIndex + 1) % TEXT_PACKET_LIST_SIZE; // We only store/display messages destined for us. - // Keep a copy of the most recent text message. devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; + IF_SCREEN( + // Guard against running in MeshtasticUI or with no screen + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + // Store in the central message history + const StoredMessage &sm = messageStore.addFromPacket(mp); + // Pass message to renderer (banner + thread switching + scroll reset) + // Use the global Screen singleton to retrieve the current OLED display + auto *display = screen ? screen->getDisplayDevice() : nullptr; + graphics::MessageRenderer::handleNewMessage(display, sm, mp); + }) // Only trigger screen wake if configuration allows it if (shouldWakeOnReceivedMessage()) { powerFSM.trigger(EVENT_RECEIVED_MSG); } + + // Notify any observers (e.g. external modules that care about packets) notifyObservers(&mp); return ProcessMessage::CONTINUE; // Let others look at this message also if they want @@ -32,3 +50,13 @@ bool TextMessageModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); } + +bool TextMessageModule::recentlySeen(uint32_t id) +{ + for (size_t i = 0; i < TEXT_PACKET_LIST_SIZE; i++) { + if (textPacketList[i] != 0 && textPacketList[i] == id) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/src/modules/TextMessageModule.h b/src/modules/TextMessageModule.h index cc0b0f9d5..42900a78e 100644 --- a/src/modules/TextMessageModule.h +++ b/src/modules/TextMessageModule.h @@ -1,9 +1,16 @@ #pragma once #include "Observer.h" #include "SinglePortModule.h" +#define TEXT_PACKET_LIST_SIZE 50 /** - * Text message handling for meshtastic - draws on the OLED display the most recent received message + * Text message handling for Meshtastic. + * + * This module is responsible for receiving and storing incoming text messages + * from the mesh. It updates device state and notifies observers so that other + * components (such as the MessageRenderer) can later display or process them. + * + * Rendering of messages on screen is no longer done here. */ class TextMessageModule : public SinglePortModule, public Observable { @@ -13,14 +20,20 @@ class TextMessageModule : public SinglePortModule, public Observable= 0) { int8_t diff = hopsTaken - *route_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*route_count < ROUTE_SIZE) { route[*route_count] = NODENUM_BROADCAST; // This will represent an unknown hop *route_count += 1; @@ -370,7 +371,7 @@ void TraceRouteModule::insertUnknownHops(meshtastic_MeshPacket &p, meshtastic_Ro } // Add unknown SNR values if necessary diff = *route_count - *snr_count; - for (uint8_t i = 0; i < diff; i++) { + for (int8_t i = 0; i < diff; i++) { if (*snr_count < ROUTE_SIZE) { snr_list[*snr_count] = INT8_MIN; // This will represent an unknown SNR *snr_count += 1; diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 4b05d5fa1..4db80ba18 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,6 +2,7 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/draw/CompassRenderer.h" #if HAS_SCREEN @@ -14,6 +15,15 @@ WaypointModule *waypointModule; +static inline float degToRad(float deg) +{ + return deg * PI / 180.0f; +} +static inline float radToDeg(float rad) +{ + return rad * 180.0f / PI; +} + ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) { #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) @@ -52,31 +62,15 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) bool WaypointModule::shouldDraw() { #if !MESHTASTIC_EXCLUDE_WAYPOINT - if (screen == nullptr) - return false; - // If no waypoint to show - if (!devicestate.has_rx_waypoint) + if (!screen || !devicestate.has_rx_waypoint) return false; - // Decode the message, to find the expiration time (is waypoint still valid) - // This handles "deletion" as well as expiration - meshtastic_Waypoint wp; - memset(&wp, 0, sizeof(wp)); + meshtastic_Waypoint wp{}; // <- replaces memset if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - // Valid waypoint - if (wp.expire > getTime()) - return devicestate.has_rx_waypoint = true; - - // Expired, or deleted - else - return devicestate.has_rx_waypoint = false; + return wp.expire > getTime(); } - - // If decoding failed - LOG_ERROR("Failed to decode waypoint"); - devicestate.has_rx_waypoint = false; - return false; + return false; // no LOG_ERROR, no flag writes #else return false; #endif @@ -85,53 +79,46 @@ bool WaypointModule::shouldDraw() /// Draw the last waypoint we received void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - if (screen == nullptr) + if (!screen) return; - // Prepare to draw - display->setFont(FONT_SMALL); + display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; - // Handle inverted display - // Unsure of expected behavior: for now, copy drawNodeInfo - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + // === Set Title + const char *titleStr = "Waypoint"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + const int w = display->getWidth(); + const int h = display->getHeight(); // Decode the waypoint const meshtastic_MeshPacket &mp = devicestate.rx_waypoint; - meshtastic_Waypoint wp; - memset(&wp, 0, sizeof(wp)); + meshtastic_Waypoint wp{}; if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) { - // This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case - display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint"); devicestate.has_rx_waypoint = false; return; } // Get timestamp info. Will pass as a field to drawColumns - static char lastStr[20]; + char lastStr[20]; getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns - static char distStr[20]; + char distStr[20]; // Get our node, to use our own position meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - // Text fields to draw (left of compass) - // Last element must be NULL. This signals the end of the char*[] to drawColumns - const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL}; - // Dimensions / co-ordinates for the compass/circle - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + display->getHeight() / 2; - } else { - compassX = x + display->getWidth() - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; - } + const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h); + const int16_t compassX = x + w - (compassDiam / 2) - 5; + const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) + ? y + h / 2 + : y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2; // If our node has a position: if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { @@ -141,7 +128,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = 0; } else { if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + myHeading = degToRad(screen->getHeading()); else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); } @@ -157,46 +144,35 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; + bearingToOtherDegrees = radToDeg(bearingToOtherDegrees); // Distance to Waypoint float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - if (d < (2 * MILES_TO_FEET)) - snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees); + float feet = d * METERS_TO_FEET; + snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°", + feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees); } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000, + bearingToOtherDegrees); } - } - // If our node doesn't have position else { - // ? in the compass display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); // ? in the distance field - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - strncpy(distStr, "? mi ?°", sizeof(distStr)); - else - strncpy(distStr, "? km ?°", sizeof(distStr)); + snprintf(distStr, sizeof(distStr), "? %s ?°", + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km"); } // Draw compass circle display->drawCircle(compassX, compassY, compassDiam / 2); - // Undo color-inversion, if set prior to drawing header - // Unsure of expected behavior? For now: copy drawNodeInfo - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - // Must be after distStr is populated - graphics::NodeListRenderer::drawColumns(display, x, y, fields); + display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here! + display->drawString(0, graphics::getTextPositions(display)[line++], lastStr); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.name); + display->drawString(0, graphics::getTextPositions(display)[line++], wp.description); + display->drawString(0, graphics::getTextPositions(display)[line++], distStr); } #endif diff --git a/src/motion/BMA423Sensor.cpp b/src/motion/BMA423Sensor.cpp index 7951a236e..5111dae32 100755 --- a/src/motion/BMA423Sensor.cpp +++ b/src/motion/BMA423Sensor.cpp @@ -2,16 +2,14 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_BMA423) && __has_include() -using namespace MotionSensorI2C; - BMA423Sensor::BMA423Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} bool BMA423Sensor::init() { - if (sensor.begin(deviceAddress(), &MotionSensorI2C::readRegister, &MotionSensorI2C::writeRegister)) { + if (sensor.begin(Wire, deviceAddress())) { sensor.configAccelerometer(sensor.RANGE_2G, sensor.ODR_100HZ, sensor.BW_NORMAL_AVG4, sensor.PERF_CONTINUOUS_MODE); sensor.enableAccelerometer(); - sensor.configInterrupt(BMA4_LEVEL_TRIGGER, BMA4_ACTIVE_HIGH, BMA4_PUSH_PULL, BMA4_OUTPUT_ENABLE, BMA4_INPUT_DISABLE); + sensor.configInterrupt(); #ifdef BMA423_INT pinMode(BMA4XX_INT, INPUT); @@ -26,9 +24,9 @@ bool BMA423Sensor::init() #ifdef T_WATCH_S3 // Need to raise the wrist function, need to set the correct axis - sensor.setReampAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); + sensor.setRemapAxes(sensor.REMAP_TOP_LAYER_RIGHT_CORNER); #else - sensor.setReampAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); + sensor.setRemapAxes(sensor.REMAP_BOTTOM_LAYER_BOTTOM_LEFT_CORNER); #endif // sensor.enableFeature(sensor.FEATURE_STEP_CNTR, true); sensor.enableFeature(sensor.FEATURE_TILT, true); @@ -50,7 +48,7 @@ bool BMA423Sensor::init() int32_t BMA423Sensor::runOnce() { - if (sensor.readIrqStatus() != DEV_WIRE_NONE) { + if (sensor.readIrqStatus()) { if (sensor.isTilt() || sensor.isDoubleTap()) { wakeScreen(); return 500; diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 5039f2551..8eb3bf95b 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -61,32 +61,6 @@ class MotionSensor uint32_t endCalibrationAt = 0; }; -namespace MotionSensorI2C -{ - -static inline int readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) -{ - Wire.beginTransmission(address); - Wire.write(reg); - Wire.endTransmission(); - Wire.requestFrom((uint8_t)address, (uint8_t)len); - uint8_t i = 0; - while (Wire.available()) { - data[i++] = Wire.read(); - } - return 0; // Pass -} - -static inline int writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint8_t len) -{ - Wire.beginTransmission(address); - Wire.write(reg); - Wire.write(data, len); - return (0 != Wire.endTransmission()); -} - -} // namespace MotionSensorI2C - #endif #endif \ No newline at end of file diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index ad35e152a..7c33f0360 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -87,10 +87,13 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length) // Generate an implicit ACK towards ourselves (handled and processed only locally!) for this message. // We do this because packets are not rebroadcasted back into MQTT anymore and we assume that at least one node // receives it when we get our own packet back. Then we'll stop our retransmissions. - if (isFromUs(e.packet)) - routingModule->sendAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); - else + if (isFromUs(e.packet)) { + auto pAck = routingModule->allocAckNak(meshtastic_Routing_Error_NONE, getFrom(e.packet), e.packet->id, ch.index); + pAck->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT; + router->sendLocal(pAck); + } else { LOG_INFO("Ignore downlink message we originally sent"); + } return; } if (isFromUs(e.packet)) { diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 69da25884..3b98eca3d 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -651,8 +651,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str()); -#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) const uint16_t connHandle = connInfo.getConnHandle(); +#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) int phyResult = ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY); if (phyResult == 0) { @@ -660,6 +660,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks } else { LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult); } +#endif int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs); if (dataLenResult == 0) { @@ -670,9 +671,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu); pServer->updateConnParams(connHandle, 6, 12, 0, 200); -#endif } +#endif +#ifdef NIMBLE_TWO virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) { LOG_INFO("BLE disconnect reason: %d", reason); @@ -818,7 +820,7 @@ void NimbleBluetooth::setup() NimBLEDevice::init(getDeviceName()); NimBLEDevice::setPower(ESP_PWR_LVL_P9); -#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) +#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)) int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu); if (mtuResult == 0) { LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 9b5abfba0..085692f96 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -101,8 +101,6 @@ #define HW_VENDOR meshtastic_HardwareModel_T_WATCH_S3 #elif defined(GENIEBLOCKS) #define HW_VENDOR meshtastic_HardwareModel_GENIEBLOCKS -#elif defined(PRIVATE_HW) -#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #elif defined(NANO_G1) #define HW_VENDOR meshtastic_HardwareModel_NANO_G1 #elif defined(M5STACK) @@ -205,6 +203,8 @@ #define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L #elif defined(HELTEC_WIRELESS_TRACKER_V2) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 +#else +#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW #endif // ----------------------------------------------------------------------------- diff --git a/src/platform/extra_variants/tbeam_displayshield/variant.cpp b/src/platform/extra_variants/tbeam_displayshield/variant.cpp new file mode 100644 index 000000000..7beac2293 --- /dev/null +++ b/src/platform/extra_variants/tbeam_displayshield/variant.cpp @@ -0,0 +1,43 @@ +#include "configuration.h" + +#ifdef HAS_CST226SE + +#include "TouchDrvCSTXXX.hpp" +#include "input/TouchScreenImpl1.h" +#include + +TouchDrvCSTXXX tsPanel; +static constexpr uint8_t PossibleAddresses[2] = {CST328_ADDR, CST226SE_ADDR_ALT}; +uint8_t i2cAddress = 0; + +bool readTouch(int16_t *x, int16_t *y) +{ + int16_t x_array[1], y_array[1]; + uint8_t touched = tsPanel.getPoint(x_array, y_array, 1); + if (touched > 0) { + *y = x_array[0]; + *x = (TFT_WIDTH - y_array[0]); + // Check bounds + if (*x < 0 || *x >= TFT_WIDTH || *y < 0 || *y >= TFT_HEIGHT) { + return false; + } + return true; // Valid touch detected + } + return false; // No valid touch data +} + +void lateInitVariant() +{ + tsPanel.setTouchDrvModel(TouchDrv_CST226); + for (uint8_t addr : PossibleAddresses) { + if (tsPanel.begin(Wire, addr, I2C_SDA, I2C_SCL)) { + i2cAddress = addr; + LOG_DEBUG("CST226SE init OK at address 0x%02X", addr); + touchScreenImpl1 = new TouchScreenImpl1(TFT_WIDTH, TFT_HEIGHT, readTouch); + touchScreenImpl1->init(); + return; + } + } + LOG_ERROR("CST226SE init failed at all known addresses"); +} +#endif diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 1568e1790..d4699cd8c 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -109,7 +109,7 @@ #elif defined(HELTEC_MESH_SOLAR) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR #elif defined(MUZI_BASE) -#define HW_VENDOR meshtastic_HardwareModel_RESERVED_FRIED_CHICKEN +#define HW_VENDOR meshtastic_HardwareModel_MUZI_BASE #else #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c03cc4454..472107229 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -335,6 +335,16 @@ void cpuDeepSleep(uint32_t msecToWake) if (Serial1) // A straightforward solution to the wake from deepsleep problem Serial1.end(); #endif + +#ifdef TTGO_T_ECHO + // To power off the T-Echo, the display must be set + // as an input pin; otherwise, there will be leakage current. + pinMode(PIN_EINK_CS, INPUT); + pinMode(PIN_EINK_DC, INPUT); + pinMode(PIN_EINK_RES, INPUT); + pinMode(PIN_EINK_BUSY, INPUT); +#endif + setBluetoothEnable(false); #ifdef RAK4630 diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 10b3a7fe4..f2b58f333 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -29,6 +29,7 @@ portduino_config_struct portduino_config; std::ofstream traceFile; +std::ofstream JSONFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; @@ -278,7 +279,7 @@ void portduinoSetup() // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 // :mac address :<16 random unique bytes in hexidecimal> : crc32 // crc32 is calculated on the eeprom string up to but not including the final colon - if (strlen(autoconf_product) < 6) { + if (strlen(autoconf_product) < 6 && portduino_config.i2cdev != "") { try { char *mac_start = nullptr; char *devID_start = nullptr; @@ -365,6 +366,14 @@ void portduinoSetup() cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml"); } else if (found_ch341) { product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml"); + // look for more data after the null terminator + size_t len = strlen(autoconf_product); + if (len < 74) { + memcpy(portduino_config.device_id, autoconf_product + len + 1, 16); + if (!memfll(portduino_config.device_id, '\0', 16) && !memfll(portduino_config.device_id, 0xff, 16)) { + portduino_config.has_device_id = true; + } + } } // Don't try to automatically find config for a device with RAK eeprom. @@ -426,11 +435,13 @@ void portduinoSetup() } getMacAddr(dmac); +#ifndef UNIT_TEST if (dmac[0] == 0 && dmac[1] == 0 && dmac[2] == 0 && dmac[3] == 0 && dmac[4] == 0 && dmac[5] == 0) { std::cout << "*** Blank MAC Address not allowed!" << std::endl; std::cout << "Please set a MAC Address in config.yaml using either MACAddress or MACAddressSource." << std::endl; exit(EXIT_FAILURE); } +#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)); @@ -463,6 +474,7 @@ void portduinoSetup() if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { SPI.begin(portduino_config.lora_spi_dev.c_str()); } + if (portduino_config.traceFilename != "") { try { traceFile.open(portduino_config.traceFilename, std::ios::out | std::ios::app); @@ -470,6 +482,21 @@ void portduinoSetup() std::cout << "*** traceFile Exception " << e.what() << std::endl; exit(EXIT_FAILURE); } + if (!traceFile.is_open()) { + std::cout << "*** traceFile open failure" << std::endl; + exit(EXIT_FAILURE); + } + } else if (portduino_config.JSONFilename != "") { + try { + 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); + } + if (!JSONFile.is_open()) { + std::cout << "*** JSONFile open failure" << std::endl; + exit(EXIT_FAILURE); + } } if (verboseEnabled && portduino_config.logoutputlevel != level_trace) { portduino_config.logoutputlevel = level_debug; @@ -517,6 +544,29 @@ bool loadConfig(const char *configPath) portduino_config.logoutputlevel = level_error; } portduino_config.traceFilename = yamlConfig["Logging"]["TraceFile"].as(""); + portduino_config.JSONFilename = yamlConfig["Logging"]["JSONFile"].as(""); + portduino_config.JSONFilter = (_meshtastic_PortNum)yamlConfig["Logging"]["JSONFilter"].as(0); + if (yamlConfig["Logging"]["JSONFilter"].as("") == "textmessage") + portduino_config.JSONFilter = meshtastic_PortNum_TEXT_MESSAGE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "telemetry") + portduino_config.JSONFilter = meshtastic_PortNum_TELEMETRY_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "nodeinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NODEINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "position") + portduino_config.JSONFilter = meshtastic_PortNum_POSITION_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "waypoint") + portduino_config.JSONFilter = meshtastic_PortNum_WAYPOINT_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "neighborinfo") + portduino_config.JSONFilter = meshtastic_PortNum_NEIGHBORINFO_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "traceroute") + portduino_config.JSONFilter = meshtastic_PortNum_TRACEROUTE_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "detection") + portduino_config.JSONFilter = meshtastic_PortNum_DETECTION_SENSOR_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "paxcounter") + portduino_config.JSONFilter = meshtastic_PortNum_PAXCOUNTER_APP; + else if (yamlConfig["Logging"]["JSONFilter"].as("") == "remotehardware") + portduino_config.JSONFilter = meshtastic_PortNum_REMOTE_HARDWARE_APP; + if (yamlConfig["Logging"]["AsciiLogs"]) { // Default is !isatty(1) but can be set explicitly in config.yaml portduino_config.ascii_logs = yamlConfig["Logging"]["AsciiLogs"].as(); @@ -827,4 +877,4 @@ void readGPIOFromYaml(YAML::Node sourceNode, pinMapping &destPin, int pinDefault destPin.line = destPin.pin; destPin.gpiochip = portduino_config.lora_default_gpiochip; } -} \ No newline at end of file +} diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 3fe017d5e..9335be90a 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -5,6 +5,7 @@ #include "LR11x0Interface.h" #include "Module.h" +#include "mesh/generated/meshtastic/mesh.pb.h" #include "platform/portduino/USBHal.h" #include "yaml-cpp/yaml.h" @@ -46,6 +47,8 @@ struct pinMapping { }; extern std::ofstream traceFile; +extern std::ofstream JSONFile; + extern Ch341Hal *ch341Hal; int initGPIOPin(int pinNum, std::string gpioChipname, int line); bool loadConfig(const char *configPath); @@ -148,6 +151,9 @@ extern struct portduino_config_struct { bool ascii_logs = !isatty(1); bool ascii_logs_explicit = false; + std::string JSONFilename; + meshtastic_PortNum JSONFilter = (_meshtastic_PortNum)0; + // Webserver std::string webserver_root_path = ""; std::string webserver_ssl_key_path = "/etc/meshtasticd/ssl/private_key.pem"; @@ -413,6 +419,29 @@ extern struct portduino_config_struct { } if (traceFilename != "") out << YAML::Key << "TraceFile" << YAML::Value << traceFilename; + if (JSONFilename != "") { + out << YAML::Key << "JSONFile" << YAML::Value << JSONFilename; + if (JSONFilter == meshtastic_PortNum_TEXT_MESSAGE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "textmessage"; + else if (JSONFilter == meshtastic_PortNum_TELEMETRY_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "telemetry"; + else if (JSONFilter == meshtastic_PortNum_NODEINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "nodeinfo"; + else if (JSONFilter == meshtastic_PortNum_POSITION_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "position"; + else if (JSONFilter == meshtastic_PortNum_WAYPOINT_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "waypoint"; + else if (JSONFilter == meshtastic_PortNum_NEIGHBORINFO_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "neighborinfo"; + else if (JSONFilter == meshtastic_PortNum_TRACEROUTE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "traceroute"; + else if (JSONFilter == meshtastic_PortNum_DETECTION_SENSOR_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "detection"; + else if (JSONFilter == meshtastic_PortNum_PAXCOUNTER_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "paxcounter"; + else if (JSONFilter == meshtastic_PortNum_REMOTE_HARDWARE_APP) + out << YAML::Key << "JSONFilter" << YAML::Value << "remotehardware"; + } if (ascii_logs_explicit) { out << YAML::Key << "AsciiLogs" << YAML::Value << ascii_logs; } diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index ce2a5cfd3..ecc292430 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -64,7 +64,7 @@ class Ch341Hal : public RadioLibHal void getProductString(char *_product_string, size_t len) { len = len > 95 ? 95 : len; - strncpy(_product_string, pinedio.product_string, len); + memcpy(_product_string, pinedio.product_string, len); } void init() override {} diff --git a/src/power.h b/src/power.h index 3f28dedb2..c826d98b4 100644 --- a/src/power.h +++ b/src/power.h @@ -13,6 +13,7 @@ #define NUM_OCV_POINTS 11 #endif +// Device specific curves go in variant.h #ifndef OCV_ARRAY #ifdef CELL_TYPE_LIFEPO4 #define OCV_ARRAY 3400, 3350, 3320, 3290, 3270, 3260, 3250, 3230, 3200, 3120, 3000 @@ -24,18 +25,6 @@ #define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000 #elif defined(CELL_TYPE_LTO) #define OCV_ARRAY 2700, 2560, 2540, 2520, 2500, 2460, 2420, 2400, 2380, 2320, 1500 -#elif defined(TRACKER_T1000_E) -#define OCV_ARRAY 4190, 4042, 3957, 3885, 3820, 3776, 3746, 3725, 3696, 3644, 3100 -#elif defined(HELTEC_MESH_POCKET_BATTERY_5000) -#define OCV_ARRAY 4300, 4240, 4120, 4000, 3888, 3800, 3740, 3698, 3655, 3580, 3400 -#elif defined(HELTEC_MESH_POCKET_BATTERY_10000) -#define OCV_ARRAY 4100, 4060, 3960, 3840, 3729, 3625, 3550, 3500, 3420, 3345, 3100 -#elif defined(SEEED_WIO_TRACKER_L1) -#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300 -#elif defined(SEEED_SOLAR_NODE) -#define OCV_ARRAY 4200, 3986, 3922, 3812, 3734, 3645, 3527, 3420, 3281, 3087, 2786 -#elif defined(WISMESH_TAG) -#define OCV_ARRAY 4240, 4112, 4029, 3970, 3906, 3846, 3824, 3802, 3776, 3650, 3072 #else // LiIon #define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100 #endif diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index b31d2dc2e..a12972cb0 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -418,8 +418,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } @@ -450,8 +451,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = new JSONValue((int)mp->rx_rssi); if (mp->rx_snr != 0) jsonObj["snr"] = new JSONValue((float)mp->rx_snr); - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = new JSONValue((unsigned int)(mp->hop_start - mp->hop_limit)); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = new JSONValue((unsigned int)(hopsAway)); jsonObj["hop_start"] = new JSONValue((unsigned int)(mp->hop_start)); } jsonObj["size"] = new JSONValue((unsigned int)mp->encrypted.size); diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 353c710a1..41f505b94 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -358,8 +358,9 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } @@ -393,8 +394,9 @@ std::string MeshPacketSerializer::JsonSerializeEncrypted(const meshtastic_MeshPa jsonObj["rssi"] = (int)mp->rx_rssi; if (mp->rx_snr != 0) jsonObj["snr"] = (float)mp->rx_snr; - if (mp->hop_start != 0 && mp->hop_limit <= mp->hop_start) { - jsonObj["hops_away"] = (unsigned int)(mp->hop_start - mp->hop_limit); + const int8_t hopsAway = getHopsAway(*mp); + if (hopsAway >= 0) { + jsonObj["hops_away"] = (unsigned int)(hopsAway); jsonObj["hop_start"] = (unsigned int)(mp->hop_start); } jsonObj["size"] = (unsigned int)mp->encrypted.size; diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index 1c2f0642a..a566dabf7 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -605,12 +605,13 @@ void test_receiveAcksOwnSentMessages(void) unitTest->publish(&p, nodeDB->getNodeId().c_str()); - TEST_ASSERT_TRUE(mockRouter->packets_.empty()); - TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); - const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); - TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); - TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); - TEST_ASSERT_EQUAL(p.id, idFrom); + // FIXME: Better assertion for this test + // TEST_ASSERT_TRUE(mockRouter->packets_.empty()); + // TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size()); + // const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front(); + // TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err); + // TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to); + // TEST_ASSERT_EQUAL(p.id, idFrom); } // Should ignore our own messages from MQTT that were heard by other nodes. diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 0c92eabcf..9e916aae2 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -55,6 +55,7 @@ // "USERPREFS_MQTT_TLS_ENABLED": "false", // "USERPREFS_MQTT_ROOT_TOPIC": "event/REPLACEME", // "USERPREFS_RINGTONE_NAG_SECS": "60", + // "USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS": "43200", "USERPREFS_RINGTONE_RTTTL": "24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p", // "USERPREFS_NETWORK_IPV6_ENABLED": "1", "USERPREFS_TZ_STRING": "tzplaceholder " diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini index 4d163d834..77a1f7043 100644 --- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini +++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini @@ -15,4 +15,5 @@ upload_protocol = esptool upload_speed = 460800 lib_deps = ${esp32_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32/betafpv_900_tx_nano/platformio.ini b/variants/esp32/betafpv_900_tx_nano/platformio.ini index 7e01fd2fa..a4ebe9694 100644 --- a/variants/esp32/betafpv_900_tx_nano/platformio.ini +++ b/variants/esp32/betafpv_900_tx_nano/platformio.ini @@ -13,5 +13,3 @@ board_build.f_cpu = 240000000L upload_protocol = esptool ;upload_port = /dev/ttyUSB0 upload_speed = 460800 -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/chatter2/platformio.ini b/variants/esp32/chatter2/platformio.ini index bf496bf26..94a846bc9 100644 --- a/variants/esp32/chatter2/platformio.ini +++ b/variants/esp32/chatter2/platformio.ini @@ -9,4 +9,5 @@ build_flags = lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index ff4f87bbe..0c1ef6967 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -62,10 +62,12 @@ #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define TFT_INVERT false +#define FORCE_LOW_RES 1 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 // fps #define DISPLAY_FORCE_SMALL_FONTS #define TFT_BACKLIGHT_ON LOW +#define USE_TFTDISPLAY 1 // Battery diff --git a/variants/esp32/diy/dr-dev/platformio.ini b/variants/esp32/diy/dr-dev/platformio.ini index 5461d27b3..a1022934d 100644 --- a/variants/esp32/diy/dr-dev/platformio.ini +++ b/variants/esp32/diy/dr-dev/platformio.ini @@ -1,7 +1,15 @@ ; Port to Disaster Radio's ESP32-v3 Dev Board [env:meshtastic-dr-dev] +custom_meshtastic_hw_model = 41 +custom_meshtastic_hw_model_slug = DR_DEV +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = DR-DEV +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 +board_level = extra board_upload.maximum_size = 4194304 board_upload.maximum_ram_size = 532480 build_flags = diff --git a/variants/esp32/diy/hydra/platformio.ini b/variants/esp32/diy/hydra/platformio.ini index a922ed874..3afd17e01 100644 --- a/variants/esp32/diy/hydra/platformio.ini +++ b/variants/esp32/diy/hydra/platformio.ini @@ -1,5 +1,13 @@ ; Hydra - Meshtastic DIY v1 hardware with some specific changes [env:hydra] +custom_meshtastic_hw_model = 39 +custom_meshtastic_hw_model_slug = HYDRA +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Hydra +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 build_flags = diff --git a/variants/esp32/diy/v1/platformio.ini b/variants/esp32/diy/v1/platformio.ini index bcbd57cfa..3d31fc24a 100644 --- a/variants/esp32/diy/v1/platformio.ini +++ b/variants/esp32/diy/v1/platformio.ini @@ -1,5 +1,14 @@ ; Meshtastic DIY v1 by Nano VHF Schematic based on ESP32-WROOM-32 (38 pins) devkit & EBYTE E22 SX1262/SX1268 module [env:meshtastic-diy-v1] +custom_meshtastic_hw_model = 39 +custom_meshtastic_hw_model_slug = DIY_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = DIY V1 +custom_meshtastic_images = diy.svg +custom_meshtastic_tags = DIY + extends = esp32_base board = esp32doit-devkit-v1 board_check = true diff --git a/arch/esp32/esp32.ini b/variants/esp32/esp32-common.ini similarity index 89% rename from arch/esp32/esp32.ini rename to variants/esp32/esp32-common.ini index 810c9780e..e582b6880 100644 --- a/arch/esp32/esp32.ini +++ b/variants/esp32/esp32-common.ini @@ -1,10 +1,16 @@ -; Common settings for ESP targes, mixin with extends = esp32_base -[esp32_base] +; Common settings for ESP targets, mixin with extends = esp32_common +[esp32_common] extends = arduino_base -custom_esp32_kind = esp32 +custom_esp32_kind = +custom_mtjson_part = platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.11.0 + platformio/espressif32@6.12.0 + +extra_scripts = + ${env.extra_scripts} + pre:extra_scripts/esp32_pre.py + extra_scripts/esp32_extra.py build_src_filter = ${arduino_base.build_src_filter} - - - - - @@ -47,6 +53,7 @@ build_flags = lib_deps = ${arduino_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} ${environmental_base.lib_deps} ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} @@ -57,7 +64,7 @@ lib_deps = # 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.1.zip + https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip # 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=rweather/Crypto packageName=rweather/library/Crypto diff --git a/variants/esp32/esp32.ini b/variants/esp32/esp32.ini new file mode 100644 index 000000000..d1a8a63b0 --- /dev/null +++ b/variants/esp32/esp32.ini @@ -0,0 +1,8 @@ +; Common settings for ESP32 OG (without suffix) +; See 'esp32_common' for common ESP32-family settings +[esp32_base] +extends = esp32_common +custom_esp32_kind = esp32 + +build_flags = + ${esp32_common.build_flags} \ No newline at end of file diff --git a/variants/esp32/heltec_v1/platformio.ini b/variants/esp32/heltec_v1/platformio.ini index 4be3ba655..770326427 100644 --- a/variants/esp32/heltec_v1/platformio.ini +++ b/variants/esp32/heltec_v1/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v1] +custom_meshtastic_hw_model = 11 +custom_meshtastic_hw_model_slug = HELTEC_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V1 +custom_meshtastic_tags = Heltec + ;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base board_level = extra diff --git a/variants/esp32/heltec_v2.1/platformio.ini b/variants/esp32/heltec_v2.1/platformio.ini index 4dcd9e583..1f7caa16f 100644 --- a/variants/esp32/heltec_v2.1/platformio.ini +++ b/variants/esp32/heltec_v2.1/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v2_1] +custom_meshtastic_hw_model = 10 +custom_meshtastic_hw_model_slug = HELTEC_V2_1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V2.1 +custom_meshtastic_tags = Heltec + board_level = extra ;build_type = debug ; to make it possible to step through our jtag debugger extends = esp32_base diff --git a/variants/esp32/heltec_v2/platformio.ini b/variants/esp32/heltec_v2/platformio.ini index ed455616d..5f15fb321 100644 --- a/variants/esp32/heltec_v2/platformio.ini +++ b/variants/esp32/heltec_v2/platformio.ini @@ -1,4 +1,11 @@ [env:heltec-v2_0] +custom_meshtastic_hw_model = 5 +custom_meshtastic_hw_model_slug = HELTEC_V2_0 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = Heltec V2.0 +custom_meshtastic_tags = Heltec + ;build_type = debug ; to make it possible to step through our jtag debugger board_level = extra extends = esp32_base diff --git a/variants/esp32/m5stack_core/platformio.ini b/variants/esp32/m5stack_core/platformio.ini index 469d93f94..d6c5c5b4c 100644 --- a/variants/esp32/m5stack_core/platformio.ini +++ b/variants/esp32/m5stack_core/platformio.ini @@ -1,4 +1,12 @@ [env:m5stack-core] +custom_meshtastic_hw_model = 42 +custom_meshtastic_hw_model_slug = M5STACK +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = M5 Stack +custom_meshtastic_tags = M5Stack + extends = esp32_base board = m5stack-core-esp32 monitor_filters = esp32_exception_decoder @@ -7,7 +15,6 @@ build_src_filter = build_flags = ${esp32_base.build_flags} -I variants/esp32/m5stack_core - -DILI9341_DRIVER -DM5STACK -DUSER_SETUP_LOADED -DTFT_SDA_READ @@ -26,4 +33,5 @@ lib_ignore = m5stack-core lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 diff --git a/variants/esp32/m5stack_core/variant.h b/variants/esp32/m5stack_core/variant.h index 72aeb160e..cf741efe3 100644 --- a/variants/esp32/m5stack_core/variant.h +++ b/variants/esp32/m5stack_core/variant.h @@ -34,11 +34,13 @@ #define GPS_RX_PIN 16 #define GPS_TX_PIN 17 +#define ILI9341_DRIVER #define TFT_HEIGHT 240 #define TFT_WIDTH 320 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define TFT_BUSY -1 +#define USE_TFTDISPLAY 1 // LCD screens are slow, so slowdown the wipe so it looks better #define SCREEN_TRANSITION_FRAMERATE 1 // fps diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index 1a00788e3..a4d44a15e 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -18,8 +18,10 @@ build_flags = -DM5STACK lib_deps = ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 lib_ignore = m5stack-coreink monitor_filters = esp32_exception_decoder diff --git a/variants/esp32/m5stack_coreink/variant.h b/variants/esp32/m5stack_coreink/variant.h index ecd93b7be..b1708352b 100644 --- a/variants/esp32/m5stack_coreink/variant.h +++ b/variants/esp32/m5stack_coreink/variant.h @@ -13,7 +13,6 @@ #define LED_STATE_ON 1 // State when LED is lit #define LED_PIN 10 -#include "pcf8563.h" // PCF8563 RTC Module #define PCF8563_RTC 0x51 #define HAS_RTC 1 diff --git a/variants/esp32/nano-g1-explorer/platformio.ini b/variants/esp32/nano-g1-explorer/platformio.ini index 2ba1f49e9..703bb9d09 100644 --- a/variants/esp32/nano-g1-explorer/platformio.ini +++ b/variants/esp32/nano-g1-explorer/platformio.ini @@ -1,9 +1,15 @@ ; The 1.0 release of the nano-g1-explorer board [env:nano-g1-explorer] +custom_meshtastic_hw_model = 17 +custom_meshtastic_hw_model_slug = NANO_G1_EXPLORER +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Nano G1 Explorer +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} 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 be8227de2..b0ebd191c 100644 --- a/variants/esp32/nano-g1/platformio.ini +++ b/variants/esp32/nano-g1/platformio.ini @@ -1,9 +1,15 @@ ; The 1.0 release of the nano-g1 board [env:nano-g1] +custom_meshtastic_hw_model = 14 +custom_meshtastic_hw_model_slug = NANO_G1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Nano G1 +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D NANO_G1 diff --git a/variants/esp32/radiomaster_900_bandit/platformio.ini b/variants/esp32/radiomaster_900_bandit/platformio.ini index d9eb78a57..6729235ed 100644 --- a/variants/esp32/radiomaster_900_bandit/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit/platformio.ini @@ -13,4 +13,5 @@ board_build.f_cpu = 240000000L upload_protocol = esptool lib_deps = ${esp32_base.lib_deps} + # renovate: datasource=github-tags depName=STK8xxx-Accelerometer packageName=gjelsoe/STK8xxx-Accelerometer https://github.com/gjelsoe/STK8xxx-Accelerometer/archive/v0.1.1.zip diff --git a/variants/esp32/radiomaster_900_bandit_micro/platformio.ini b/variants/esp32/radiomaster_900_bandit_micro/platformio.ini index 36a45787b..32e9280e1 100644 --- a/variants/esp32/radiomaster_900_bandit_micro/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit_micro/platformio.ini @@ -15,5 +15,3 @@ build_flags = -I variants/esp32/radiomaster_900_bandit_nano board_build.f_cpu = 240000000L upload_protocol = esptool -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/radiomaster_900_bandit_nano/platformio.ini b/variants/esp32/radiomaster_900_bandit_nano/platformio.ini index 9a7fad83b..924447ee4 100644 --- a/variants/esp32/radiomaster_900_bandit_nano/platformio.ini +++ b/variants/esp32/radiomaster_900_bandit_nano/platformio.ini @@ -1,4 +1,12 @@ [env:radiomaster_900_bandit_nano] +custom_meshtastic_hw_model = 64 +custom_meshtastic_hw_model_slug = RADIOMASTER_900_BANDIT_NANO +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RadioMaster 900 Bandit Nano +custom_meshtastic_tags = RadioMaster + extends = esp32_base board = esp32doit-devkit-v1 build_flags = @@ -10,5 +18,3 @@ build_flags = -I variants/esp32/radiomaster_900_bandit_nano board_build.f_cpu = 240000000L upload_protocol = esptool -lib_deps = - ${esp32_base.lib_deps} diff --git a/variants/esp32/rak11200/platformio.ini b/variants/esp32/rak11200/platformio.ini index 170e80b41..63821a092 100644 --- a/variants/esp32/rak11200/platformio.ini +++ b/variants/esp32/rak11200/platformio.ini @@ -1,4 +1,13 @@ [env:rak11200] +custom_meshtastic_hw_model = 13 +custom_meshtastic_hw_model_slug = RAK11200 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = RAK WisBlock 11200 +custom_meshtastic_images = rak11200.svg +custom_meshtastic_tags = RAK + extends = esp32_base board = wiscore_rak11200 board_level = pr diff --git a/variants/esp32/station-g1/platformio.ini b/variants/esp32/station-g1/platformio.ini index 693a41ae8..ab7fcac2b 100644 --- a/variants/esp32/station-g1/platformio.ini +++ b/variants/esp32/station-g1/platformio.ini @@ -1,9 +1,15 @@ ; The 1.0 release of the nano-g1 board [env:station-g1] +custom_meshtastic_hw_model = 25 +custom_meshtastic_hw_model_slug = STATION_G1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Station G1 +custom_meshtastic_tags = B&Q + extends = esp32_base board = ttgo-t-beam -lib_deps = - ${esp32_base.lib_deps} build_flags = ${esp32_base.build_flags} -D STATION_G1 diff --git a/variants/esp32/tbeam/platformio.ini b/variants/esp32/tbeam/platformio.ini index e53f22d30..51952457a 100644 --- a/variants/esp32/tbeam/platformio.ini +++ b/variants/esp32/tbeam/platformio.ini @@ -1,15 +1,35 @@ ; The 1.0 release of the TBEAM board [env:tbeam] +custom_meshtastic_hw_model = 4 +custom_meshtastic_hw_model_slug = TBEAM +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Beam +custom_meshtastic_images = tbeam.svg +custom_meshtastic_tags = LilyGo + extends = esp32_base board = ttgo-t-beam -board_level = pr + board_check = true -lib_deps = - ${esp32_base.lib_deps} -build_flags = - ${esp32_base.build_flags} +build_flags = ${esp32_base.build_flags} -D TBEAM_V10 -I variants/esp32/tbeam -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue upload_speed = 921600 + +[env:tbeam-displayshield] +extends = env:tbeam +board_level = extra +build_flags = + ${env:tbeam.build_flags} + -D USE_ST7796 + +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 + lewisxhe/SensorLib@0.3.3 diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index 5b521a2de..2d144a888 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -42,4 +42,35 @@ #define GPS_UBLOX #define GPS_RX_PIN 34 #define GPS_TX_PIN 12 -// #define GPS_DEBUG \ No newline at end of file +// #define GPS_DEBUG + +// Used when the display shield is chosen +#ifdef USE_ST7796 + +#undef EXT_NOTIFY_OUT +#undef LED_STATE_ON +#undef LED_PIN + +#define HAS_CST226SE 1 +#define HAS_TOUCHSCREEN 1 +// #define TOUCH_IRQ 35 // broken in this version of the lib 0.3.1 +#ifndef TOUCH_IRQ +#define TOUCH_IRQ -1 +#endif +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define USE_VIRTUAL_KEYBOARD 1 + +#define ST7796_NSS 25 +#define ST7796_RS 13 // DC +#define ST7796_SDA 14 // MOSI +#define ST7796_SCK 15 +#define ST7796_RESET 2 +#define ST7796_MISO -1 +#define ST7796_BUSY -1 +#define VTFT_LEDA 4 +#define TFT_SPI_FREQUENCY 60000000 +#define TFT_HEIGHT 222 +#define TFT_WIDTH 480 +#define BRIGHTNESS_DEFAULT 100 // Medium Low Brightness +#define SCREEN_TRANSITION_FRAMERATE 5 // fps +#endif \ No newline at end of file diff --git a/variants/esp32/tbeam_v07/platformio.ini b/variants/esp32/tbeam_v07/platformio.ini index 1647d9fd7..e2763fdec 100644 --- a/variants/esp32/tbeam_v07/platformio.ini +++ b/variants/esp32/tbeam_v07/platformio.ini @@ -1,5 +1,12 @@ ; The original TBEAM board without the AXP power chip and a few other changes [env:tbeam0_7] +custom_meshtastic_hw_model = 6 +custom_meshtastic_hw_model_slug = TBEAM_V0P7 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-Beam V0.7 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-t-beam diff --git a/variants/esp32/tlora_v1/platformio.ini b/variants/esp32/tlora_v1/platformio.ini index 1d879b6b0..c45cc2ce9 100644 --- a/variants/esp32/tlora_v1/platformio.ini +++ b/variants/esp32/tlora_v1/platformio.ini @@ -1,4 +1,11 @@ [env:tlora-v1] +custom_meshtastic_hw_model = 2 +custom_meshtastic_hw_model_slug = TLORA_V1 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-LoRa V1 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-lora32-v1 diff --git a/variants/esp32/tlora_v2/platformio.ini b/variants/esp32/tlora_v2/platformio.ini index 4a710ee34..68358bfc3 100644 --- a/variants/esp32/tlora_v2/platformio.ini +++ b/variants/esp32/tlora_v2/platformio.ini @@ -1,4 +1,11 @@ [env:tlora-v2] +custom_meshtastic_hw_model = 1 +custom_meshtastic_hw_model_slug = TLORA_V2 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = false +custom_meshtastic_display_name = LILYGO T-LoRa V2 +custom_meshtastic_tags = LilyGo + board_level = extra extends = esp32_base board = ttgo-lora32-v1 diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini index 8d5bdab9e..d9cb8ed3b 100644 --- a/variants/esp32/tlora_v2_1_16/platformio.ini +++ b/variants/esp32/tlora_v2_1_16/platformio.ini @@ -1,4 +1,13 @@ [env:tlora-v2-1-1_6] +custom_meshtastic_hw_model = 3 +custom_meshtastic_hw_model_slug = TLORA_V2_1_1P6 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.6 +custom_meshtastic_images = tlora-v2-1-1_6.svg +custom_meshtastic_tags = LilyGo + extends = esp32_base board = ttgo-lora32-v21 board_check = true diff --git a/variants/esp32/tlora_v2_1_18/platformio.ini b/variants/esp32/tlora_v2_1_18/platformio.ini index 432117485..173a48692 100644 --- a/variants/esp32/tlora_v2_1_18/platformio.ini +++ b/variants/esp32/tlora_v2_1_18/platformio.ini @@ -1,4 +1,13 @@ [env:tlora-v2-1-1_8] +custom_meshtastic_hw_model = 15 +custom_meshtastic_hw_model_slug = TLORA_V2_1_1P8 +custom_meshtastic_architecture = esp32 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-LoRa V2.1-1.8 +custom_meshtastic_images = tlora-v2-1-1_8.svg +custom_meshtastic_tags = LilyGo, 2.4GHz + extends = esp32_base board_level = extra board = ttgo-lora32-v21 diff --git a/variants/esp32/wiphone/platformio.ini b/variants/esp32/wiphone/platformio.ini index 5cce94b13..fcf36a23c 100644 --- a/variants/esp32/wiphone/platformio.ini +++ b/variants/esp32/wiphone/platformio.ini @@ -10,6 +10,9 @@ build_flags = -I variants/esp32/wiphone lib_deps = ${esp32_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - sparkfun/SX1509 IO Expander@^3.0.5 - pololu/APA102@^3.0.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 + # renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander + sparkfun/SX1509 IO Expander@3.0.6 + # renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102 + pololu/APA102@3.0.0 diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index 70973db16..619ac622a 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -34,6 +34,7 @@ #define ST7789_SCK 18 #define ST7789_CS 5 #define ST7789_RS 26 +#define USE_TFTDISPLAY 1 // I don't have a 'wiphone' but this I think should not be defined this way (don't set TFT_BL if we don't have a hw way to control // it) // #define ST7789_BL -1 // EXTENDER_PIN(9) diff --git a/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini b/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini index c87baa7bf..2dca8e1a9 100644 --- a/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini +++ b/variants/esp32c3/diy/esp32c3_super_mini/platformio.ini @@ -4,7 +4,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/diy/esp32c3_super_mini -D ARDUINO_USB_MODE=1 diff --git a/arch/esp32/esp32c3.ini b/variants/esp32c3/esp32c3.ini similarity index 82% rename from arch/esp32/esp32c3.ini rename to variants/esp32c3/esp32c3.ini index 2ba3036d0..2d7ae71bc 100644 --- a/arch/esp32/esp32c3.ini +++ b/variants/esp32c3/esp32c3.ini @@ -1,5 +1,5 @@ [esp32c3_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32c3 monitor_speed = 115200 diff --git a/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini b/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini index 5a72b9d74..6e31af200 100644 --- a/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini +++ b/variants/esp32c3/hackerboxes_esp32c3_oled/platformio.ini @@ -3,7 +3,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -D ARDUINO_USB_MODE=1 -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/variants/esp32c3/heltec_esp32c3/platformio.ini b/variants/esp32c3/heltec_esp32c3/platformio.ini index 705e2e996..d087e4fd0 100644 --- a/variants/esp32c3/heltec_esp32c3/platformio.ini +++ b/variants/esp32c3/heltec_esp32c3/platformio.ini @@ -1,9 +1,18 @@ [env:heltec-ht62-esp32c3-sx1262] +custom_meshtastic_hw_model = 53 +custom_meshtastic_hw_model_slug = HELTEC_HT62 +custom_meshtastic_architecture = esp32-c3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec HT62 +custom_meshtastic_images = heltec-ht62-esp32c3-sx1262.svg +custom_meshtastic_tags = Heltec + extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = pr build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D HELTEC_HT62 -I variants/esp32c3/heltec_esp32c3 monitor_speed = 115200 diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini index b5ff63eae..8200b6e87 100644 --- a/variants/esp32c3/heltec_hru_3601/platformio.ini +++ b/variants/esp32c3/heltec_hru_3601/platformio.ini @@ -2,8 +2,9 @@ extends = esp32c3_base board = adafruit_qtpy_esp32c3 build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D HELTEC_HRU_3601 -I variants/esp32c3/heltec_hru_3601 lib_deps = ${esp32c3_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32c3/m5stack-stamp-c3/platformio.ini b/variants/esp32c3/m5stack-stamp-c3/platformio.ini index 1072df664..9ea669014 100644 --- a/variants/esp32c3/m5stack-stamp-c3/platformio.ini +++ b/variants/esp32c3/m5stack-stamp-c3/platformio.ini @@ -3,7 +3,7 @@ extends = esp32c3_base board = esp32-c3-devkitm-1 board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32c3_base.build_flags} -D PRIVATE_HW -I variants/esp32c3/m5stack-stamp-c3 monitor_speed = 115200 diff --git a/arch/esp32/esp32c6.ini b/variants/esp32c6/esp32c6.ini similarity index 91% rename from arch/esp32/esp32c6.ini rename to variants/esp32c6/esp32c6.ini index 7b06f4cd8..c1dfa4d28 100644 --- a/arch/esp32/esp32c6.ini +++ b/variants/esp32c6/esp32c6.ini @@ -1,5 +1,5 @@ [esp32c6_base] -extends = esp32_base +extends = esp32_common platform = # Do not renovate until we have switched to pioarduino tagged builds https://github.com/Jason2866/platform-espressif32/archive/22faa566df8c789000f8136cd8d0aca49617af55.zip @@ -28,19 +28,20 @@ lib_deps = ${environmental_extra.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib - lewisxhe/XPowersLib@0.3.1 + lewisxhe/XPowersLib@0.3.2 # 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=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 build_src_filter = - ${esp32_base.build_src_filter} - + ${esp32_common.build_src_filter} - monitor_speed = 460800 monitor_filters = esp32_c3_exception_decoder lib_ignore = + ${esp32_common.lib_ignore} NonBlockingRTTTL NimBLE-Arduino libpax diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index da1c70c0a..3054d342a 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -1,4 +1,13 @@ [env:m5stack-unitc6l] +custom_meshtastic_hw_model = 111 +custom_meshtastic_hw_model_slug = M5STACK_C6L +custom_meshtastic_architecture = esp32-c6 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = M5Stack Unit C6L +custom_meshtastic_images = m5_c6l.svg +custom_meshtastic_tags = M5Stack + extends = esp32c6_base board = esp32-c6-devkitc-1 ;OpenOCD flash method @@ -12,8 +21,10 @@ build_unflags = -D HAS_WIFI lib_deps = ${esp32c6_base.lib_deps} - adafruit/Adafruit NeoPixel@^1.12.3 - h2zero/NimBLE-Arduino@^2.3.6 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 + # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino + h2zero/NimBLE-Arduino@2.3.7 build_flags = ${esp32c6_base.build_flags} -D M5STACK_UNITC6L @@ -22,8 +33,6 @@ build_flags = -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 -D HAS_BLUETOOTH=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER - -D MESHTASTIC_EXCLUDE_MQTT -DCONFIG_BT_NIMBLE_EXT_ADV=1 -DCONFIG_BT_NIMBLE_MAX_EXT_ADV_INSTANCES=2 -D NIMBLE_TWO diff --git a/arch/esp32/esp32s2.ini b/variants/esp32s2/esp32s2.ini similarity index 59% rename from arch/esp32/esp32s2.ini rename to variants/esp32s2/esp32s2.ini index 0f97408b8..c806943ee 100644 --- a/arch/esp32/esp32s2.ini +++ b/variants/esp32s2/esp32s2.ini @@ -1,19 +1,19 @@ [esp32s2_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32s2 build_src_filter = - ${esp32_base.build_src_filter} - - - + ${esp32_common.build_src_filter} - - - monitor_speed = 115200 build_flags = - ${esp32_base.build_flags} + ${esp32_common.build_flags} -DHAS_BLUETOOTH=0 -DMESHTASTIC_EXCLUDE_PAXCOUNTER -DMESHTASTIC_EXCLUDE_BLUETOOTH lib_ignore = - ${esp32_base.lib_ignore} + ${esp32_common.lib_ignore} NimBLE-Arduino libpax diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini b/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini index 3fcfbf281..092b36a2f 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/platformio.ini @@ -1,4 +1,13 @@ [env:CDEBYTE_EoRa-S3] +custom_meshtastic_hw_model = 61 +custom_meshtastic_hw_model_slug = CDEBYTE_EORA_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = EBYTE EoRa-S3 +custom_meshtastic_tags = EByte +custom_meshtastic_requires_dfu = true + extends = esp32s3_base board = CDEBYTE_EoRa-S3 build_flags = diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini index 01e82184b..cfea4c1c0 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/platformio.ini @@ -1,4 +1,14 @@ [env:thinknode_m2] +custom_meshtastic_hw_model = 90 +custom_meshtastic_hw_model_slug = THINKNODE_M2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M2 +custom_meshtastic_images = thinknode_m2.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = false + extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 build_flags = diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h index cd8d43555..ff4f883fe 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h @@ -56,10 +56,6 @@ #define HAS_SCREEN 1 #define USE_SH1106 1 -// PCF8563 RTC Module -// #define PCF8563_RTC 0x51 -// #define PIN_RTC_INT 48 // Interrupt from the PCF8563 RTC -#define HAS_RTC 0 #define HAS_GPS 0 #define BUTTON_PIN PIN_BUTTON1 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index 7dac6e66e..59ac625b6 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -1,4 +1,14 @@ [env:thinknode_m5] +custom_meshtastic_hw_model = 107 +custom_meshtastic_hw_model_slug = THINKNODE_M5 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M5 +custom_meshtastic_images = thinknode_m1.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = false + extends = esp32s3_base board = ESP32-S3-WROOM-1-N4 build_flags = @@ -16,6 +26,7 @@ build_flags = -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 - maxpromer/PCA9557-arduino @ ^1.0.0 \ No newline at end of file + # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino + maxpromer/PCA9557-arduino@1.0.0 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 129b398e9..5f5133e61 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -44,9 +44,6 @@ #define PIN_SERIAL1_RX GPS_TX_PIN #define PIN_SERIAL1_TX GPS_RX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - #define SX126X_CS 17 #define LORA_SCK 16 #define LORA_MOSI 15 diff --git a/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini b/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini index 57af0da82..e8c50adcc 100644 --- a/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini +++ b/variants/esp32s3/bpi_picow_esp32_s3/platformio.ini @@ -8,9 +8,10 @@ board_level = extra upload_protocol = esptool ;upload_port = /dev/ttyACM2 lib_deps = - ${esp32_base.lib_deps} - caveman99/ESP32 Codec2@^1.0.1 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=caveman99-ESP32_Codec2 packageName=caveman99/library/ESP32 Codec2 + caveman99/ESP32 Codec2@1.0.1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/bpi_picow_esp32_s3 diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index eed21a412..315a53ffd 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -25,6 +25,7 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip [env:crowpanel-esp32s3-4-epaper] @@ -54,6 +55,7 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip [env:crowpanel-esp32s3-2-epaper] @@ -83,4 +85,5 @@ build_flags = ;-DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index 267544c40..95e909b91 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -9,14 +9,16 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM1 upload_speed = 921600 lib_deps = - ${esp32_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - adafruit/Adafruit NeoPixel @ ^1.12.0 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/my_esp32s3_diy_eink -Dmy diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini index aa3e6e482..4f759d1e6 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini @@ -9,13 +9,14 @@ upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 lib_deps = - ${esp32_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/diy/my_esp32s3_diy_oled -DBOARD_HAS_PSRAM diff --git a/variants/esp32s3/dreamcatcher/platformio.ini b/variants/esp32s3/dreamcatcher/platformio.ini index d088f2dac..c830346e0 100644 --- a/variants/esp32s3/dreamcatcher/platformio.ini +++ b/variants/esp32s3/dreamcatcher/platformio.ini @@ -12,8 +12,10 @@ build_flags = -D ARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s3_base.lib_deps} - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:dreamcatcher-2206] extends = esp32s3_base diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 065f22538..5c9a4bfaf 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -41,9 +41,13 @@ 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=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.0.1 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality + # renovate: datasource=custom.pio depName=TCA9534 packageName=hideakitai/library/TCA9534 hideakitai/TCA9534@0.1.1 [crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch @@ -71,6 +75,17 @@ build_flags = -D DISPLAY_SET_RESOLUTION [env:elecrow-adv-24-28-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 2.4/2.8 TFT +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 + extends = crowpanel_small_esp32s3_base build_flags = ${crowpanel_small_esp32s3_base.build_flags} @@ -95,6 +110,17 @@ build_flags = -D LGFX_TOUCH_ROTATION=0 [env:elecrow-adv-35-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 3.5 TFT +custom_meshtastic_images = crowpanel_3_5.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = crowpanel_small_esp32s3_base board_level = pr build_flags = @@ -123,6 +149,17 @@ build_flags = ; 4.3, 5.0, 7.0 inch 800x480 IPS (V1) [env:elecrow-adv1-43-50-70-tft] +custom_meshtastic_hw_model = 97 +custom_meshtastic_hw_model_slug = CROWPANEL +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Crowpanel Adv 4.3/5.0/7.0 TFT +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 + extends = crowpanel_large_esp32s3_base build_flags = ${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 11bd4f5a3..8b65c3ca3 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -22,5 +22,7 @@ build_flags = ${esp32s3_base.build_flags} -DEINK_HEIGHT=128 lib_deps = ${esp32s3_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/arch/esp32/esp32s3.ini b/variants/esp32s3/esp32s3.ini similarity index 74% rename from arch/esp32/esp32s3.ini rename to variants/esp32s3/esp32s3.ini index 8d8b6899e..5e333f3ce 100644 --- a/arch/esp32/esp32s3.ini +++ b/variants/esp32s3/esp32s3.ini @@ -1,5 +1,5 @@ [esp32s3_base] -extends = esp32_base +extends = esp32_common custom_esp32_kind = esp32s3 monitor_speed = 115200 diff --git a/variants/esp32s3/hackaday-communicator/pins_arduino.h b/variants/esp32s3/hackaday-communicator/pins_arduino.h new file mode 100644 index 000000000..65d4e1751 --- /dev/null +++ b/variants/esp32s3/hackaday-communicator/pins_arduino.h @@ -0,0 +1,59 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// static const uint8_t TX = 43; +// static const uint8_t RX = 44; + +static const uint8_t SDA = 47; +static const uint8_t SCL = 14; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 17; +static const uint8_t MOSI = 3; +static const uint8_t MISO = 9; +static const uint8_t SCK = 8; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +// static const uint8_t BAT_ADC_PIN = 4; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/hackaday-communicator/platformio.ini b/variants/esp32s3/hackaday-communicator/platformio.ini new file mode 100644 index 000000000..29b2c2305 --- /dev/null +++ b/variants/esp32s3/hackaday-communicator/platformio.ini @@ -0,0 +1,16 @@ +; Hackaday Communicator +[env:hackaday-communicator] +extends = esp32s3_base +board = hackaday-communicator +board_check = true +board_build.partitions = default_16MB.csv +upload_protocol = esptool + +build_flags = ${esp32s3_base.build_flags} + -D HACKADAY_COMMUNICATOR + -D BOARD_HAS_PSRAM + -I variants/esp32s3/hackaday-communicator + +lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-Arduino_GFX packageName=https://github.com/meshtastic/Arduino_GFX gitBranch=master + https://github.com/meshtastic/Arduino_GFX/archive/054e81ffaf23784830a734e3c184346789349406.zip diff --git a/variants/esp32s3/hackaday-communicator/variant.h b/variants/esp32s3/hackaday-communicator/variant.h new file mode 100644 index 000000000..a127f548f --- /dev/null +++ b/variants/esp32s3/hackaday-communicator/variant.h @@ -0,0 +1,61 @@ +#define TFT_BL 2 +#define SPI_FREQUENCY 2000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 142 +#define TFT_WIDTH 428 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define SCREEN_TRANSITION_FRAMERATE 5 +#define HAS_SCREEN 1 +#define TFT_BLACK 0 +#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness +#define USE_TFTDISPLAY 1 + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +#define GPS_DEFAULT_NOT_PRESENT 1 +// #define GPS_RX_PIN 44 +// #define GPS_TX_PIN 43 + +// #define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) +// #define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +// #define ADC_CHANNEL ADC1_GPIO4_CHANNEL + +// keyboard +#define I2C_SDA 47 // I2C pins for this board +#define I2C_SCL 14 +// #define KB_POWERON -1 // must be set to HIGH +// #define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 +// #define KB_BL_PIN 46 // not used for now +#define KB_INT 13 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +#define TFT_DC 39 +#define TFT_CS 41 + +// LoRa +#define USE_SX1262 + +#define LORA_SCK 8 +#define LORA_MISO 9 +#define LORA_MOSI 3 +#define LORA_CS 17 + +// #define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 18 +#define LORA_DIO1 16 // SX1262 IRQ +#define LORA_DIO2 15 // SX1262 BUSY +// #define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#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 LED_PIN 1 \ No newline at end of file diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini index 92b90d9b9..ded0c22fe 100644 --- a/variants/esp32s3/heltec_sensor_hub/platformio.ini +++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini @@ -9,4 +9,5 @@ build_flags = -D HELTEC_SENSOR_HUB lib_deps = ${esp32s3_base.lib_deps} - adafruit/Adafruit NeoPixel @ ^1.12.0 + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 diff --git a/variants/esp32s3/heltec_v3/platformio.ini b/variants/esp32s3/heltec_v3/platformio.ini index af0854e49..2f53c8756 100644 --- a/variants/esp32s3/heltec_v3/platformio.ini +++ b/variants/esp32s3/heltec_v3/platformio.ini @@ -1,4 +1,14 @@ -[env:heltec-v3] +[env:heltec-v3] +custom_meshtastic_hw_model = 43 +custom_meshtastic_hw_model_slug = HELTEC_V3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V3 +custom_meshtastic_images = heltec-v3.svg, heltec-v3-case.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_level = pr diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 4ff7ff253..6582335af 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -7,11 +7,20 @@ build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 -I variants/esp32s3/heltec_v4 -lib_deps = - ${esp32s3_base.lib_deps} [env:heltec-v4] +custom_meshtastic_hw_model = 110 +custom_meshtastic_hw_model_slug = HELTEC_V4 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec V4 +custom_meshtastic_images = heltec_v4.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = heltec_v4_base build_flags = ${heltec_v4_base.build_flags} @@ -23,8 +32,6 @@ build_flags = -D I2C_SCL=18 -D I2C_SDA1=4 -D I2C_SCL1=3 -lib_deps = - ${heltec_v4_base.lib_deps} [env:heltec-v4-tft] extends = heltec_v4_base @@ -107,6 +114,10 @@ build_flags = lib_deps = ${heltec_v4_base.lib_deps} ; ${device-ui_base.lib_deps} + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.0 + # renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip + ; TODO revert to official device-ui (when merged) + # renovate: datasource=git-refs depName=Quency-D_device-ui packageName=https://github.com/Quency-D/device-ui gitBranch=heltec-v4-tft https://github.com/Quency-D/device-ui/archive/7c9870b8016641190b059bdd90fe16c1012a39eb.zip diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 72bbf14fc..6524bbc72 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -34,6 +34,9 @@ #define LORA_PA_EN 2 #define LORA_PA_TX_EN 46 // enable tx +#if HAS_TFT +#define USE_TFTDISPLAY 1 +#endif /* * GPS pins */ diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 43f6199af..a03755970 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-vision-master-e213] +custom_meshtastic_hw_model = 67 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E213 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master E213 +custom_meshtastic_images = heltec-vision-master-e213.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_e213 board_build.partitions = default_8MB.csv @@ -17,8 +28,8 @@ build_flags = -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] @@ -27,7 +38,7 @@ board = heltec_vision_master_e213 board_level = pr board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index 08056b639..4cc913668 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -1,5 +1,16 @@ ; Using the original screen class [env:heltec-vision-master-e290] +custom_meshtastic_hw_model = 68 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_E290 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master E290 +custom_meshtastic_images = heltec-vision-master-e290.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv @@ -20,8 +31,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/448c8538129fde3d02a7cb5e6fc81971ad92547f.zip - lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] @@ -29,7 +40,7 @@ extends = esp32s3_base, inkhud board = heltec_vision_master_e290 board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index e7e7ff4e4..bbc518b39 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-vision-master-t190] +custom_meshtastic_hw_model = 66 +custom_meshtastic_hw_model_slug = HELTEC_VISION_MASTER_T190 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Vision Master T190 +custom_meshtastic_images = heltec-vision-master-t190.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_vision_master_t190 board_build.partitions = default_8MB.csv @@ -8,6 +19,6 @@ build_flags = -D HELTEC_VISION_MASTER_T190 lib_deps = ${esp32s3_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip upload_speed = 921600 diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index f16dcd257..ac32fb219 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -1,5 +1,15 @@ ; Using the original screen class [env:heltec-wireless-paper] +custom_meshtastic_hw_model = 49 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Paper +custom_meshtastic_images = heltec-wireless-paper.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv @@ -18,8 +28,8 @@ build_flags = -D EINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/1655054ba298e0e29fc2044741940f927f9c2a43.zip - lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 [env:heltec-wireless-paper-inkhud] @@ -27,7 +37,7 @@ extends = esp32s3_base, inkhud board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index 99f2eddeb..a4a21c55c 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -1,4 +1,14 @@ [env:heltec-wireless-paper-v1_0] +custom_meshtastic_hw_model = 57 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_PAPER_V1_0 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Heltec Wireless Paper V1.0 +custom_meshtastic_images = heltec-wireless-paper-v1_0.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board_level = extra board = heltec_wifi_lora_32_V3 @@ -15,6 +25,6 @@ build_flags = -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 upload_speed = 115200 diff --git a/variants/esp32s3/heltec_wireless_tracker/platformio.ini b/variants/esp32s3/heltec_wireless_tracker/platformio.ini index 3a373bf4f..c2dab0c93 100644 --- a/variants/esp32s3/heltec_wireless_tracker/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker/platformio.ini @@ -1,4 +1,15 @@ [env:heltec-wireless-tracker] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Tracker V1.1 +custom_meshtastic_images = heltec-wireless-tracker.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -12,4 +23,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 diff --git a/variants/esp32s3/heltec_wireless_tracker/variant.h b/variants/esp32s3/heltec_wireless_tracker/variant.h index 79fa0e801..3b19f5afd 100644 --- a/variants/esp32s3/heltec_wireless_tracker/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker/variant.h @@ -27,6 +27,7 @@ #define TFT_OFFSET_Y -1 #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS +#define USE_TFTDISPLAY 1 // pin 3 is Vext on v1.1 - HIGH enables LDO for Vext rail which goes to: // GPS UC6580: GPS V_DET(8), VDD_IO(7), DCDC_IN(21), pulls up RESETN(17), D_SEL(33) and BOOT_MODE(34) through 10kR diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini index cd961533d..efeb63b8e 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/platformio.ini @@ -1,4 +1,14 @@ [env:heltec-wireless-tracker-V1-0] +custom_meshtastic_hw_model = 58 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER_V1_0 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Heltec Wireless Tracker V1.0 +custom_meshtastic_images = heltec-wireless-tracker.svg +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board_level = extra board = heltec_wireless_tracker @@ -11,4 +21,5 @@ build_flags = ;-D DEBUG_DISABLED ; uncomment this line to disable DEBUG output lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h index 876ff1146..df5ab4716 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h @@ -27,6 +27,8 @@ #define VTFT_CTRL 46 // Heltec Tracker needs this pulled low for TFT #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS +#define FORCE_LOW_RES 1 +#define USE_TFTDISPLAY 1 #define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost #define VEXT_ON_VALUE LOW diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index 0f9265f91..8bdb71541 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -10,4 +10,5 @@ build_flags = -D HELTEC_WIRELESS_TRACKER_V2 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index 9ac064ea2..0ce6b3e00 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -27,6 +27,7 @@ #define TFT_INVERT false #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS +#define USE_TFTDISPLAY 1 #define VEXT_ENABLE 3 // active HIGH - powers the GPS, GPS LNA and OLED #define VEXT_ON_VALUE HIGH diff --git a/variants/esp32s3/heltec_wsl_v3/platformio.ini b/variants/esp32s3/heltec_wsl_v3/platformio.ini index c038a463e..0903a6bc7 100644 --- a/variants/esp32s3/heltec_wsl_v3/platformio.ini +++ b/variants/esp32s3/heltec_wsl_v3/platformio.ini @@ -1,4 +1,14 @@ -[env:heltec-wsl-v3] +[env:heltec-wsl-v3] +custom_meshtastic_hw_model = 44 +custom_meshtastic_hw_model_slug = HELTEC_WSL_V3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Wireless Stick Lite V3 +custom_meshtastic_images = heltec-wsl-v3.svg +custom_meshtastic_tags = Heltec +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wifi_lora_32_V3 board_build.partitions = default_8MB.csv diff --git a/variants/esp32s3/icarus/platformio.ini b/variants/esp32s3/icarus/platformio.ini index de450da93..0aaff52f5 100644 --- a/variants/esp32s3/icarus/platformio.ini +++ b/variants/esp32s3/icarus/platformio.ini @@ -7,9 +7,6 @@ board_build.mcu = esp32s3 board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 -platform_packages = platformio/framework-arduinoespressif32@https://github.com/PowerFeather/powerfeather-meshtastic-arduino-lib/releases/download/2.0.16a/esp32-2.0.16.zip -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/link32_s3_v1/platformio.ini b/variants/esp32s3/link32_s3_v1/platformio.ini index 8d88075c4..acce3dafb 100644 --- a/variants/esp32s3/link32_s3_v1/platformio.ini +++ b/variants/esp32s3/link32_s3_v1/platformio.ini @@ -1,8 +1,9 @@ [env:link32-s3-v1] extends = esp32s3_base board = esp32-s3-devkitc-1 +board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D LINK_32 -I variants/esp32s3/link32_s3_v1 -DARDUINO_USB_CDC_ON_BOOT diff --git a/variants/esp32s3/m5stack_cores3/platformio.ini b/variants/esp32s3/m5stack_cores3/platformio.ini index 9973abfce..6ad09a8bf 100644 --- a/variants/esp32s3/m5stack_cores3/platformio.ini +++ b/variants/esp32s3/m5stack_cores3/platformio.ini @@ -6,8 +6,7 @@ board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -D M5STACK_CORES3 -I variants/esp32s3/m5stack_cores3 -lib_deps = ${esp32_base.lib_deps} diff --git a/variants/esp32s3/mesh-tab/platformio.ini b/variants/esp32s3/mesh-tab/platformio.ini index e21bc38e1..21d8fb432 100644 --- a/variants/esp32s3/mesh-tab/platformio.ini +++ b/variants/esp32s3/mesh-tab/platformio.ini @@ -46,11 +46,12 @@ build_flags = ${esp32s3_base.build_flags} -D VIEW_320x240 -D USE_PACKET_API -I variants/esp32s3/mesh-tab -build_src_filter = ${esp32_base.build_src_filter} +build_src_filter = ${esp32s3_base.build_src_filter} lib_deps = - ${esp32_base.lib_deps} + ${esp32s3_base.lib_deps} ${device-ui_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 [mesh_tab_xpt2046] extends = mesh_tab_base diff --git a/variants/esp32s3/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 63ef17d85..99204bba3 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -55,5 +55,6 @@ #define SX126X_RESET 14 #define SX126X_RXEN 47 #define SX126X_TXEN RADIOLIB_NC // Assuming that DIO2 is connected to TXEN pin +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif diff --git a/variants/esp32s3/nibble_esp32/platformio.ini b/variants/esp32s3/nibble_esp32/platformio.ini index 2f6960d2e..8e8cead16 100644 --- a/variants/esp32s3/nibble_esp32/platformio.ini +++ b/variants/esp32s3/nibble_esp32/platformio.ini @@ -3,6 +3,6 @@ extends = esp32s3_base board = esp32-s3-zero board_level = extra build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/esp32s3/nibble_esp32 diff --git a/variants/esp32s3/picomputer-s3/platformio.ini b/variants/esp32s3/picomputer-s3/platformio.ini index 4131cc30a..bef7b19a0 100644 --- a/variants/esp32s3/picomputer-s3/platformio.ini +++ b/variants/esp32s3/picomputer-s3/platformio.ini @@ -1,4 +1,12 @@ [env:picomputer-s3] +custom_meshtastic_hw_model = 52 +custom_meshtastic_hw_model_slug = PICOMPUTER_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Pi Computer S3 +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = bpi_picow_esp32_s3 board_check = true @@ -15,7 +23,8 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 build_src_filter = ${esp32s3_base.build_src_filter} diff --git a/variants/esp32s3/picomputer-s3/variant.h b/variants/esp32s3/picomputer-s3/variant.h index 8252e841c..275da1b61 100644 --- a/variants/esp32s3/picomputer-s3/variant.h +++ b/variants/esp32s3/picomputer-s3/variant.h @@ -32,6 +32,7 @@ #define ST7789_CS 6 #define ST7789_RS 1 #define ST7789_BL 5 +#define USE_TFTDISPLAY 1 #define ST7789_RESET -1 #define ST7789_MISO -1 diff --git a/variants/esp32s3/rak3312/pins_arduino.h b/variants/esp32s3/rak3312/pins_arduino.h index 15a26e991..dc5d30d61 100644 --- a/variants/esp32s3/rak3312/pins_arduino.h +++ b/variants/esp32s3/rak3312/pins_arduino.h @@ -17,6 +17,8 @@ static const uint8_t MOSI = 11; static const uint8_t MISO = 10; static const uint8_t SCK = 13; +#define SPI_INTERFACES_COUNT 1 + #define SPI_MOSI (11) #define SPI_SCK (13) #define SPI_MISO (10) @@ -25,4 +27,51 @@ static const uint8_t SCK = 13; // LEDs #define LED_BUILTIN LED_GREEN +#ifdef _VARIANT_RAK3112_ +/* + * Serial interfaces + */ +// TXD1 RXD1 on Base Board +#define PIN_SERIAL1_RX (44) +#define PIN_SERIAL1_TX (43) + +/* + * Internal SPI to LoRa transceiver + */ +#define LORA_SX126X_SCK 5 +#define LORA_SX126X_MISO 3 +#define LORA_SX126X_MOSI 6 + +/* + * Analog pins + */ +#define PIN_A0 (21) +#define PIN_A1 (14) + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 2 + +#define PIN_WIRE1_SDA (17) +#define PIN_WIRE1_SCL (18) + +/* + * GPIO's + */ +#define WB_IO1 21 +#define WB_IO2 2 +// #define WB_IO2 14 +#define WB_IO3 41 +#define WB_IO4 42 +#define WB_IO5 38 +#define WB_IO6 39 +// #define WB_SW1 35 NC +#define WB_A0 1 +#define WB_A1 2 +#define WB_CS 12 +#define WB_LED1 46 +#define WB_LED2 45 +#endif + #endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/rak3312/platformio.ini b/variants/esp32s3/rak3312/platformio.ini index 0de36498f..113c2f527 100644 --- a/variants/esp32s3/rak3312/platformio.ini +++ b/variants/esp32s3/rak3312/platformio.ini @@ -1,4 +1,15 @@ [env:rak3312] +custom_meshtastic_hw_model = 106 +custom_meshtastic_hw_model_slug = RAK3312 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK3312 +custom_meshtastic_images = rak_3312.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = false +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = wiscore_rak3312 board_level = pr @@ -6,6 +17,18 @@ board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D RAK3312 -I variants/esp32s3/rak3312 + +[env:rak3112] +extends = esp32s3_base +board = wiscore_rak3312 +board_level = extra +upload_protocol = esptool + +build_flags = + ${esp32_base.build_flags} + -D RAK3312 + -D _VARIANT_RAK3112_ + -I variants/esp32s3/rak3312 diff --git a/variants/esp32s3/rak3312/variant.h b/variants/esp32s3/rak3312/variant.h index dfdf4de71..1f8eb9e39 100644 --- a/variants/esp32s3/rak3312/variant.h +++ b/variants/esp32s3/rak3312/variant.h @@ -18,11 +18,6 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #endif -#define SX126X_POWER_EN (4) - -#define PIN_POWER_EN PIN_3V3_EN -#define PIN_3V3_EN (14) - #define LED_GREEN 46 #define LED_BLUE 45 @@ -35,10 +30,30 @@ #define LED_STATE_ON 1 // State when LED is litted +#define BATTERY_PIN 1 +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL + +#ifdef _VARIANT_RAK3112_ // Modular variant (stamp) +#define ADC_MULTIPLIER 2.11 + +#define BUTTON_NEED_PULLUP + +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +#define I2C_SDA1 PIN_WIRE1_SDA +#define I2C_SCL1 PIN_WIRE1_SCL +#else // Generic 3312 variant (40-pin standard connector) +#define ADC_MULTIPLIER 1.667 + +#define SX126X_POWER_EN (4) + +#define PIN_POWER_EN PIN_3V3_EN +#define PIN_3V3_EN (14) + #define HAS_GPS 1 #define GPS_TX_PIN 43 #define GPS_RX_PIN 44 -#define BATTERY_PIN 1 -#define ADC_CHANNEL ADC1_GPIO1_CHANNEL -#define ADC_MULTIPLIER 1.667 \ No newline at end of file +#endif \ No newline at end of file diff --git a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini index de4714efa..e4ff9812c 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini +++ b/variants/esp32s3/rak_wismesh_tap_v2/platformio.ini @@ -8,14 +8,15 @@ upload_protocol = esptool board_build.partitions = default_8MB.csv build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D RAK3312 -D RAK_WISMESH_TAP_V2 -I variants/esp32s3/rak_wismesh_tap_v2 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 [ft5x06] extends = mesh_tab_base diff --git a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini index 25ec3ebfc..70a10e0d4 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/platformio.ini +++ b/variants/esp32s3/seeed-sensecap-indicator/platformio.ini @@ -1,5 +1,16 @@ ; Seeed Studio SenseCAP Indicator [env:seeed-sensecap-indicator] +custom_meshtastic_hw_model = 70 +custom_meshtastic_hw_model_slug = SENSECAP_INDICATOR +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed SenseCAP Indicator +custom_meshtastic_images = seeed-sensecap-indicator.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_partition_scheme = 8MB + = true + extends = esp32s3_base platform_packages = platformio/framework-arduinoespressif32 @ https://github.com/mverch67/arduino-esp32/archive/aef7fef6de3329ed6f75512d46d63bba12b09bb5.zip ; add_tca9535 (based on 2.0.16) @@ -9,7 +20,7 @@ board_check = true board_build.partitions = partition-table-8MB.csv upload_protocol = esptool -build_flags = ${esp32_base.build_flags} +build_flags = ${esp32s3_base.build_flags} -Ivariants/esp32s3/seeed-sensecap-indicator -DSENSECAP_INDICATOR -DCONFIG_ARDUHAL_LOG_COLORS @@ -24,10 +35,12 @@ build_flags = ${esp32_base.build_flags} -DUSE_ARDUINO_HAL_GPIO lib_deps = ${esp32s3_base.lib_deps} + ; TODO switch back to official LovyanGFX https://github.com/mverch67/LovyanGFX/archive/4c76238c1344162a234ae917b27651af146d6fb2.zip - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 - + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:seeed-sensecap-indicator-tft] extends = env:seeed-sensecap-indicator @@ -64,4 +77,5 @@ build_flags = lib_deps = ${env:seeed-sensecap-indicator.lib_deps} ${device-ui_base.lib_deps} + ; TODO switch back to official bb_captouch https://github.com/mverch67/bb_captouch/archive/8626412fe650d808a267791c0eae6e5860c85a5d.zip ; alternative touch library supporting FT6x36 diff --git a/variants/esp32s3/seeed-sensecap-indicator/variant.h b/variants/esp32s3/seeed-sensecap-indicator/variant.h index 8915395f3..f946528ae 100644 --- a/variants/esp32s3/seeed-sensecap-indicator/variant.h +++ b/variants/esp32s3/seeed-sensecap-indicator/variant.h @@ -37,6 +37,7 @@ #define TFT_BL 45 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 // fps +#define USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT (6 | IO_EXPANDER) diff --git a/variants/esp32s3/seeed_xiao_s3/platformio.ini b/variants/esp32s3/seeed_xiao_s3/platformio.ini index ffc6e9638..b0e66241b 100644 --- a/variants/esp32s3/seeed_xiao_s3/platformio.ini +++ b/variants/esp32s3/seeed_xiao_s3/platformio.ini @@ -1,4 +1,15 @@ [env:seeed-xiao-s3] +custom_meshtastic_hw_model = 81 +custom_meshtastic_hw_model_slug = SEEED_XIAO_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed Xiao ESP32-S3 +custom_meshtastic_images = seeed-xiao-s3.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = seeed-xiao-s3 board_level = pr @@ -6,8 +17,6 @@ board_check = true board_build.partitions = default_8MB.csv upload_protocol = esptool upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini index 056d543d9..091b35f00 100755 --- a/variants/esp32s3/station-g2/platformio.ini +++ b/variants/esp32s3/station-g2/platformio.ini @@ -1,4 +1,15 @@ [env:station-g2] +custom_meshtastic_hw_model = 31 +custom_meshtastic_hw_model_slug = STATION_G2 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = Station G2 +custom_meshtastic_images = station-g2.svg +custom_meshtastic_tags = B&Q +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = station-g2 board_level = pr @@ -8,8 +19,6 @@ board_build.mcu = esp32s3 upload_protocol = esptool ;upload_port = /dev/ttyACM0 upload_speed = 921600 -lib_deps = - ${esp32s3_base.lib_deps} build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/t-beam-1w/pins_arduino.h b/variants/esp32s3/t-beam-1w/pins_arduino.h new file mode 100644 index 000000000..92b74615d --- /dev/null +++ b/variants/esp32s3/t-beam-1w/pins_arduino.h @@ -0,0 +1,25 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// I2C for OLED and sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI mapped to Radio/SD +static const uint8_t SS = 15; // LoRa CS +static const uint8_t MOSI = 11; +static const uint8_t MISO = 12; +static const uint8_t SCK = 13; + +// SD Card CS +#define SDCARD_CS 10 + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini new file mode 100644 index 000000000..54ddb6c3e --- /dev/null +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -0,0 +1,14 @@ +; LilyGo T-Beam-1W (1 Watt LoRa with external PA) +[env:t-beam-1w] +extends = esp32s3_base +board = t-beam-1w +board_build.partitions = default_8MB.csv +board_check = true + +lib_deps = + ${esp32s3_base.lib_deps} + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/t-beam-1w + -D T_BEAM_1W diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h new file mode 100644 index 000000000..01404afcb --- /dev/null +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -0,0 +1,97 @@ +// LilyGo T-Beam-1W variant.h +// Configuration based on LilyGO utilities.h and RF documentation + +// I2C for OLED display (SH1106 at 0x3C) +#define I2C_SDA 8 +#define I2C_SCL 9 + +// GPS - Quectel L76K +#define GPS_RX_PIN 5 +#define GPS_TX_PIN 6 +#define GPS_1PPS_PIN 7 +#define GPS_WAKEUP_PIN 16 // GPS_EN_PIN in LilyGO code +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 + +// Buttons +#define BUTTON_PIN 0 // BUTTON 1 +#define BUTTON_PIN_ALT 17 // BUTTON 2 + +// SPI (shared by LoRa and SD) +#define SPI_MOSI 11 +#define SPI_SCK 13 +#define SPI_MISO 12 +#define SPI_CS 10 + +// SD Card +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +// LoRa Radio - SX1262 with 1W PA +#define USE_SX1262 + +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 15 +#define LORA_RESET 3 +#define LORA_DIO1 1 +#define LORA_BUSY 38 + +// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! +// GPIO 40 powers the SX1262 + PA module via LDO +#define SX126X_POWER_EN 40 + +// TX power offset for external PA (0 = no offset, full SX1262 power) +#define TX_GAIN_LORA 10 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET + +// RF switching configuration for 1W PA module +// DIO2 controls PA (via SX126X_DIO2_AS_RF_SWITCH) +// CTRL PIN (GPIO 21) controls LNA - must be HIGH during RX +// Truth table: DIO2=1,CTRL=0 → TX (PA on, LNA off) +// DIO2=0,CTRL=1 → RX (PA off, LNA on) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_RXEN 21 // LNA enable - HIGH during RX + +// TCXO voltage - required for radio init +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SX126X_MAX_POWER 22 +#endif + +// LED +#define LED_PIN 18 +#define LED_STATE_ON 1 // HIGH = ON + +// Battery ADC +#define BATTERY_PIN 4 +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_MULTIPLIER 2.9333 + +// NTC temperature sensor +#define NTC_PIN 14 + +// Fan control +#define FAN_CTRL_PIN 41 +// Meshtastic standard fan control pin macro +#define RF95_FAN_EN FAN_CTRL_PIN + +// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) +// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U +#define SX126X_PA_RAMP_US 0x05 + +// Display - SH1106 OLED (128x64) +#define USE_SH1106 +#define OLED_WIDTH 128 +#define OLED_HEIGHT 64 + +// 32768 Hz crystal present +#define HAS_32768HZ 1 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 0b3c7843a..28fef86ba 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -1,11 +1,22 @@ [env:t-deck-pro] +custom_meshtastic_hw_model = 102 +custom_meshtastic_hw_model_slug = T_DECK_PRO +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Deck Pro +custom_meshtastic_images = tdeck_pro.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck-pro board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} -I variants/esp32s3/t-deck-pro + ${esp32s3_base.build_flags} -I variants/esp32s3/t-deck-pro -D T_DECK_PRO -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_310_GDEQ031T10 @@ -17,7 +28,11 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - https://github.com/ZinggJM/GxEPD2/archive/refs/tags/1.6.4.zip + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.4 + # 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 https://github.com/CIRCUITSTATE/CSE_CST328/archive/refs/tags/v0.0.4.zip + # renovate: datasource=git-refs depName=BQ27220 packageName=https://github.com/mverch67/BQ27220 gitBranch=main https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index 35cb99435..36a1310f1 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -100,3 +100,5 @@ #define MODEM_DTR 8 #define MODEM_RX 10 #define MODEM_TX 11 + +#define HAS_PHYSICAL_KEYBOARD 1 \ No newline at end of file diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 9ab0756d1..58335796a 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -1,5 +1,16 @@ ; LilyGo T-Deck [env:t-deck] +custom_meshtastic_hw_model = 50 +custom_meshtastic_hw_model_slug = T_DECK +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Deck +custom_meshtastic_images = t-deck.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck board_check = true @@ -12,9 +23,12 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/t-deck lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio + earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 [env:t-deck-tft] extends = env:t-deck @@ -68,4 +82,5 @@ build_flags = lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} + # renovate: datasource=github-tags depName=bb_captouch packageName=bitbank2/bb_captouch https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip diff --git a/variants/esp32s3/t-deck/variant.h b/variants/esp32s3/t-deck/variant.h index 9b0de631a..8d2996131 100644 --- a/variants/esp32s3/t-deck/variant.h +++ b/variants/esp32s3/t-deck/variant.h @@ -22,6 +22,8 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness +#define USE_TFTDISPLAY 1 +#define HAS_PHYSICAL_KEYBOARD 1 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 diff --git a/variants/esp32s3/t-eth-elite/platformio.ini b/variants/esp32s3/t-eth-elite/platformio.ini index 1a5823bc3..5ed67e977 100644 --- a/variants/esp32s3/t-eth-elite/platformio.ini +++ b/variants/esp32s3/t-eth-elite/platformio.ini @@ -15,4 +15,5 @@ lib_ignore = lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=github-tags depName=ETHClass2 packageName=meshtastic/ETHClass2 https://github.com/meshtastic/ETHClass2/archive/v1.0.0.zip diff --git a/variants/esp32s3/t-watch-s3/platformio.ini b/variants/esp32s3/t-watch-s3/platformio.ini index 59ff8891d..7d7b07ff6 100644 --- a/variants/esp32s3/t-watch-s3/platformio.ini +++ b/variants/esp32s3/t-watch-s3/platformio.ini @@ -1,21 +1,33 @@ ; LilyGo T-Watch S3 [env:t-watch-s3] +custom_meshtastic_hw_model = 51 +custom_meshtastic_hw_model_slug = T_WATCH_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = LILYGO T-Watch S3 +custom_meshtastic_images = t-watch-s3.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = t-watch-s3 board_check = true board_build.partitions = default_16MB.csv upload_protocol = esptool -build_flags = ${esp32_base.build_flags} +build_flags = ${esp32s3_base.build_flags} -DT_WATCH_S3 -Ivariants/esp32s3/t-watch-s3 - -DPCF8563_RTC=0x51 - -DHAS_BMA423=1 lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 - lewisxhe/PCF8563_Library@1.0.1 - adafruit/Adafruit DRV2605 Library@^1.2.2 - earlephilhower/ESP8266Audio@^1.9.9 - earlephilhower/ESP8266SAM@^1.0.1 - lewisxhe/SensorLib@0.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 + # 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=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM + earlephilhower/ESP8266SAM@1.1.0 diff --git a/variants/esp32s3/t-watch-s3/variant.h b/variants/esp32s3/t-watch-s3/variant.h index 578c23c0a..923ac86ce 100644 --- a/variants/esp32s3/t-watch-s3/variant.h +++ b/variants/esp32s3/t-watch-s3/variant.h @@ -18,6 +18,7 @@ #define TFT_OFFSET_ROTATION 2 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 // fps +#define USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -40,11 +41,14 @@ #define HAS_AXP2101 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 #define HAS_RTC 1 #define I2C_SDA 10 // For QMC6310 sensors and screens #define I2C_SCL 11 // For QMC6310 sensors and screens +#define HAS_BMA423 1 #define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor #define HAS_GPS 0 diff --git a/variants/esp32s3/tbeam-s3-core/platformio.ini b/variants/esp32s3/tbeam-s3-core/platformio.ini index fba8e4003..c0a32c49c 100644 --- a/variants/esp32s3/tbeam-s3-core/platformio.ini +++ b/variants/esp32s3/tbeam-s3-core/platformio.ini @@ -1,5 +1,16 @@ ; The 1.0 release of the LilyGo TBEAM-S3-Core board [env:tbeam-s3-core] +custom_meshtastic_hw_model = 12 +custom_meshtastic_hw_model_slug = LILYGO_TBEAM_S3_CORE +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam Supreme +custom_meshtastic_images = tbeam-s3-core.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = tbeam-s3-core board_build.partitions = default_8MB.csv @@ -7,9 +18,9 @@ board_check = true lib_deps = ${esp32s3_base.lib_deps} - lewisxhe/PCF8563_Library@1.0.1 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/tbeam-s3-core - -D PCF8563_RTC=0x51 ;Putting definitions in variant.h does not compile correctly diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index 1d99fbf14..1f900fcae 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -53,6 +53,8 @@ // #define PMU_IRQ 40 #define HAS_AXP2101 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 #define HAS_RTC 1 // Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus @@ -72,9 +74,6 @@ #define HAS_SDCARD // Have SPI interface SD card slot #define SDCARD_USE_SPI1 -// PCF8563 RTC Module -// #define PCF8563_RTC 0x51 //Putting definitions in variant. h does not compile correctly - // has 32768 Hz crystal #define HAS_32768HZ 1 diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index d63537904..3a7afb016 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -1,5 +1,16 @@ ; LilyGo T-Lora-Pager [env:tlora-pager] +custom_meshtastic_hw_model = 103 +custom_meshtastic_hw_model_slug = T_LORA_PAGER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-LoRa Pager +custom_meshtastic_images = lilygo-tlora-pager.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 16MB + extends = esp32s3_base board = t-deck-pro ; same as T-Deck Pro board_check = true @@ -17,14 +28,23 @@ build_flags = ${esp32s3_base.build_flags} -D ROTARY_BUXTRONICS lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.7 + # renovate: datasource=custom.pio depName=ESP8266Audio packageName=earlephilhower/library/ESP8266Audio earlephilhower/ESP8266Audio@1.9.9 + # renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM earlephilhower/ESP8266SAM@1.0.1 + # 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=PCF8563 packageName=lewisxhe/library/PCF8563_Library lewisxhe/PCF8563_Library@1.0.1 - lewisxhe/SensorLib@0.3.1 - https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 + # renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver + https://github.com/pschatzmann/arduino-audio-driver/archive/v0.1.3.zip + # TODO renovate https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + # TODO renovate https://github.com/mverch67/RotaryEncoder/archive/da958a21389cbcd485989705df602a33e092dd88.zip [env:tlora-pager-tft] diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h index 2875f6804..e6ac2c57e 100644 --- a/variants/esp32s3/tlora-pager/variant.h +++ b/variants/esp32s3/tlora-pager/variant.h @@ -20,6 +20,8 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness +#define USE_TFTDISPLAY 1 +#define HAS_PHYSICAL_KEYBOARD 1 #define I2C_SDA SDA #define I2C_SCL SCL @@ -34,11 +36,8 @@ #define GPS_TX_PIN 12 #define PIN_GPS_PPS 13 -// PCF8563 RTC Module -#if __has_include("pcf8563.h") -#include "pcf8563.h" -#endif -#define PCF8563_RTC 0x51 +// PCF85063 RTC Module +#define PCF85063_RTC 0x51 #define HAS_RTC 1 // Rotary diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index eca052f57..fdf3f7814 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -1,11 +1,21 @@ [env:tlora-t3s3-epaper] +custom_meshtastic_hw_model = 16 +custom_meshtastic_hw_model_slug = TLORA_T3_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +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 + extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} + ${esp32s3_base.build_flags} -D TLORA_T3S3_EPAPER -I variants/esp32s3/tlora_t3s3_epaper -DUSE_EINK @@ -13,10 +23,14 @@ build_flags = -DEINK_WIDTH=250 -DEINK_HEIGHT=122 -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk - -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted //20 + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates //30 + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip [env:tlora-t3s3-epaper-inkhud] @@ -25,7 +39,7 @@ board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_src_filter = - ${esp32_base.build_src_filter} + ${esp32s3_base.build_src_filter} ${inkhud.build_src_filter} build_flags = ${esp32s3_base.build_flags} diff --git a/variants/esp32s3/tlora_t3s3_v1/platformio.ini b/variants/esp32s3/tlora_t3s3_v1/platformio.ini index 56ece0d62..95686e417 100644 --- a/variants/esp32s3/tlora_t3s3_v1/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_v1/platformio.ini @@ -1,8 +1,20 @@ [env:tlora-t3s3-v1] +custom_meshtastic_hw_model = 16 +custom_meshtastic_hw_model_slug = TLORA_T3_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-LoRa T3-S3 +custom_meshtastic_images = tlora-t3s3-v1.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = true + extends = esp32s3_base board = tlora-t3s3-v1 board_check = true upload_protocol = esptool build_flags = - ${esp32_base.build_flags} -D TLORA_T3S3_V1 -I variants/esp32s3/tlora_t3s3_v1 + ${esp32s3_base.build_flags} + -D TLORA_T3S3_V1 + -I variants/esp32s3/tlora_t3s3_v1 diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index 6f75ad0e2..2287dfe0b 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -28,6 +28,7 @@ #define TFT_OFFSET_Y -1 #define SCREEN_TRANSITION_FRAMERATE 3 // fps #define DISPLAY_FORCE_SMALL_FONTS +#define USE_TFTDISPLAY 1 #define VEXT_ENABLE 3 // active HIGH, powers the lora antenna boost #define VEXT_ON_VALUE HIGH diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index 843bf3924..f42a5b19f 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -16,6 +16,7 @@ #define ST7789_CS 38 #define ST7789_RS 40 #define ST7789_BL 21 +#define USE_TFTDISPLAY 1 // P#define TFT_BL 21 /* V1.1 PCB marking */ #define ST7789_RESET -1 diff --git a/variants/esp32s3/tracksenger/platformio.ini b/variants/esp32s3/tracksenger/platformio.ini index 213a917b1..419a3539b 100644 --- a/variants/esp32s3/tracksenger/platformio.ini +++ b/variants/esp32s3/tracksenger/platformio.ini @@ -1,4 +1,13 @@ [env:tracksenger] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (small TFT) +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -12,9 +21,19 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 [env:tracksenger-lcd] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (big TFT) +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv @@ -28,9 +47,18 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@^1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.7 [env:tracksenger-oled] +custom_meshtastic_hw_model = 48 +custom_meshtastic_hw_model_slug = HELTEC_WIRELESS_TRACKER +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = TrackSenger (big OLED) +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = heltec_wireless_tracker board_build.partitions = default_8MB.csv diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index f17a27e17..28be1f3e1 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -1,6 +1,15 @@ ; platformio.ini for unphone meshtastic [env:unphone] +custom_meshtastic_hw_model = 59 +custom_meshtastic_hw_model_slug = UNPHONE +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = unPhone +custom_meshtastic_requires_dfu = true +custom_meshtastic_partition_scheme = 8MB + extends = esp32s3_base board = unphone board_build.partitions = partition-table-8MB.csv @@ -27,10 +36,12 @@ build_src_filter = +<../variants/esp32s3/unphone> lib_deps = ${esp32s3_base.lib_deps} - lovyan03/LovyanGFX@ 1.2.0 + # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.0 + # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 - adafruit/Adafruit NeoPixel @ ^1.12.0 - + # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel + adafruit/Adafruit NeoPixel@1.15.2 [env:unphone-tft] board_level = extra diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 366b49233..6f0710d62 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -36,6 +36,7 @@ #define TFT_INVERT false #define SCREEN_ROTATE true #define SCREEN_TRANSITION_FRAMERATE 5 +#define USE_TFTDISPLAY 1 #define HAS_TOUCHSCREEN 1 #define USE_XPT2046 1 diff --git a/variants/native/portduino-buildroot/platformio.ini b/variants/native/portduino-buildroot/platformio.ini index a3d0f4639..a3a779aeb 100644 --- a/variants/native/portduino-buildroot/platformio.ini +++ b/variants/native/portduino-buildroot/platformio.ini @@ -5,5 +5,4 @@ extends = portduino_base build_flags = ${portduino_base.build_flags} -O0 -I variants/native/portduino-buildroot board = buildroot board_level = extra -lib_deps = ${portduino_base.lib_deps} build_src_filter = ${portduino_base.build_src_filter} \ No newline at end of file diff --git a/variants/native/portduino-buildroot/variant.h b/variants/native/portduino-buildroot/variant.h index 3e91c6820..affd83051 100644 --- a/variants/native/portduino-buildroot/variant.h +++ b/variants/native/portduino-buildroot/variant.h @@ -1,4 +1,5 @@ #define HAS_SCREEN 1 +#define USE_TFTDISPLAY 1 #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone diff --git a/arch/portduino/portduino.ini b/variants/native/portduino.ini similarity index 89% rename from arch/portduino/portduino.ini rename to variants/native/portduino.ini index bce06f907..cc6c39aa2 100644 --- a/arch/portduino/portduino.ini +++ b/variants/native/portduino.ini @@ -21,18 +21,21 @@ build_src_filter = lib_deps = ${env.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} ${radiolib_base.lib_deps} ${environmental_base.lib_deps} # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX - lovyan03/LovyanGFX@^1.2.0 + lovyan03/LovyanGFX@1.2.7 # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library 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 build_flags = ${arduino_base.build_flags} diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 474d45492..045e3edea 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -6,7 +6,8 @@ board = cross_platform board_level = extra lib_deps = ${portduino_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 build_src_filter = ${portduino_base.build_src_filter} @@ -114,5 +115,5 @@ extends = env:native build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:native.build_flags} ; https://docs.platformio.org/en/latest/projectconf/sections/env/options/test/test_testing_command.html test_testing_command = - ${platformio.build_dir}/${this.__env__}/program + ${platformio.build_dir}/${this.__env__}/meshtasticd -s diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index af05fcf8d..972443450 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -1,6 +1,7 @@ #ifndef HAS_SCREEN #define HAS_SCREEN 1 #endif +#define USE_TFTDISPLAY 1 #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE portduino_config.maxtophone diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index 83044c206..880871e13 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -11,5 +11,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_nRF52840-pca10059-v1> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 debug_tool = jlink diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index f89b05d1f..041d3b76f 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -1,5 +1,14 @@ ; First prototype eink/nrf52840/sx1262 device [env:thinknode_m1] +custom_meshtastic_hw_model = 89 +custom_meshtastic_hw_model_slug = THINKNODE_M1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M1 +custom_meshtastic_images = thinknode_m1.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M1 board_check = true @@ -23,9 +32,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M1> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/33db3fa8ee6fc47d160bdb44f8f127c9a9203a10.zip - lewisxhe/PCF8563_Library@^1.0.1 - khoih-prog/nRF52_PWM@^1.0.1 + # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM + khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs [env:thinknode_m1-inkhud] @@ -45,4 +55,3 @@ build_src_filter = lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 \ No newline at end of file diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h index 79e31c54a..cde0f49c1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/variant.h @@ -62,17 +62,11 @@ extern "C" { /* * Buttons */ -#define PIN_BUTTON2 (32 + 10) +#define PIN_BUTTON1 (32 + 10) +#define PIN_BUTTON2 (32 + 7) #define ALT_BUTTON_PIN PIN_BUTTON2 #define ALT_BUTTON_ACTIVE_LOW true #define ALT_BUTTON_ACTIVE_PULLUP true -#define PIN_BUTTON1 (32 + 7) - -// #define PIN_BUTTON1 (0 + 11) -// #define PIN_BUTTON1 (32 + 7) - -// #define BUTTON_CLICK_MS 400 -// #define BUTTON_TOUCH_MS 200 /* * Analog pins @@ -99,8 +93,6 @@ static const uint8_t A0 = PIN_A0; #define TP_SER_IO (0 + 11) -#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC - /* External serial flash WP25R1635FZUIL0 */ @@ -157,18 +149,15 @@ External serial flash WP25R1635FZUIL0 #define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake // Seems to be missing on this new board // #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS -#define GPS_TX_PIN (32 + 9) // This is for bits going TOWARDS the CPU -#define GPS_RX_PIN (32 + 8) // This is for bits going TOWARDS the GPS +#define GPS_TX_PIN (32 + 8) // This is for bits going TOWARDS the GPS +#define GPS_RX_PIN (32 + 9) // This is for bits going TOWARDS the CPU #define GPS_THREAD_INTERVAL 50 #define PIN_GPS_SWITCH (32 + 1) // GPS开关判断 -#define PIN_SERIAL1_RX GPS_TX_PIN -#define PIN_SERIAL1_TX GPS_RX_PIN - -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN /* * SPI Interfaces @@ -196,11 +185,10 @@ External serial flash WP25R1635FZUIL0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.02F) -// #define HAS_RTC 0 // #define HAS_SCREEN 0 #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini index 958e48e48..bf9492075 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini @@ -1,8 +1,17 @@ [env:thinknode_m3] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = thinknode_m3.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M3 board_check = true debug_tool = jlink +custom_meshtastic_hw_model = 115 +custom_meshtastic_hw_model_slug = THINKNODE_M3 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Elecrow ThinkNode M3 +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/ELECROW-ThinkNode-M3 @@ -13,5 +22,7 @@ build_flags = build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M3> lib_deps = ${nrf52840_base.lib_deps} - khoih-prog/nRF52_PWM@^1.0.1 - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM + khoih-prog/nRF52_PWM@1.0.1 + ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + ; lewisxhe/SensorLib@0.3.3 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp index b7a7b7342..9769e3edd 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.cpp @@ -63,9 +63,20 @@ void initVariant() // called from main-nrf52.cpp during the cpuDeepSleep() function void variant_shutdown() { + digitalWrite(red_LED_PIN, HIGH); + digitalWrite(green_LED_PIN, HIGH); + digitalWrite(LED_BLUE, HIGH); + + digitalWrite(PIN_EN1, LOW); + digitalWrite(PIN_EN2, LOW); digitalWrite(EEPROM_POWER, LOW); digitalWrite(KEY_POWER, LOW); + digitalWrite(DHT_POWER, LOW); + digitalWrite(ACC_POWER, LOW); + digitalWrite(Battery_POWER, LOW); + digitalWrite(GPS_POWER, LOW); + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. for (int pin = 0; pin < 48; pin++) { if (pin == PIN_POWER_USB || pin == BUTTON_PIN || pin == PIN_EN1 || pin == PIN_EN2 || pin == DHT_POWER || pin == ACC_POWER || pin == Battery_POWER || pin == GPS_POWER || pin == LR1110_SPI_MISO_PIN || diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index cf940172b..29a6c85fd 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -54,6 +54,7 @@ extern "C" { #define LED_POWER red_LED_PIN #define LED_CHARGE LED_POWER // Signals the Status LED Module to handle this LED #define green_LED_PIN 35 +#define PIN_LED2 green_LED_PIN #define LED_BLUE 37 #define LED_PAIRING LED_BLUE // Signals the Status LED Module to handle this LED @@ -78,11 +79,11 @@ extern "C" { #define GPS_BAUDRATE 9600 #define PIN_GPS_RESET 25 #define PIN_GPS_STANDBY 21 -#define GPS_TX_PIN 20 -#define GPS_RX_PIN 22 +#define GPS_TX_PIN 22 +#define GPS_RX_PIN 20 #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_RX GPS_TX_PIN -#define PIN_SERIAL1_TX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN // Button #define BUTTON_PIN 12 #define BUTTON_PIN_ALT (0 + 12) @@ -113,7 +114,8 @@ extern "C" { #define LR11X0_DIO_AS_RF_SWITCH // PCF8563 RTC Module -#define PCF8563_RTC 0x51 +// REVISIT https://github.com/meshtastic/firmware/pull/9084 +// #define PCF8563_RTC 0x51 #ifdef __cplusplus } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini index 2bf227791..413eb4fab 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini @@ -1,5 +1,14 @@ ; ThinkNode M6 - Outdoor Solar Power nrf52840/sx1262 device [env:thinknode_m6] +custom_meshtastic_hw_model = 120 +custom_meshtastic_hw_model_slug = THINKNODE_M6 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M6 +custom_meshtastic_images = thinknode_m6.svg +custom_meshtastic_tags = Elecrow + extends = nrf52840_base board = ThinkNode-M6 board_check = true @@ -12,4 +21,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW-ThinkNode-M6> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + ; lewisxhe/SensorLib@0.3.3 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index 09872d409..9c7b521ef 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -41,3 +41,30 @@ void initVariant() pinMode(VDD_FLASH_EN, OUTPUT); digitalWrite(VDD_FLASH_EN, HIGH); } + +// called from main-nrf52.cpp during the cpuDeepSleep() function +void variant_shutdown() +{ + // This sets the pin to OUTPUT and LOW for the pins *not* in the if block. + for (int pin = 0; pin < 48; pin++) { + if (pin == PIN_GPS_EN || pin == ADC_CTRL || pin == PIN_BUTTON1 || pin == PIN_SPI_MISO || pin == PIN_SPI_MOSI || + pin == PIN_SPI_SCK) { + continue; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + if (pin >= 32) { + NRF_P1->DIRCLR = (1 << (pin - 32)); + } else { + NRF_GPIO->DIRCLR = (1 << pin); + } + } + + digitalWrite(PIN_GPS_EN, LOW); + digitalWrite(ADC_CTRL, LOW); + // digitalWrite(RTC_POWER, LOW); + + nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input + nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW; + nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index 5e543b21f..984f967d8 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -44,8 +44,10 @@ extern "C" { #define LED_BLUE -1 #define LED_CHARGE (12) #define LED_PAIRING (7) +#define PIN_LED2 LED_PAIRING -#define LED_STATE_ON 1 +#define LED_STATE_ON HIGH +#define LED_STATE_OFF LOW // USB power detection #define EXT_PWR_DETECT (13) @@ -107,19 +109,20 @@ static const uint8_t A0 = PIN_A0; #define PIN_GPS_REINIT (29) #define PIN_GPS_STANDBY (30) #define PIN_GPS_PPS (31) -#define GPS_TX_PIN (3) -#define GPS_RX_PIN (2) +#define GPS_TX_PIN (2) +#define GPS_RX_PIN (3) #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_RX GPS_TX_PIN -#define PIN_SERIAL1_TX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN // Secondary UART #define PIN_SERIAL2_RX (22) #define PIN_SERIAL2_TX (24) // PCF8563 RTC Module -#define PCF8563_RTC 0x51 +// REVISIT https://github.com/meshtastic/firmware/pull/9084 +// #define PCF8563_RTC 0x51 // SPI #define SPI_INTERFACES_COUNT 1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini index 1279f12c6..ef2e6e517 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD/platformio.ini @@ -10,8 +10,6 @@ build_flags = ${nrf52840_base.build_flags} -DME25LS01_4Y10TD board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD> -lib_deps = - ${nrf52840_base.lib_deps} ; 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/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index f8d6da008..025e28078 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -15,7 +15,8 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS01-4Y10TD_e-ink> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 ; 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/MS24SF1/platformio.ini b/variants/nrf52840/MS24SF1/platformio.ini index df15b5605..1cbb3a5a5 100644 --- a/variants/nrf52840/MS24SF1/platformio.ini +++ b/variants/nrf52840/MS24SF1/platformio.ini @@ -9,8 +9,6 @@ build_flags = ${nrf52840_base.build_flags} -Isrc/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MS24SF1> -lib_deps = - ${nrf52840_base.lib_deps} ; 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 50e5495f0..d823a7230 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -12,7 +12,9 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_eink> lib_deps = ${nrf52840_base.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 - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini b/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini index c7418e53c..16cef8964 100644 --- a/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_oled/platformio.ini @@ -8,5 +8,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/MakePython_nRF52840_oled> lib_deps = ${nrf52840_base.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 debug_tool = jlink diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index 77aeee26e..e196029ec 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -8,5 +8,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mesh_v4> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 debug_tool = jlink diff --git a/variants/nrf52840/canaryone/platformio.ini b/variants/nrf52840/canaryone/platformio.ini index 251937e9c..a2cf55972 100644 --- a/variants/nrf52840/canaryone/platformio.ini +++ b/variants/nrf52840/canaryone/platformio.ini @@ -1,5 +1,13 @@ ; Public Beta oled/nrf52840/sx1262 device [env:canaryone] +custom_meshtastic_hw_model = 29 +custom_meshtastic_hw_model_slug = CANARYONE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Canary One +custom_meshtastic_tags = Canary + extends = nrf52840_base board = canaryone debug_tool = jlink @@ -11,5 +19,4 @@ build_flags = build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/canaryone> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 ;upload_protocol = fs diff --git a/variants/nrf52840/canaryone/variant.h b/variants/nrf52840/canaryone/variant.h index 836fa74a3..61d1e8df9 100644 --- a/variants/nrf52840/canaryone/variant.h +++ b/variants/nrf52840/canaryone/variant.h @@ -103,7 +103,7 @@ static const uint8_t A0 = PIN_A0; #define EXTERNAL_FLASH_USE_QSPI // Add a delay on startup to allow LoRa and GPS to power up -#define PIN_PWR_DELAY_MS 100 +#define PERIPHERAL_WARMUP_MS 100 /* * Lora radio @@ -128,13 +128,13 @@ static const uint8_t A0 = PIN_A0; // #define PIN_GPS_WAKE (GPIO_PORT1 + 2) // An output to wake GPS, low means allow sleep, high means force wake // Seems to be missing on this new board #define PIN_GPS_PPS (GPIO_PORT1 + 4) // Pulse per second input from the GPS -#define GPS_TX_PIN (GPIO_PORT1 + 9) // This is for bits going TOWARDS the CPU -#define GPS_RX_PIN (GPIO_PORT1 + 8) // This is for bits going TOWARDS the GPS +#define GPS_TX_PIN (GPIO_PORT1 + 8) // This is for bits going TOWARDS the GPS +#define GPS_RX_PIN (GPIO_PORT1 + 9) // This is for bits going TOWARDS the CPU #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_RX GPS_TX_PIN -#define PIN_SERIAL1_TX GPS_RX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN #define GPS_RESET_PIN (GPIO_PORT1 + 5) // GPS reset pin @@ -178,4 +178,4 @@ static const uint8_t A0 = PIN_A0; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/arch/nrf52/cpp_overrides/lfs_util.h b/variants/nrf52840/cpp_overrides/lfs_util.h similarity index 100% rename from arch/nrf52/cpp_overrides/lfs_util.h rename to variants/nrf52840/cpp_overrides/lfs_util.h diff --git a/variants/nrf52840/diy/WashTastic/platformio.ini b/variants/nrf52840/diy/WashTastic/platformio.ini index 881b961e1..0e67f72ad 100644 --- a/variants/nrf52840/diy/WashTastic/platformio.ini +++ b/variants/nrf52840/diy/WashTastic/platformio.ini @@ -8,6 +8,4 @@ build_flags = ${nrf52840_base.build_flags} -D PRIVATE_HW -D EBYTE_E22_900M30S build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-Micro_Pinouts.pdf b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-micro_Pinouts_2025-12-04.pdf similarity index 82% rename from variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-Micro_Pinouts.pdf rename to variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-micro_Pinouts_2025-12-04.pdf index 63a80dbbe..6fb9c11c6 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-Micro_Pinouts.pdf +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/Schematic_Pro-micro_Pinouts_2025-12-04.pdf @@ -10,7 +10,7 @@ endobj 4 0 obj << -/Length 334350 +/Length 339732 >> stream 0.14 w @@ -364,14 +364,6 @@ BT /F2 9.818181818181817 Tf 10.80 TL 0.000 0.000 0.502 rg -730.08 129.04 Td -(2025-11-08) Tj -ET -7.20 w -BT -/F2 9.818181818181817 Tf -10.80 TL -0.000 0.000 0.502 rg 730.08 114.64 Td (2025-11-07) Tj ET @@ -4753,64 +4745,6 @@ ET 0.63 0.00 0.00 RG 0.00 g [] 0 d -370.080 1068.480 m -366.480 1064.880 l -355.680 1064.880 l -355.680 1072.080 l -366.480 1072.080 l -370.080 1068.480 l -S -1 J -1 j -0.72 w -0.63 0.00 0.00 RG -[] 0 d -370.080 1068.480 m -370.080 1068.480 l -S -7.20 w -BT -/F2 6.545454545454544 Tf -7.20 TL -0.000 0.000 1.000 rg -332.05 1065.61 Td -(GPSTX) Tj -ET -1 J -1 j -0.72 w -0.63 0.00 0.00 RG -0.00 g -[] 0 d -370.080 1075.680 m -366.480 1072.080 l -355.680 1072.080 l -355.680 1079.280 l -366.480 1079.280 l -370.080 1075.680 l -S -1 J -1 j -0.72 w -0.63 0.00 0.00 RG -[] 0 d -370.080 1075.680 m -370.080 1075.680 l -S -7.20 w -BT -/F2 6.545454545454544 Tf -7.20 TL -0.000 0.000 1.000 rg -331.32 1072.81 Td -(GPSRX) Tj -ET -1 J -1 j -0.72 w -0.63 0.00 0.00 RG -0.00 g -[] 0 d 370.080 1061.280 m 366.480 1057.680 l 355.680 1057.680 l @@ -16535,27 +16469,27 @@ ET 0.63 0.00 0.00 RG 0.00 g [] 0 d -85.680 1075.680 m +85.680 1068.480 m 82.080 1072.080 l 71.280 1072.080 l -71.280 1079.280 l -82.080 1079.280 l -85.680 1075.680 l +71.280 1064.880 l +82.080 1064.880 l +85.680 1068.480 l S 1 J 1 j 0.72 w 0.63 0.00 0.00 RG [] 0 d -85.680 1075.680 m -85.680 1075.680 l +85.680 1068.480 m +85.680 1068.480 l S 7.20 w BT /F2 6.545454545454544 Tf 7.20 TL 0.000 0.000 1.000 rg -46.92 1072.81 Td +46.92 1066.77 Td (GPSRX) Tj ET 1 J @@ -16564,27 +16498,27 @@ ET 0.63 0.00 0.00 RG 0.00 g [] 0 d -85.680 1068.480 m -82.080 1064.880 l -71.280 1064.880 l +85.680 1075.680 m +82.080 1079.280 l +71.280 1079.280 l 71.280 1072.080 l 82.080 1072.080 l -85.680 1068.480 l +85.680 1075.680 l S 1 J 1 j 0.72 w 0.63 0.00 0.00 RG [] 0 d -85.680 1068.480 m -85.680 1068.480 l +85.680 1075.680 m +85.680 1075.680 l S 7.20 w BT /F2 6.545454545454544 Tf 7.20 TL 0.000 0.000 1.000 rg -47.65 1065.61 Td +47.65 1073.97 Td (GPSTX) Tj ET 1 J @@ -17606,7 +17540,7 @@ ET 0 j 72 M 0.72 w -0.00 G +0.63 0.00 0.00 RG [] 0 d 35.28 476.64 777.60 -208.80 re S @@ -19796,42 +19730,6 @@ S 0.00 0.53 0.00 RG 0.00 g [] 0 d -85.680 1075.680 m -96.480 1075.680 l -S -1 J -1 j -0.72 w -0.00 0.53 0.00 RG -0.00 g -[] 0 d -85.680 1075.680 m -96.480 1075.680 l -S -1 J -1 j -0.72 w -0.00 0.53 0.00 RG -0.00 g -[] 0 d -85.680 1068.480 m -96.480 1068.480 l -S -1 J -1 j -0.72 w -0.00 0.53 0.00 RG -0.00 g -[] 0 d -85.680 1068.480 m -96.480 1068.480 l -S -1 J -1 j -0.72 w -0.00 0.53 0.00 RG -0.00 g -[] 0 d 85.680 1061.280 m 96.480 1061.280 l S @@ -27766,6 +27664,556 @@ BT 672.48 619.79 Td (E22P-915M30S) Tj ET +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +96.480 1075.680 m +85.680 1075.680 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +96.480 1075.680 m +85.680 1075.680 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +96.480 1068.480 m +85.680 1068.480 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +96.480 1068.480 m +85.680 1068.480 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +370.080 1068.480 m +366.480 1072.080 l +355.680 1072.080 l +355.680 1064.880 l +366.480 1064.880 l +370.080 1068.480 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +370.080 1068.480 m +370.080 1068.480 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +331.32 1066.77 Td +(GPSRX) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +370.080 1075.680 m +366.480 1079.280 l +355.680 1079.280 l +355.680 1072.080 l +366.480 1072.080 l +370.080 1075.680 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +370.080 1075.680 m +370.080 1075.680 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +332.05 1073.97 Td +(GPSTX) Tj +ET +7.20 w +BT +/F2 8.181818181818182 Tf +9.00 TL +0.000 g +150.48 112.52 Td +(Example GNSS) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +226.080 60.480 m +229.680 56.880 l +240.480 56.880 l +240.480 64.080 l +229.680 64.080 l +226.080 60.480 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 60.480 m +226.080 60.480 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +241.92 57.61 Td +(GPSRX) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +226.080 67.680 m +229.680 64.080 l +240.480 64.080 l +240.480 71.280 l +229.680 71.280 l +226.080 67.680 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 67.680 m +226.080 67.680 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +241.92 64.81 Td +(GPSTX) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +229.680 85.680 m +229.680 78.480 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +226.080 85.680 m +233.280 85.680 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +229.680 78.480 m +229.680 78.480 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 233.03 86.40 Tm +(VCC) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +229.680 42.480 m +229.680 49.680 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +223.200 42.480 m +236.160 42.480 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +225.360 41.040 m +234.000 41.040 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +227.520 39.600 m +231.840 39.600 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +0.00 g +[] 0 d +228.960 38.160 m +230.400 38.160 l +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +229.680 49.680 m +229.680 49.680 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 0.000 1.000 rg +0.00 1.00 -1.00 0.00 230.91 23.26 Tm +(GND) Tj +ET +7.20 w +BT +/F2 6.363634909090909 Tf +7.00 TL +0.627 0.000 0.000 rg +150.93 28.72 Td +(NEO-6M) Tj +ET +2 J +0 j +72 M +0.72 w +0.63 0.00 0.00 RG +[] 0 d +144.72 84.24 33.12 -43.20 re +S +q +1 0 0 1 177.84 51.12 cm +-0.0000 -1.0000 1.0000 -0.0000 0 0 cm +1 0 0 1 -33.12 -33.12 cm +43.20 0 0 33.12 0 0 cm +/I1 Do +Q +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +210.24 28.08 m 210.24 30.07 208.63 31.68 206.64 31.68 c +204.65 31.68 203.04 30.07 203.04 28.08 c +203.04 26.09 204.65 24.48 206.64 24.48 c +208.63 24.48 210.24 26.09 210.24 28.08 c +S +2 J +0 j +72 M +0.72 w +0.63 0.00 0.00 RG +[] 0 d +110.88 102.24 100.80 -79.20 re +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +210.24 97.20 m 210.24 99.19 208.63 100.80 206.64 100.80 c +204.65 100.80 203.04 99.19 203.04 97.20 c +203.04 95.21 204.65 93.60 206.64 93.60 c +208.63 93.60 210.24 95.21 210.24 97.20 c +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +119.52 28.08 m 119.52 30.07 117.91 31.68 115.92 31.68 c +113.93 31.68 112.32 30.07 112.32 28.08 c +112.32 26.09 113.93 24.48 115.92 24.48 c +117.91 24.48 119.52 26.09 119.52 28.08 c +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +119.52 97.20 m 119.52 99.19 117.91 100.80 115.92 100.80 c +113.93 100.80 112.32 99.19 112.32 97.20 c +112.32 95.21 113.93 93.60 115.92 93.60 c +117.91 93.60 119.52 95.21 119.52 97.20 c +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +119.88 36.00 m 119.88 36.99 119.07 37.80 118.08 37.80 c +117.09 37.80 116.28 36.99 116.28 36.00 c +116.28 35.01 117.09 34.20 118.08 34.20 c +119.07 34.20 119.88 35.01 119.88 36.00 c +S +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +200.16 95.04 m 200.16 97.43 198.23 99.36 195.84 99.36 c +193.45 99.36 191.52 97.43 191.52 95.04 c +191.52 92.65 193.45 90.72 195.84 90.72 c +198.23 90.72 200.16 92.65 200.16 95.04 c +S +7.20 w +BT +/F2 3.6363665454545444 Tf +4.00 TL +0.627 0.000 0.000 rg +178.56 94.04 Td +(Battery) Tj +ET +7.20 w +BT +/F2 3.6363665454545444 Tf +4.00 TL +0.627 0.000 0.000 rg +0.00 1.00 -1.00 0.00 119.00 39.48 Tm +(Antenna) Tj +ET +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +194.98 50.99 Td +(GND) Tj +ET +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +215.28 54.59 Td +(1) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 53.280 m +211.680 53.280 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +201.15 58.19 Td +(TX) Tj +ET +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +215.28 61.79 Td +(2) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 60.480 m +211.680 60.480 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +200.43 65.39 Td +(RX) Tj +ET +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +215.28 68.99 Td +(3) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 67.680 m +211.680 67.680 l +S +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +1.000 0.000 0.000 rg +195.70 72.59 Td +(VCC) Tj +ET +7.20 w +BT +/F2 6.545454545454544 Tf +7.20 TL +0.000 g +215.28 76.19 Td +(4) Tj +ET +1 J +1 j +0.72 w +0.63 0.00 0.00 RG +[] 0 d +226.080 74.880 m +211.680 74.880 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +229.680 78.480 m +229.680 74.880 l +S +229.680 74.880 m +226.080 74.880 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +229.680 78.480 m +229.680 74.880 l +S +229.680 74.880 m +226.080 74.880 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +229.680 49.680 m +229.680 53.280 l +S +229.680 53.280 m +226.080 53.280 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +229.680 49.680 m +229.680 53.280 l +S +229.680 53.280 m +226.080 53.280 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +226.080 67.680 m +226.080 67.680 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +226.080 67.680 m +226.080 67.680 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +226.080 60.480 m +226.080 60.480 l +S +1 J +1 j +0.72 w +0.00 0.53 0.00 RG +0.00 g +[] 0 d +226.080 60.480 m +226.080 60.480 l +S +7.20 w +BT +/F2 10.307802433786685 Tf +11.34 TL +0.000 g +730.08 127.34 Td +(2025-12-04) Tj +ET 0.80 0.00 0.00 rg 656.28 233.28 m 656.28 234.27 655.47 235.08 654.48 235.08 c 653.49 235.08 652.68 234.27 652.68 233.28 c @@ -28290,6 +28738,294 @@ stream xíÔ€ ú?º†#Ø j]€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„ Âa€0@˜ L&„== endstream endobj +12 0 obj +<< +/Type /XObject +/Subtype /Image +/Width 645 +/Height 455 +/ColorSpace /DeviceRGB +/BitsPerComponent 8 +/Length 67274 +/Filter /DCTDecode +>> +stream +ÿØÿàJFIFHHÿá¤ExifMM* +† Œ–ž(1¦2®BC‡iÂAppleiPhone 8HH11.2.62018:03:08 17:52:25‚š<‚�Dˆ"ˆ'd�0221�L�`‘’ +t’|’ +„’ +Œ’’ ’ +”’œ’|°¤’‘884’’884 0100 … Ç¢£¤¤¤¤¤2T¤3t¤4"z 2018:03:08 17:52:252018:03:08 17:52:25 oùÿÿö×o�dßç©2Apple iOSMM  .ìh  ¥ ¢  +‚ +š        ª bplist00O%&*07:33-0)+"ü ##&()ïøÿ ! + äìõó÷ýøø÷ÝãçæÝ�+NO‹àìñûüÐÜÚ×Â_�D@^´áææ÷ùM·Îζ?ntHqèÖÙãèð s¼®@ñY]²ÇÍ×àß4Šb j¦ Œ½ÄÌÑÑ M<!º#żÁÇÈÉ 4Ú% —ͱ·»½¾"'Mƒ¯­­²¸½!"/<Cs¡¯°µº#Ew«2()‰®±±'" N¤°p bplist00ÔUflagsUvalueYtimescaleUepoch£|¤3”;šÊ'-/8= ?ÿÿþ¯`MÍmeÿÿÝy!ÐÓw€q825s�d�d  AppleiPhone 8 back camera 3.99mm f/1.8ÿá +http://ns.adobe.com/xap/1.0/ ÿí8Photoshop 3.08BIM8BIM%ÔŒÙ�²é€ ˜ìøB~ÿâ4ICC_PROFILE$applmntrRGB XYZ á  acspAPPLAPPLöÖÓ-applÊ•‚%M8™ÕÑê‚ +descüecprtd#wtptˆrXYZœgXYZ°bXYZÄrTRCØ chadø,bTRCØ gTRCØ desc Display P3textCopyright Apple Inc., 2017XYZ óQÌXYZ ƒß=¿ÿÿÿ»XYZ J¿±7 +¹XYZ (8 ȹparaffò§ YÐ +[sf32 BÞÿÿó&“ý�ÿÿû¢ÿÿý£ÜÀnÿÀÇ…"ÿÄ + ÿĵ}!1AQa"q2�‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2�B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÛC ÿÛC  ÿÝ)ÿÚ ?þ·c¯Ø·À?ü jfÓ×í²"HLƒ,¤ŒŸ¥}ý ��¢ˆà‰Tc°o^(Xÿüß…Ilz±2“ÕŒWe»ÆsRù~Ô˜¥uò3 ‘çÒîzTø­8FiÆ›eE®¥cŽÃÆa�½E^òÿÏù5 Âç•✨³E$UÉ +™Jƒó©¼³Œ@™¨öÎBÒøà +pL–�‹œ‘C¢gq Ñj ƒ¸ÅXwì֡ѹQ›E]‹Iå¯ùÿõÕ‚ƒµ7Ë?çÿ×Qì~Õ‘¤k¸ŸëVj5LÔ•¥:™Tl)ihª•O;TE8RÑYÂØ®­&sKEoÈ™7 +(¢Ÿ"0)»O­>ŠÍ­KR]HAjªñàŽi÷×öºt~mÃüWÇ?¿lï„ÿ +wA®êQE"g)‘»�A\®”¤ívP„¥ª>¸uvásR‚À‚IÈæ¿!£ÿ‚±|ŠìFÚŒ^I#œøÏ=+íoƒÿ¶'Á/ŒÒ-Ÿ„µ«iî[þY‡Ÿ×"º>«4®Ñ×*Rz#ê„rXóS”Ën&’ÞHæ]é‚=EIC‰åÔm1ËØ-MP§Þ©ª¡Ì®ÃQES¤‚ì(¢Š=’ ±0}ih¢§Ø¥°s1¥rsJsÚ–ŠËêè9˜Ò=iÔQZBŠìÌhPîô¸¥¢©Ñ�`æb}hæ–Š=’[Äù©h¤$(Éè* Ũe¸†õ¬êkæÏÚ ö¢øcðÂóëÞ/Ôà·xÔ²Æî0œ§ð¯å?ö¬ÿ‚ôøŸWñÖ�ð^»‡æPß0ùI 2+|>]R²º=*=^‡ö¯|EðŸ‡`3j×IÁ<‘Ð~5ävÿµ'ÂYnc·MBÒp£räŸ~køñí/ÿ#ý¨ï è×wÖV�¡!(Ï~µ¯oðþ +e,züz…Ü�·Ÿ™ÉÚº§’B?•ÏF–_¬ÿBÍâ—‚|C2ÛØ]£9ã÷ú×ö’�°ÈûWùøøöŠý¼gÝE¢ÕäÔ.Ê€P‘ƒõ¯ØÙƒþ +ÿ¬Þêv:GÅ8M�èP“G—¼ç·®q<ªJîâ«–+{¬þ¥ðwgµ:¼;á?Æï üQÒ!Ôô{ˆåYTPç­{‚²°ÊœŠÊðjÒpvc²i(¢®R¹‘ v4|ØÍMEH¿yÙ©Ê$#žj@ƒ½sÞ ñV‰á›SuªÎ‘*÷f¦�.cZps÷b�òª:šË¸Õ,lŽé¥}s_•ÿ´§ü'à�ÁËÙtDÕ –uFÌhÁ›#ŽƒÞ¿¼ÿuøÁñ$ÉiðFž52ùÒ�Ã9ùpvÓÉêTÕh{xl¦M^Gõ™}ñÂö„÷Q‚§^c®~Ò ´2ë}{~XËêöæ¿“� jðPoŽ—æx.o£20r"Ê€9Ïô¯K¸ÿ‚x~ÖïT×®/^sþ·{ã‘ߨäû +sÊ©Çã™ßõ +1ø™ý-Û~Öß îå�lïc3¿˜„í]m—íà[¢ª÷1©sÆ]Æ¿—øv?í¤¤rÛKxfFɾïÐç½yÄÙoöÉøn[TÒ/¯!Ž,àJäœW4rú2ûdKIìÏì§Nø‰á}MÕ­¥7|Œsï]¬7–Ó°¸9÷¯à§Ã_¶�í[ð7ÄÚø’æêê8!� é_ªÿ³ßü‡E×õK-⾕:íGiÎçøóÓ¤òZëXjrUÁ%±ýD:å2ëPÇQ_/üý£¼#ñcOK­ú+ˆß’Êà�Ï5õeeMñœ�­s¸Éi#Š­' ÄÚzLÚ­F¬F~aTäš1Um±]ƒ“RRÛÎ3KPíЊ’¸QIÍ5>v.vÿÐþú)é×4ÁRGÞ¾‘ÑËf=º‚§a‘Šˆ©Ö“µb íšŠIã‰KÈÁ@õ¬ÝEXÔ[Ñ(®[Pñ^�¦Çæ]\"÷倪x÷ÃS¨e»NÚ³XúODÎøeX™Gž4Û^‡qLóùÿõVU¾¯auþ¦`Ù÷­pÁ†VºaQKcŽ¥)EÚJÃ|ÁþýUX¨¶ו™64rqR„šhCœš¨NÍC]Á]’Raih¦10* +±Q|€íb3éUÜ™_ Ñɧymþýtíƒ9úS§Üq¿R/-¿Ïÿ®�-¿Ïÿ®¥¢³TÐȼ¶ÿ?þº–Š*’°QLÙ;Š‚C…ÝéVŒb¹ßjcFÐ.õR»¼ˆ™ñþè&¹«"é«É#ñ þ +…û~Ið?ø@¼*âmVë ÄyyïÇjþA¾/~ÓŸ¼q㻫¯]ÝMw,üÄ»„`Àöâ¿I|}âcûUÁIn<â76ñÜnù�kúW;û|ø á—ìãûFiZ™l�cwåµÄ²(cž=:W«G–’äk[\údÔ’,x áw…5oYË«Ê!º¹€3±l2î³^¬ÂËý”|ogã Þ\Iažh*ø=x#Ûñ¯±¾9üHø£ü?Ò¢ð ì3jRP“É#¨ÇoJúÛIJ鸉ûKãr/9ã¶óVVUÈþÉàÔ}g•ÅÔVLÊë3öOþ ½ûfYþÕ? +¢½Õ×W´TYQñó cw¾kô׿�ø ß‹¼A¥|y¼ðD·j`O1zîàí_Ù}sU¢£RQ<œÎ*I �NÈôÿ?�%¹Qæ{F.G§ùüér¾”Ú(²hÇe}(ÊúSh¢È=£å£å¤¢‹ öŒwËGËïM¢‹ öŒwËïIòÒQG*´å£å¤¢—"h;hõ €)QN~jeq‡«ã¿Û7ö¢ð÷ìÁð®ëƤè—CœnlqŠúîîæ;;Y.¦8XÔ±>¿Šÿø,ïí5¬|lø×eðKÛî¡I*‘¶J»6TzмÎwžÈôòì?3ç{#ñ¿öêý¼>,þÔž-¾ñ­<–öi#¥¼�Ý=Hì~€Áÿ`ü|½¶ñÏŒÈxˆRCC ÷Î+Ìÿh?ø&v«ð�à·ÄýuâY!g–%?¼@FBªã–ú×ÿÌÿ‚…ë¼Eià«ÖštO´Hçn�æîÕïb*ª´Ùt=YNM{¬þç¾þÆ_¾ÚÁ…£Û«"`•Q†¯q_…>ÿ�y4¸c€q�Ê¿>ÿfÿø)GÁ�Ž—0èÚ®’ÝÆB2“´ƒéïõ¯³~4|vðï¯Kâ­NdH!PÍ&~\øú”eÍï-N.Y^×3¢„!†§w¹ùWû)ÿÁ|cñOøN~.Ë9IÝY„äïaÔýâN+÷«áüWö}ø[n«—Çhqž�ëôVxmâXmÔ"(À��¥U¾¿±Óâ2ݰP9¯3Ž«7y3Ë–>sz‡†þx3ÂùZŒP`cåP+«0Z€F£·Ý‹Œ¼9!Σ>¦¶¡¾´¾�Ì´mãÔWŸ*’}L}î£ÖÒÙÇÌŠ\·Š¼á}gN—ûZÊ@S˨?Ò»zâ¿'?à¨_·•�ì­ðâãLðó¤ÚÅÌeU ÊO·^;ñUF“›²F¸hÎu9bͯˆŸ±·ÀoŠâãM|Ë!Éd†¯ÇÚŸþ ªÛ}£[ðLQ–Øv<`ü›{`z×±ÿÁ"j?ˆß´¯Šµkývbßfuf�œõÅI馨\[y71«‡ääf»iã*áçdÏFS�'ï;ŸÂÁOŠ?ÿbo®•â‘-L23 +çÒ¿­ÿØÿö´ð·ÇŸ Ú\ÛN>Ñ"èÄ|¬8'×—þܰD޾5x2ïXÐí"ƒT³‰ÝPe€+ÇZþi¿f¯�¿?cßÚ~ø„„·–}ƒÌ%@¿„c¯­zõyqtùÒ´‘ßÉ E7mÏî`èjMŠËÍy§Â¿ØøÏÂZÅŒ«"Ï +È9ê+Ó¼±þýuó‡ËV§É.VUÙÎJ_, +³å­ZÿŸÿ]\è«´ŸÿUGùÿõU¯-Ïÿ®�-Ïÿ®€çGÿÑþúŠ•ëN�½4±$J@Ät¯ˆæI�M“ÓI BÎqÉ®_Ö¡Ñl^òc…E'šÎ¾6ãÍ#\>Uf©ÁjÌÿø»Mðž˜÷×ò*äúWæOÆÛ£LÒm3Ae’AÆòp¼SöÑý¬m£†}Æð"&C�{z +ü)ñßÅMgÅW�6˜Ì–àrq’ø¯æî?ñ:tjÊŽZŸéÇÑ£è{ à Ï;ƒ³Õ'·Ìýñ÷í¡ãK›™†¥ª  å##ÕãVß·µÓ ~cŒ îÿëWÄ�|?ñ¦½ Õ`Ó.®"Øeó6œçéÒ¸ï"(›j¦ÃÓi6AÁ¯Ã±¦×õTÑ´[�QÎ1³~B¿™OŠßð[;~Òÿð¬,×í� ’:¶Ò¾{Wô�ñ ¹ðmú³|¢$û`×ùlþÒš”ÒÿÁI¯m-Ý£t׃g„*Û�þ|b½Œž”+96¶:°#(´Ñþ¥? üV¾7ð>™â”C½·Yv“’ õ®ÝA'5àÿ³:y?¼0å·4–1;v'Å�Ú7á?Á�*çRñƱoiötgewáFN}+Ç�^gÊ‘ÁS)T´Qï4WãFµÿ�ý–4«û{D×-eWùŽçiéÉÀ'Ú¾°øû~|øó2ÚxcW‰f�3 ²‚î¼ �=kz”*E^Q7ž]V*ö>碹ÍcÄ6Z6“&¯;æ8×~G§­~uÝÁP¿g­+Ä÷^×/ŽͬÂ#¼€‰ÆÔÖ)Éü +æT°u'ð£ôÞŠóŸüIð§ÄAã�éÆu-¸�6ã¨>˜¯€~5ÁU?g¯‚¾2okw±=Ä{K“sÇÀ u#$ãµ]J¤¹b… %IIÅ-OÔ^õÎø¯E#ðÕîˆX§Ú¡x²;nR3^sðGã.…ñ·Á�øÓÃŒ^Òàn�±€Ê@ Œõö`Gݬd•Ú3³§=zçíªêú‡ì¥ÿ1Õ|IâØL1 §òšPGîË r}E{/Æ_ Ú~ߟ¶—¦è'̰˜¡‘�û§‘Þ¿z?à§ßðLý#ö—µ¹ø�àè#�\�›hÄ„Ä8d=ÉÀÈï_�ÿ²çˆ¼kÿúøÓs«øïA–úRâ6Že;�(áóŒ|½V2W�ÅcéiËž<Ñ>°ý¹ÿà–S|)ðÖ•ã_ ©h­B%ÀLḎêz×Ñvßµ€<)ûÿ¸Õ$1ßÅbÖ�o·’Ä›'Þ¸Ÿ�ðX|GðÓxJÇKžSp +²õmç#�Î@>Õùïðoöqø«ûRøêI$†ê-y2ÊXÎã�¡�E*KÙÅâ�ÚØ•wß{LÿÁ> ø�þ:k^;¼‚AgyÈý%±Ôõ9¯ìR¾Hý’?fŸ~Ï·Ñ´{T·�áE”¨åˆ¯Ò¾·®YbY¹4|þeWž¥ÂŠ(¦yáEPEPEPEPEPEPŽ¢–^ÿJmž )+£H>‡ŠþÐ^*�Áß 5�rW äÛ;Op+øRý‘¢?´§üÆmG^l?k TóÈpükû4ÿ‚„^\XþÎZĶÙÜ"~rGê+ø½ÿ‚BMkkÿ’ç?h¹¸ÜTtR­Éüýk¯ M,<ä}N\¿pì@ßðZÔ�ÁŸ²ÕÂèÀÇå!¯Pv㿱¯Áø&üßÀ¿´_ÃýWÆ>3ØÏ�»¾ñb»·uÏS_¼ÿð]õQû6Ý(%GæHV;€gÔ +þC?aoÚ³ö¯ðì—øc \ŸÆ¼/þ +Gû|_øEáyaigm=#GnìXaNLu5ÓMÓ©8Â÷’* + è~¾ÿÁ?l_ +i ßJñ=ÊCy摉´uëÞ¿©/ü~øuã-‹£êPM!8!1×8¯ó¤ÿ‚R~Ë^<øó®Oa£Ý\ØÛÆþ[¶âª’�ÎOsÀ¯ÓÏ�Z·Æ_ø'wÄÝ"}SS–öÆòæ4Ê3|ã#;¹Åe‹Ë)N¬£ïU„e&™ýšüNðö�ñÁšmàÞ²Âû6õ'b¿“?ë²þÏ?·TÞ‰…½¤·xž ý{×ôïû&|B²øµðƒIñ‚¿7êÍœz×òMûxx‘_þ +C¦Ûøv@ +ßæ1ò2`døÓӒö”çØéÀ´¯k¾Õ± C¨FþbHŠU½r3]}y�ÁÛIl¾évó6÷X#ÜùÚ3ú×§W-�žÅ«T�WÀ¿ðRoŒÏð[öYñ¿jOÚ'‹ÈŒ>ÿ¾ú¯Ä_ø.f…â?~Ê7ºO†ƒ¸Ùd@§€# ×UyÅy�9U5*Êçá·üCá‡ÇÚCSø¯â«/´EÑxš\¾Ìuå½súWöñioo§ÚGgl»c�B¨ô¿–ïø7ÓÂMáÿHuBVûÍ>zÆ~QŽÙ$þ5ýE$=ëLƧ5vvféÊV%¼»[;/¢)jþ\?n¿ø*ï�o>97ìùðA=¼¢Þâd>i`ÁÅGŸuÙ¼=ð·W¿€VÒS¹x+µIÏ׊ÿ;[�‰šö…ûeêZí¬]jR_´å°ÌF~¸=k\¯ ÊN] +Ê©.W#úÔuïÚÓÁþ:×oçX¢ýëÙùW¹ÿ +ýÿ‚{þÙ¶4³§Þ] åY61>ª0xëœ×å·ÄÏ~Öþ �éSÇoy©¸�ÏOjËÿ‚RþÏ_þ xÿUÔ¼`Ù¶I7IèH=ýjeF2ƒszž„éÅÁ¹ÒÇí ñçÁ¿³ÿÂýOâ/Н#¶Š  @i$…@z“_ǿĆŸà¡:î³ñ�Wߘ�'’¼Œ 9ÞsÔžäWoÿ ý¢þ5øßÄöŸ tè™t�:dB�«;�K ú�×µvmß +ü$ý—áµÒ¦7‹fb%£ÇU ·çœ +륆p¤�=ÙL:Š÷Ogÿ‚%øj…ÿ¼Kà™$/´ao “�é_Õ%¼D[¨¯à÷þ ‡ûnÚøoö€¼¸ñ$²$÷÷!·*w*Žçéœ×÷Gà?¯‹<9e­ªm[«xæÐ8¯'2ÁÊ�VåÔáÌ9•ΨÂ'¶x_ø��'ÿð\oƒºg€üc§|V¶‹Ë&,ïL.2Äsœ÷Ç5ýd¨ÚM4ÿðqŸˆ¬t/ƒºE´£^Æè +õáÇ$u#$Ư.“UcÔY-fªòkÁ%>6Oñ?à�—ß4¶±Xó�@ïŸQÓû:WóÝÿ!‚÷þæÞëP §Wˆ°ÁØŠÛ¸"¿¡ÐW!Z¬¬a›ÛÚh-QYHQEÿÒþú(¢—iÆká%Î�§�šøcöŸľðUÅ�«bYQ�'¿}Ç3ŒçÒ¿�ÿø)oÄ‹Ë]RãM,Á`%#�Xúšüÿ�3…ÀN¯dGý¸CÅT0Õã_ÈüiøÃñ÷ÇšõÁº“0$�òÔç½pÞWZí…¬0�%&Mã8ÊgœšÊŠÍ…œä³cêO&­Ú�NÇPö2òþeúŽ‚¿‚gRx¿mQß[ŸôQ<†•±àp¾î–_qýmü#øðåþØßõý/Á:?i)|_¡/�5iLÆ%Sã–Rz®OQÅ~üø âÜ�è€bØF8ÉížµöÀ [Ä?>9éº â˜f†tŽE=ÕÎ0?:ý‹Ã F+V3›÷YüŸô¬É²~&Ëñ}+´ðÿíK¤_ç|Úp[î�ùé_Û™5vð\ýOñ.ŒÁ£öËÂÿðEøÃÂþ"‰n#”Úä€v!m¤†EêsŸ¥~Hø�âWÆïø'oÇ�øD'¹žÖ;YA`åÀ�ç’;ŒzÿGo„r#xNÔ­ULol›Jò¿ttÿÕü>ÁÇQxnËã¶�wh‘o»”PíÆn+,³*ópš1ÂÏÚ]3ú¶ý�¿iWö•ý“añÆ· ’èÚì•”�›ä$÷ã­žÏí4úxÿ‚Œêz„³Qu•VÏÞ â¿®ø eíó~Èú†‘­;48�ãÞIJ� gµ!ß¶£=×ü\¶Ó8ä]\'¸:ïÉkrº±FX*v”�ôdð¯Åëo‚ÿ±‡ü{zq¦…›œãæØ[$ût¯àûö¡ý´¾0~Þ?®>xcTŸìz�Ђ1HeÞ9ÈÀëž+ú°ý®oµM'þ 7o÷M$Ñé³îéóÇ€£Ú¿�ÿø%O‰|áÿÚÃÃWÞ2‰Ö[ÉcË(ÎYò9ÈÏOZçʽÚ*-õ0Œ]›GïÁ?ø7âûâ»m[ÄW²Ã©µ¹džVÆóü.s�ǵ~Fþ׳¯í#ÿ´ø•¦êR^ÜM¦Êà­Ärü\§£šÿHÏϦϠYO¤•6¯ŠôØTcü¼ÁËx=¾鉨ª5ÚÈ¡Rž*²œÆ¥JªÕ1eةʧ,�Òø%wí{§~ÚŸ³Å½Î·sö�NŠd��ì…p2\z×ágü—þ å㿆šµßíðѦ0+›‡Šß/9àÛ­u¿ðl\ZÕ½·ˆ®.a‡!‰é†é_Ñ?üOÄÞÐÿf�v<¦¶|y݈SÈô§†«*éS†ÌÙÔ�:Ö�üLüÿ‚âø»áÁ{߆òA3ëVјvÈçkÈF€yÈã�nµà_ðOoÙsã�ííûJøÉ¯.ì.ï„·,YʦöÜAç�Ú¿)>(˦êÿuíGÃÎZ½’H_88Žxí_Ýü›ñá¤ÿ G‡Zá[žÙ]ƒáY˜˜äãŠúlÆôh¹Q[îÎÜF‘rKSúCø!ð«ÃŸ>iÞðÜb+kV1Ôd�ƒ×ô¯`ò×­AÞ£oUªùçÆÖ›”®Èe…&]®3_)übýŽþüfQ/‰lƒN¹Ä�ð{_ƾ±a‘PŒçŠNšnýBž*tþ~[i?ðIïÙÏL¼ûPÓâ`p0[ßæÎy¯¼þ|ø}ð«J�Hð†Ÿ¬QŒ£Ÿÿ]zÎõ¥`Õº î:¸ÙÏâ¸ý)´£¨§=ER�ÑÄåq”QEH‚Š( Š( Š( Š( Š( Š( '¬8¡>õI´“A¤SæŸÚÏÁpøãàŽµ¥>âÍ +2IÇ¿��ÙÙuïÙ—þ +Höz¹h ú˜H݆ +:çœÿF�wN�UÒfÓä)×ñÁÿNý‘¼Cð·ã?´^‹iç-¬¢l ù�SŸòk·4å*ê}NI4âà϶ÿ൞$oþÉi°�IqÁc�Ü0Âöõ¯�?àÞŸØïÃZ�‡.¾"xîÉf¹.|¢Ë�œƒëœ +÷¯ÙŸÆíÃðºËÂ%œNÅV ã‘�`ÃÔOZýêý–f¯þÏ^�Að¬!ÅøË;�Ò¼üLÝ*n‡PÅUPƒƒÜúFð—‡UP~EÁdüáÙ?f�SRª×2#"L•Êã�©¯ÙÆá·c5â?>h¿¼,þÖãà ÚU†G=ë‹ (ÓšŸcÈÃb-Q9çWû~ØšçìuãûmZ'6WyŒ¥OÉ"÷ǯ­t?¶Ÿü7\ýº~2øÃƒCmm{„Ú1'†õ¯ë#Ç?ðE¯…Ö›4ÖV¶Íy)f  é“Þ¿ž¿Ú“þ ¿oû|D¶øµ|²í³¹BBýοÈ{WÕSÌ0ò“”¼ÏWÚÆwq?­ØÍ"ø'ûØêž#;^ÞÊK–fë…L¯á_ÊÇìÅዟÛ#þ +3¨xæÊ6—J²¼ÞιÚÅd 3ìJçßþÐðVKÏ|‹à_èškÛ›qhÊ„®Ð˳!†3þïë_¯?ðEضÇá/Ãåø�®ØˆµIDŒí�Û¸ç1^uHÊŒ>ì¸þê ³÷ß@±‡MÑ­¬`]‹j¸÷VÅÀ§ljòéÁ¤|Ô�ÝÆ×Æß¶ßÃI¾$ü¾Òíáó›Ë`G¶8ýkì°5KTÓmõ;),n×tr 05£º\ˡф­ìê)ÈOì#ãÝköføûwà VG³µžós£œ©V<}:WõiáOYx†Â;»I’U ¥Nx"¿à ß±u톨>(|>„Çs$ùY\àç/x;Çz=½þ‘{ñÌ ‡R9íY?g¿‡¿íäå¤RI/ü´* Ú¹°˜§M¿Äã¥_Ù' -ÃÁNþ|<øc†ô=K»ï$ RPvã `’ÙÏJØÿ‚høÏãí­êž-×tƒ§h’»•óI !Œzã8ϵ~ŠCÿÑø6¯£ªé±Ì! ¯‚:u¯¸<ðÓÁÿ t„Ñ<%e¤:"�Ò»%^ƒ„#c:˜Úq�¢�–ðeüš†•nˆòÄ€2Mnøßâ/„¾éÇSñMäV�¨Ë4ŒIÍMLMJ²¼µ5ÄÕ•yZ�>­ªÙhz|š–¢â8ã$×ð—ÿfý¦ïmoÚ‚?„_ϦØOöH˜¹fvI|(ÀÃ0uãÞ¿@¿à¦_ðWFñ•å×ÀÏÙíÚånìí4ù¤ô,1Æ=;Õ/ø$·ü/Å3x¦Úâbšàù¨.{`�džÜ}Aâ½¼5áéºÕ·{#ÓÂPT å=ÏÝø'ÏÀ;�‚ß|;¡ê°§†ÊÙ€ d(fwÜM~€•OO´K 8ìãÔ:qZ^ZÿŸÿ]y¡ÌÜ�ž«[šWdTT¾ZÿŸÿ]ZÿŸÿ]_ÕŒîˆy£æ©¼±Ûüþ´ž_ùÿ&—ÕÇxŸÿÓþú#4¹'­)\ Š@2q_;IêT½ÿ�W#®+ùyÿ‚“J­â­RÙ�Ï ‹!åŽ{ãÒ¿¨«„2@È:‘_ÎüáÞ¡aâcâ"Æ8ïÔà(Îý£•>˜Ækò¢ÿ²ª(£ûwè+šÐÃñz�Wg(éêš•ÏĘü³uÉõ­|ª¦zóYv÷*ɳn1ÅO‰ÿÕ�“_Á5ZR?ßu¬5Pãk·!AïúVn¦&n"`Ka°8ã=*ÌW‘Ebbeɤt·½Hâ™¶`ä{ò8®œ¶‰äf4ä°óvèT±…¿Ù¿cí6o,àÙÊÙÏ^?¥1Ÿš ˆšçšwvì AÉ5ý?~ÅÖŸhý“të 3æ*ÚÈÀ{0'üÂ|]‚K‰zõœÀ¬¦éò¬=Oµ~ïÆô\ð8t¿­þyý±�‡q«+75ÿ¥Hýbÿ‚W[C{ux¬å”‚Ät#� yoÇ«‹Ë/ÚöHa_Þ¨£\åÀÍ} ÿ£ð®¬±Þê÷VÅ"»PCt +S·â büEøk«ø³öäfÓ—tQÞFŽÁ‡?‡Zû\—/oIÛª?;Îø§5Îj9§búõ>˜ÿ‚†ÏrŸ±½Á�™UFÑœ“¶¿…•Ò|aΈ°jOLÞ,±üøÁ8 �NÕþ–¿~èþ¿€5˜ÄqL‹Êã*WŠüå·ÿ‚3|¹e›Y)pùÉw�æL zWô–G‰tpÊœ‘þff9�*µ¥;Ú÷?7~ÿÁhü!ðOà¬>ñTÍ,öKˆX)w\/!»u¯æ�öŠø§ãßø(í/‰4»K™mMÁŽÓ –U-Õ¾µýjü]ÿ‚ øÅš»Â3äVmàβ#ç0¹œWÑß²�üçáŸìù¬A¬KkÒŃ€>RqÏQšíž"�4åJ:³Š†&”qgGÿíý›µ‚±Ý®‹oh"½’Ƀ; m¼’>µüþÔÿü[«~ß7𥽫�í¼3(%NÙ9$œWú¡ÚøjÚÇE�E³P�Æ›˜Æ+òKÅ_ðH/…~#ø¹'ÅW�¤ŸÎhZ5Á9Ï^µž_ŠT›Z²pØØEI¶z)ømñãöµø}–[Ý#–*œ\祟ÇO�dŸŽ—6íguo—æDÂ7VB[8éÁ?ZÿTx/MðW…ìü-`£ìöqˆã_î�_4~Ð?±ÁÚn/|U¤ÃöùÓi¸DØŽ…³•?ˆ5 s¡{Æéœø|t> =çSöoÿ‚ûø{Á?¬4?,—Zd+, ¼ì@Àôí_�ß¶·í[ñƒþ +ƒñ‚Ë@ðî—q�øŽ6Žxàtý1_ÿÁþÉ®¶©osÖÏÞ·RO?~Æ´„žû<î�ÝÚ5Îk}ÝÚÿI¿ x[Fðž…‡tXDV°®ÕQÇò¯•|Oû +üñWŽ[â¨³‰7.6†9Û‚+¾–sQEÂj鎖iß1[}㟅š7Šï¢h%¼¶�Þ6ä«mäf½g{VN…¢ØøoI‹FÓP$0�ª£€jW�¡ó¸š‰Í¸Ž,O)óM¢©;\±Aâ EO['qÊWšuS$BëQ”#¥KE&®z*r Ô$Ö³”,QEQEQEQEQE*œÔ»Ö¡¢‚”¬K¼ŽÕòçíKðÿüs£êÖë1XÛh#¹kéêyÛ"”~sO™¦¤·GN*sRGðwã¯|dý„~7]x�À°M.–ÎÄÇåLgø�ò¯ÜØËþ +ÿðãÇöká߈3µ†£k¶7Ž@BúpN3ÏzýtøÁû/ü0ø·¥\[kZ|fiAÃ…7ãÛÚ¿Ÿ/Ú‡þ·¬Íyuâo†w&Êiÿçßä#8Î?*è�jU•««>çÕF­LuÜþ‹|9ñÿΈáé—ñÉÀ?+×ñ®âøb壹L0ÎIw¯àÛÄ_¿oß�:ŒÚ%†¡{<ÿ(�ΓÃ^0ÿ‚’]^-‚ÜßÎ’ ªÅßåÏ8¬žU¯ˆä©•E;£û‰ñŸíð³À¶÷7Zö­1ÛÆdbÎÀ÷&¿�ïø,üÂ?µݧÁß„Ðë˜ä*­|¹3Àf dãµqÚ'ìAûs~Ь¶^5Ôõ³Ë"—Y‚ž{û +ý±ý�¿à�ß¾-ž³ã製¼€‰¼Ö\±sÛ-Ú¯ìè>g«&2¥A]“ßðJÏø$¯‹uëý;âwŸ|©D®ï”çkrþÉ<#á}7Â: ¾�¤D±Cn�UW§/‡|/¢xZÂ=7E�`Š%Ú€8Jèƒ�ÐW-YJ¬¹æy˜Üo´~D€`b–¢.HÅ2¬óܡ栢š´0üKá­7ÅDÚN ¡ÒU+Èõ¯ççöäÿ‚aÝx�Qo|>´�.˜ä4'nÓô¯è“q5ÑCu†u <Ôò8¾joS» ˜Î“џŧƒüûV~ÇÞ ]_ŠâM:Ýà À¯µ~¹| ÿ‚³ø&þÙ4ÏG%žÜ.é�÷ÜOJýYø¡û5|<ø¡e-¶µl Ê¥ +2¯Ë‹ÿðHo 둼þ˜[‚Iuë‘øw­¹èTV¯3èichUVš>þðí½ðcÅRí5›)7`|“)çÒ½ËMøåðëS`°jåŽÕýâüÇÐs_͈ÿà’<1q%ç…oç�iÊàž +͸ýƒÿlÝ.Ö5Òuyü¡Œ0ÆGüŒ°¸hü9±ZÐþž/¾2øLQ&¡{ *r2ò(çñãò¯#ñ?í¥ð !×uËx<±“—^Ÿ�4Þ$ÿ‚~Ûþ(¸_k7· +˜*C²�ÿ¥Rпà�Ÿ´7‹ —>-¼ºs#&w`HúdÖßQõwPÒ– £“?Hÿi¯ø.Á�‡¶ZOíªr±†äw'¥~øËã·íñÿ ÖeÐtwžËGº¸ +Qw.îœã ‡û#ðGþqá-&ê ß|Â6]Êß6{“Í~àü"ý˜>|Ó×Oð~— AT.í£9’lÒ…zt´¢®üÍkchQV¥¹øUÿÿÿ‚+i»‡ÇÿÒµ3É`'¶ãž~•ýxsÃZ?…ô¸t}‚UQU +08­ØãŽ#Œ`˜©ð+–Q•IsÔwgÏbq“ªï&7bÓ袵JÇ(QEÀ(¢ŠÿÔþû›…Å5zy8¤Zù¸Å=ÈäÂó_ ~Ü_¡ø�ðÖ{›BòÜÊ«ÜaÚ¾î•w +ÎÔ4ë]RÍìîÔ2º•?C_;ÄLqXz”d´húîâÚù&kG2û84ÿÍÂ/įj_ uÆÒµ4*åþa‘½I=Àèx®BCž¡‡#ë_¾·ïì}5ÝÿįG%ÐÈ™¢NXŒýÐ1“Œ WሴWB¾µDÐ<+©ÍžÜwÁ•òü\’�ºÑ�~6e¼S”Ó¯Jªr·¼º¦SB"Ù¹&«ÜF³À‹)#Ë;‰ŸJt²Ka]ÝÈ$pj2ºþó; ã®+â#VjI-ÏèƤlžŒþ…àš¿´·‡¯|ÿ +ÿ^™-fŠ0ˆ70«µ�'6œ{׫|\ýŽþøïÅ2øð…Y§Ä’m*A#×òõ¯æãÃ'ñ‚µHµ½é­|¶9�­žç•ïÑü{øáâÆ•§ê“Kmyòm�›©ààc?Žk÷žâ¸UÃC ^’?ϯ>Œ˜Œ}_>ÈñÞÂ5u’üÏé‡öy½øMà«!àß\B.¾în +ßü?ð7Âvž8oˆ¯7²n!�mÃøÝÿüýž>%Üx®jóÉ q“!,[¹#>£#ñ¯è2Ú²Dz¨¿¦87/•j*u#kl—>6Ê–WœVÃåø—QËã}ØèÀCбQçÔµúEr«3ùêu +(¢µ"ì*7­IE&®ez\šs.9ÊÅ«Øìû +7æ›E+‡3Xž´™4”SL..IëR EEJ "œXù™)PNhØ´ ƒÒ–¶$nÅ£bÓ¨ …šuPEPEPAâŠ(6@EGS?ݨk9  +(¢³@ÉÅ?Ëÿ?äÒ'Þ©h4Œt")�šeLÿv¡ ™«QA!EPNSƒšm& ÖÐÜ)I0ô4ôûÕ5_³RZš*­}«|/ð>»p×:¶› ÎòŠÎ³ø1ðÒÂUšÓJ…NFuü«Ô¨©�­�~°û™Vº&‘`¡--ãŒE¯gåéSà´˜Z·Iº¤ p)J3S`”SPFmܯEXªõ2�„QE@Q@HéO ž™E•‡•„žT {kVmÞZ–õÀ©”eª]¢Ÿ²M{VRV äļ²*p#Q…Pùö©±ëÍ-8QHWЋ#Ò™šŸhÎhÂÑ(XJì� Î*ZL +Z’Ò +(¢�…Q@Q@ÿÕþû\ñN7;‰Z}|ÄUõE¸¶Æ°ÈÅ3Ë?çÿ×RÑJTî‚1w1u­"ÓW³k+¥ ¬;ŠüÔý¡ÿàŸ~øŸ×Ú[[Â×+òäœòé_¨L¹éQùdŒb¾?>áŠÔãZ>ÿ‚|E͸~¿Ö2ÊÎð~§ò™ñ þ ©ñZÍä‹Hs:@ÄlKþÍypý�>4FVÌés†Qœ²`�®z×õîúUŒÎ^HT“Ô‘M:6œ9ò�jü毂ù|Ÿ7-�ë<³éëÅXzJœ’•�åoÀÿðNŸ‰š½¤1ëÖr¯–Ü…ç¸=ÇÒ¿T>Á:ô­½÷‰– Ú¡FïÆ¿Wãµ…�íS¯Êv×Ñd>`0³U9v?0ãߥ—g”� Ôä‹ÞÇ5áOè¾ÓLÑ XQFÐ9®¦Š+õ:T8(ÁY̸¬EJÓu*;·»#ÿ–•%Fß“RS8Š( Š( ¢eÇ"¥¢“W½æ\Sk¬ER¢Š(É÷ªj…>õM[G` +(¢¨Š( Š( Š( Š( ?JŠ¥~•e0 +(¢ §Z–¢Nµ-ÐØkýÚ†¦»PÐD÷ +(¢‚Š( Š( 'Þ©ªûÕ5m €(¢Š  +(¢€ +(¢€ÀÍW©ÏCPTLŠ(¬€(¢Š(¢Šr}êš¡O½SVÐØŠ(ª¢Š*&m ‚Š(¬Š +(¢€ +(¢€ +(¢€?ÿÖþúÓ­KQ æ¤¯šÃÇÝ:Š(­ùQ\uÕ¤EV 5¾í:�ŒŒRÖÔÖ†s�Š(«±<Œ(¢Š,+0¢�ŸjLû,Ä:Š@sKHŠ( ?JŠ¥~•ð+)­A!(§ìjM�K‘�•�¢�±¨ØÔ¹XY‚}ꚣU äÔ•´°Y…QUfaEQfaER°¬QEQE1úTU+ô¨«)îETôëRÔIÖ¥ Ú »PÔÏ÷jžáEP@QEQE8)#4Ò¸ÒûÕ5BŸz¦­c°‚Š(ª¢Š(¢Š(CPT硨+:€QEfEPEP“ïTÕ +}êš¶†ÀQEPQYÔ6†ÁEVeQ@Q@Q@ÿ×þûäýIQTäôÀ©sšùÌ<’ާ@QEº« +(¢¸«ÊìŠCžÔŒÛk €ê*&`V²äÕô¨NÙnbSÓ—QýjûIì�š+šŸÅ^µžþúÈ¿ãNµñ>�}r¶v—°É+�Ê�Æâp(öˆÓêõ-~VttT‰ä—pÆ{Õ©ˆê*œ×ÖÖ‰¾åÂs\äÞ;ð•»mŸP�»Š×Û¶®Z£)kc¯¢±ô�Eסiô{˜îQN �³ƒ[QwÔÂ¥6˜QEÌÆ?Jju©jd†2JÁBõ$ÔÉ[Vi>„Ô„‘Óšó{Ÿ‹ß,õÒîõ{Xî¢4Š<õÛ麾™¬Aö�.xî#Î7FÁ†~¢¥TW±Õ<<â¯$^ßþÈ£Ìÿ?äR� àR<ˆ¼7zkÔçQc•²qN¦©V—{S«x=TPN9ª2êV0J –UWnŠHÉ©�xÇâe¤ÙzŠ‚ ã�wÄÁ—8È9©Y±ÅRªšº"R°ì�ÖšÄ�‘JFhf +2iJZ]Š×#Üôå,z×�øÇã÷Âßjk¤ø—U†Þvm»Y‡޼עøcÅZŒt˜µÏ ÝGwk(]5Š‘¬°ÒŠ»GEEV­hc4# ŒTl¸©9Í-% ’£r „õ¥òÿÏù5Ìø³Æþð6šÚ¿‹/¢±¶NZI˜*�Ä׉xö´ø#ñ'ÄSxoÂÚÝ­Äñ6Ï–EäçzÖVÔê†R�2ZJªãšu"°u ¼ƒÈ œ Ò^fv°Œ Ï,÷ÿ?­yÅ‹þøC¢{ÇZ„VPçÈÀd׉ü+ý´þ üXÔ×Gðíú4òHѦ×VV+èAëX,BÞÚÁT’æQ>½òÏoóúÒ f‘z†S‘Ô}*@Cq]P´•ÑÅ(ëb0¥ºRìj“*8®Å<#à颇Ä7ðÚ´ÙÚ$p¤�ÔóYNj;šÒù;E]�˜RN*QÀÅ`xwÅ:Šì§ \-ÌãzŒ×A[Ò”ºb•7 ÀbiôQT`Š( Š( Š( = ASž† ¬êQE˜Q@Q@Nµ-D�jZÚQE@QEgPÚQY”QEQEQEÿÐþúH,t¦=ª#œx©OQ_(¶F×Ôòo�tï„?5_ˆ§ú�6”ç§¿?g_ø.ïÂ?ŠŸGÃÙ�:ŤhVñó¸0û¤��FzúWÝ¿ðUÜx[ö8ñf­LIm‰êUŽ=G­šU¶§â»?\x‹Â×1\,�,InÄ8$žF9ü«Z—+œ�¥Ë°tçGÞ[ŸëeáŸh1Ñ¡ñ†n’òÎq˜åŒåXVë¶ÑÇZþR¿à€ÿðPoüIVýž¾%LÍuk•‡Í9‘ˆÏô5ýW㸫Vq¼^ç“�Â:59øqûjÿÁ[aýšþ=iÿ4í:9·È«<®H*XãqŸZý’ø{ã(|wà=/Æp€P¶IÀ· ÷¯àWþ ã_ÁGÆ‘tCG �ÈÈ óü«û…ý�µ¦ñìÅá E‰,ÚlJr1Ê®;WFeIS§Ùêc¨ÓöPqGâWü þ â�Ù{Çíð»À°H·lŸëàŒû ¯ÃÝ/ãÏüÛö½½ŸÅþ¹Ô$ÓQÉkx † ô<~£Šñïø-ω¢ÿ†æ¿·»m±E´ì„éÏ5ýaÁã­o|zø?á_� u/x¡ŠxX¤œf7†¿šßø'ŸíàOÙcöÁñÀ�~øm¸•íã,ÛW;þRsÅ*\ònF²pÆÒrQ´¢W´›†qUฎêî `É î4”“Õ!S{ -ÄPDÓLB¢‚I=ëù ÿ‚ÃÿÁLõ¿‡‘Áðoà]ÞýVñšZ&åsÆî9úWéïüö„Ö?g�Ù‹Xñ~†q9‰‘Hꤎ ¿ðM¹¼%ûLþÕrx‡öˆÕâo2q2µËŒƒ¸ž7qŒS¢ïz­^ݧÊ0±KÚËV{G…ÿc�ø(·Ž¼2Ÿ­µ«ë’£ÏX »1œN+ë?ø'íÿûJ|%ø¹Á¿‰wwÐÝMäÈŽ¤„ÜØß“Ækú¢ðwÆ_ÙÓNÒ¡ð…õí7É‚1ňAck#ÿ²ŸÀÈk:\ÿ»·²p…ñ6xo^•ýBH_Jâq䓇cÌÇQå¨Ñ? _œ¿ðP/Û«À²/Ë›½Fö?íkˆÜ[ÀofÁÀsÖ¿Au{£i¥\]gTNÙú +þ?n�ë´7üOð~»y¿M‚ø[ì,X0ã�î+·AVmËe©Ý”`ÔåÎöEß +|$ý·¿o�wTøá ßÞÇ Nó„ËŽª1‘ž+ô þ ›ÿ ø�û?|F“ö~øüÏbq5À 2¤�Aú×ôßû4|ðwÁß„:g…<;j�Æð#¾Õ’Àþlàà�…ÞøO§ÆØ¥Ž¨ÒFdt ž¸ÇZ0Øåˆ«ìª-Ç£\kJTí¡ýeøw_ÓoG–N sÚ¸ßxïßü=sâ\-µ¥ª˜ã�ØzšìK`â¿?à¼_¼Aà?Ùäé¾�à–nYÔ‘´wý)F2©8Â=MðeR§+Øü¢ý¹l¯‹ß·¯Æé> | +šäxí6á²UNߘ¤×�x×ö]ý¦?`û�Ç–Ó]][$Â_.@í´u$ú{w¯Ñ�ø7{á·‚ü_àÏ|FÖa[½RÒáS2ä�Ü“Ÿ\×ôGûFøÂÞ7øK«éÞ&·Y¡HÁ ¤Ï¥tbq*3tâ´GÐK5=ŠGÇ¿°/íñá¯Ú_Àö¶zŒRZêÖÀA>ò<²è?„“»ŸqÖ¾ìø¥ñg +<{ãܤ6¶q4¤“×hè+ø]ýŠþ(ê??à£ò|6Òõ "ÒçÕˆ0ä€rø¯¡ú×î/üƒÇ7~ý› Ð4û·¶ï³*ÛAÏ­pË 8â#BzsÓÁSuÒ—Sò»ö¶ý©~5ÁG¿hHþü †A¥ÛÜÂ[bóŒœzŽMu?a�Úö(}â/ƒõ ‹Û„–T\˜žL ÷Æ}kí�ø7§à΃7à [â­ +ɨÇtJTn9Å@ÿ´w†¼?âOƒºÕ�ˆ-c¹…mÙ€uÎôæ°ÇV”*¸Ceø�ï3䬨Åh|5ÿóý¾4Úþ ñc%¿‰,PE:–;Ñë_«q(ÓJÀ*‚Iöü~Çž5×ôø(Hðå«´C¨6ÝŽP¿_ÝgŠ¡¸¸ð-ݽ±-$¶ÅT޹eë]8š^Êj+®§�œ`ቮ§á?ü£þ ¢~Î:‰ðO¹ ö£2\ˆ ‡¦K`àf¿•ÏÚWþ +iûF|nñ¾·{®µ¸·“ÛÇ�U\òIõ¯éƒÃ_ðGˆ>4øÿ[ñÇÅ9‘ÒêfhÄ‹“‚xkð“þ +ãûøköKÔ¬µ�Zý–}§æê3ÁÇ¡5ê`çG�A+¶va¥îÓèT?ðF¿‰ú¯Ä¯Ù®Â]Váîg¶Q¾fêÛ±€}Å~É×à'üû5Ü¿²Ñ{¡÷äÜoOé_¿uÅìÔg$»žkuSP¢Š+^].y|º\(¢Š’BŠ( Š( ¿Ý¨jI;TuœÀ(¢ŠÌŠ( Š( §Z–¢Nµ-m €(¢Š  +(¢³¨m ‚Š(¬Ê +(¢€ +(¢€ +(¢€?ÿÑþùÁÍX=EAÚ¦'‘_*–ˆÕ•ÿðX™.ìCâømћ̴eàp8cÉü3_Êüëöqð_ÅÏ�³éŸm–ú("ù�Ô¡‰ÚëŽzWõýÿRÒXý†|lT3®™<Š;ü±¶vûã8¯æ{þ ¶‚ Oö‚×d�e¼–Ýýå «ýMk¯ÕfÑõIµ…mYûpüø¯ÿÊý·�Æ/†:{Eá{™Òxgƒ#j1åN8>•ý\þß·‚¿kO†êÐ\Fšµ¼@\¢‘†,8eçó�{·í9û6xö™øoyàÛG1xÈ…ÝAÚ߈èzWñGª|aÕ¿à�ÿµ®·á,\jJƒ ¶FÙ´;:á¯ÊCÞ–õŠk›âA°ÄR´÷G‹ÁX¯îfÿ‚”Ϋ&$r‘¨# ÜGó¯îÛö>´‹Oý˜<ol8M2/Ŷóú×ù¢|wý¥o?jïÛ?Š-o%¸ÔnU'.‡®}Æ}ëý1¿dÈ1û2x6%$‘¦B2}@Áýj³õeN=‰ÆEF’Gðÿ½ íÕâ$d/Ž6ð þyª±çì“ÿño…à¾øqm @ÂGX÷wò@Àäã­zwü™´ÙoÝSJÔ—y"Šàúµý­ÿÁ:ü#¤xgöQð»éëû»a,�<� ã®¦/*4áɹߊÆ:Qº?•Ý7öÿ‚ j÷QÞË},¯•Mó2*¨éžù?JúOà‡ì3ÿð÷Ä7Pñ.©qŒ2£M vl…`p ×õä‘¢}ÑJU[ïkÊ©˜Õ¨µ±åÏ=¨Ó‹JÇá /TÑü–¢Å®#€Xää/?­Ÿ‡üoÆ#ñ7üJÛO´v&Öå#ŒuÚÀç�_è�ÊJž†¿‚ßø-×ìÿãÏ„?µ­ÏÆ+›7�L»‘$¶ºˆPÛSÛ§ë[e)FN=YY=~Gö‰û } þÍ~–èîvÓ¢$Ÿq_G×ä§üWöÀðÇíð MðM½ÂM¨øzlî �=S±¿Jýk¬%US¼CÈÇP’ªÓ3õvUÒ.™ñ´Bäç§Ý5þvµ%Åí¯ü†çVÒó&¦!˜¡à“!Èü«ûÉý«¾;x_àÂGÅÞ%�aW�ãBÄÊœž/©¯àëàÇïþÛ?¶ÍÖ·áÑ(Iõ#xX©*ã´‚= ëe¥vöh÷r+Ó„¦ögúüž[¯„žžà’æÆ “É8AɯT`M|“ñâ,¿²·ì絨Âo¥Ðôè¢Ø3—’8Âà�VïŠü׿àà_é7×vÞg•$uñå@�œ2ÿ#ô®\%)Õ�à�*xT“œ^—>›ÿƒ„<]†?dö·¬Mt¯9ÙŽ1îç_Æ×ì]û/|Eý©|`°|*’[k›}®eˆ�àƒéè_júûþ +oÿ1øÑûmiv~¿Ð—O†ØæD¶8'…è½ÏLšûkþ  ñ7†t¯‹ž!³ñ©ŠÞúâ†ÔtC! �3þÊñô5­UR…mÏb‚•:^gesÿ…ýµþé-â}]½]‘™\™8ÊŒäàñ_UÿÁ2¿à¥ž*ømñÙ¯ö€Õõ­ßìðÉ9ýâȨPO''Ö¿©Iooá›û‰Â²Gm+z`!=+üã|U¬‹ø)ijÙȲ%ƹ‰W§úÓŒŽÜàÑ…¨ëÓ—´èm‚šÄR”f�ôrÖeKÿ \Ïvɰõå ¯ó?ý½¼Šÿà¢W~½Ä–’_Gg©Yœ(ë_é;áÙæŸá�µì’y­-‚ʹ E�wí;sŸðU°žçc©B69= ã#ÝIÈÕ†W8N]ŒrzkšIl�ª?nïø'u¿ìáû7x{ãW‚ôìÜJåcË!›¸Çc_´?ðDÚ÷Zøƒû<]ÙøÚFvÓ”¤l_'*1È<�ð¯ØŸˆß ü'ñköe‡Ã:Þ�os ÒÃÂ’¨`ŒbàŽ8ÆkøT�öª×?a/‰^5øoá÷ºb"#ÈÆ2+úÍO°Ýꦻ3þ +5㟉¿µ¿íÚ<.¤»²¶Ô<”ˆ1+³ÌØ+ÙਿðO½#ösý›<=â-*åQ®<¤Ûû½¼“ž¤WØßð@¯�ý¦~+ø‹ãÿĸîúÂìÏŽ7óæga€9Îkïø96x´¯ÙžÉáÄq²ˆð gæ��jîx‡õÈQ†È˜é^0G)ÿɦ~ëú»`}²QòŒ|¾YÁÏׯJþ¢ÍÄmÐñë_Ë÷ü]§K¦þÌúþ©*�Mò±ø˜ƒùázW³~Õ?ðPÏÚ[À?/¼1à$5­žQ<¸ÌŒX6Ü�§Jâ¯ÏS*qîeˆÀªÕ¤ïc÷[âî¦ÚOÃ]cR�ÂyVÎʼnÆ0Jÿ:+ÍSûSþ +d³,¾dgYFŒ�CM€§5ú�ñŸþ + ÿø¹á‹ÏÛè×°ÉC*˜ü²á�œŠü:øo¡üBðíE¡êŸ­ç°�¯Wt“�¸bÀäŸNF zØ*Nœ'Õìvåô=”e ÏôþðfïøDôÑ'ÞÑgë´Wó[ÿÛ[„š\·Ñ üã'*HÍ~þü +ø½áˆ_ tígK»ŒùV貂Àm(¸'éÇZþj?àâ¿‹ž ñ‡‡´¿…Þ½[­QT0Ž2›�ÇLŽkÄÊêFXˆÛ¡Á–á§õ‡t}=ÿçêS_~ͺŸ�;HV`… ÈR¤óëÈ#Ò¿£L×óûÿùøÄ~ ýš5I|Qiöyï®#š7*AhÈ  Úú×è¯íññ ãG€>\jŸcgÕQ”.=ž"JX‰r½0ÆaÕLK‚v>í2¬`»ôü¦ÿÁÆ?,­¼§xBÉÕ¥¾ã¨ã×ðÿ +ò¯øj_ø*„6гè××’á@=ëƒ×•ù%ûsxoöÏøÙ­7‰>3iײ.K¬mXÚFÔŠõð8nY©JHôpo³�Ô®E_ðmñ˜þÏ:ø—aÝ")^Üþuýü[·û_Ã}f×ó-d¦kùxÿƒt>5x_ÂþÔþ ê·‚ÂáÒ9>S•ÎA'¿Jþ‰¿jï�> ü×|UâíZÞÖ1k" . ÀŽrÍø +ññP—¶—©ÃŽ¡'‰ºGð§ð>ÃìÿðVkQóÚj‡pì~by=«öGþ2½Týžt{¸�·C²_ÝòCqù×ägì áo~ПðQåø�áÛRºt·ìKÄ B¹ÈÆNzWï·ü;à?‹ƒ“ü…<+V‰Ç’Jõõ7üósç~Êml�¹-¥Xþ‡šýú¯çÿþ ìØ?d«�‰´}³,}K‘ýUOã—©Ïœÿ…QZÃcΆÁETʉ”;QYØÌ(¢ŠŽNÕLÿv¡¬æEV`QEQE=:ÔµuÍI[Ç` +(¢˜QYOshlQEAAEPEPEPÿÒþúX`Ó³�´7@hPO>•óZ6~vÿÁV.ï¬aßO§»#ý�ä Ëç*?Þ~5üÄÿÁ¶š†¡§þÑ:åŒGbË«0 mÄýO§¥hþxkã€ïþø¶3-•úluØçÚ¾2ý•?à�_ +¿e?Ýx³Áh»î°;UJ–Îq€9¥í­NT­¹îa±��¹ú"#!G9¯ç·þ -ÿ¤ÐÿjŸÝüqð2´^)°ƒ,«Ïœ€1ôâ¿¡Ç9¥’8®bh¥« yàÖTg*oš�†ÅrJèÿ&Û„^(ð/Å› +ëš|¶º…ž¡˜YJ¶T‘ú÷¯ô÷ýŽ®ÅïìÅàé„o =U‘þò°fÈ?C_3|Zÿ‚XþÏ?þ%ŸŠ� ‚ýä>Å]¤ƒž†¿Cüá3Àþ³ð¦��-¬£ÆSŒªëµ~‡©�ÅS”‹?Ïþ �¡\韷¦»©ÎC£1’&3ƒòœŽ¹9â¿H¿à�¿ð[Ý#à�Á;…Ÿ´§ºšÓýT‰¹QP£±à8ýþÑðMÙÇö’ñ:ø»Çšq{Å9 §÷ˆ=yôé^3¡Áÿc� þÌ–Q/íàuÀàŸÖ»¥Œ¥*j#{UÇPš´���üÃövF"k•6ðûOÞôÇz<ÿÎøiñ ź…´ÏΠ䢬Á8Î:ôæ¾¢³ÿ‚?~Çšp-e¢|í÷™Àl�ÇŠè|ÿ¨ý”üâxî?¯^køòý©¿d¯Ú#þ ¯ñéümð¾Îäi2ÌÒ[Íl‡h>¤qØ×ú `לüIøOàŸ‹Bè~8±ŠþÙxYTZé¥Z¤óø<ÅÆ^þ¨þB,ÿà¶´³ð>_kÞ¼:ƒÚ42]` ÆÝÃŒò:�Züâÿ‚}þÆ?iŸÚÎÃâV«gqmf·âúiäS¸�Ä€¯é_Þ ×ìyû=Þéë¦ÜønÔÆ«·åP§ë�+Ð>| øoð§wü!tv{†>P?žO¶©ÈãÚçrÍáµt£KþÇð:èð‚Eµ „× +›@ü|cøã«¿ø)5׈¢ÓgšàêSÍ�†Ò¬Fp}šÿC2ªF1^3ªüøQ¬x xÒÿFµ}P>ÿ´˜�ÉŸ÷ˆÍ<-IÑ‹ŒUîr`3%J÷ênxJ‰þiRÊ`ÝxŒùWñÿFÿ‚VüVÓ¿hÝOÇ~ŽKËÝ–yqй't8¿»{{x-âÂ0«À‡®xOÃÞ$òƹg×’w'˜¡°×- +uh»Ó븰¹£§'}S?ÿàŽß°æ¥û!ü-»žðºÏ®„’Uq€äm\×Ï?ðp�€üAâïÙÿOþŵ’îDŸQIÊœÇ^Æ¿£ {H­a[x" @è®kÅ> +ðÏŒ¬þÁâkH®âÏÝ‘C:t¹ãQUê*Y�ªûI-Á¯ø §Ãü8øªh^%GˆÏr’$mÐ#e°¶sÍ~ãÞ|"øu©^I¨ê:=´óÊrÒ2Çê{×IáŸx_ÁÖGOðÍ”VP“�‘.Ñšè÷µ8Òr“œ÷g>+0s›”t¹çRü!øm,Mš%¡Và�)kñËþ +'ÿ²ð§Ç]:OxÂ;[Ëh]ÕÓï2ãèGnÆ¿t÷sóR:,èU†AàƒZª|�š†0�9sç¿mãoø(çÀ�çá×…-/Å­¤²gq%ö°’1Èç½} ÿûÿ‚oüvý¬~/Iñgö“k¹"Ža![ÍÀ±@)CÑp8°¯í~ÿág€µ fºŸK¶óî×”D›Î?Ú Öî…ámÂÖßeЭc¶CŒ„g3Z´ðŸ‡"Áo¦N8ö®êóO³Ô¡ò/bYPõV�Ö¬’M%b©¤¬|´«6îaÂ!á¬/ú gÝùÉøëá€üáû�^ÒífŽâ6�-’7 ãƒ^œ„ž)õ§°MÒÅJ.éŸÅ¿íÃÿÙøßû/|B>:ýš$™¾ÝpÍ VŇÊìIÞÁêxðνû/ÁGj?Xøâ”-hBÄÒ;6 xã,Þ•þ„7znŸ�¶Â’ã¦à+6/ ø~Þåníí"Içr®kµW©ÚÈ÷cŸY]­OÍÏø'‡ü¿Àß±ÿ€mÚÄu¹Yv†*qÈ�οC~ÈžµµÒ­WûJH”Êä|ÛÈç'ϵ~‰¶qÅ:ŠQ¥»{ž-z²¨îÈ”rkð;þ Aû(|@ý¥|5a¤ø.‘—©=8úƒŠýöªóÚZÝ \Æ®?Ú¦éÎR�Ñ€Å{s¤~OÁ#ÿg/þÎ_?áñ()#Ì$!—iàÈü•~´ÔqE(#…B¨à0*JQ¦îÜ·dⱬù˜QE¥¬`QE +(¢³šêg5Ô(¢ŠÌÌkýÚ†¬ž*0j&€J(¢²¢Š(¢Š(HûÔ•^¬VðÔŠ(ªjÀQE#EQE³ +(¢‹¤QE+ ´‚Š(¢È—&�ÿÓþú‰ÊŠ‘FŠhS´Š’¾];—%­‚‘†E-!õ®‡n¥´"¨�jP01KS(®S>Vµ#f`qšnö§fäñþ~´ž[Ÿÿ]r´ú &= #šu1~QÍ.õ¥í-¹¢E ò)j®¤29;S2GJ{|Ý( ëY™¸»ŒÉ£8§”½3i©eôI¤ øYᛯøÚþ +EÜòLáG=>µùiãïø-ì•àýSû6ÆâmEU•â,yö y$ñ¯“à²?²Oí}ûOjVÃ9nŸÃQíiíìÂç#– +Fü`õ'u©eÿø!gÁ‹‚‰Å(æ—SÔãYe‰˜©Gá[;ŠáŽNÓÎ**ºqIɶßCé(að°‚•MYûû8~Ùÿ¿i]=_ÁzŒI}‚ZÍäV|êTá†9⾸<×ùà|vÓükÿÛý²€¾ßÝ[ÙµÚ€†Vbªòa@9û¼àwÁ濼¿Ù£Ç·ÿ~øcÆÚ®>Õ}cÍ�ïíÃ~µ¤ ©É'³9óLºâªSÙžæNMx—‹hŸƒ¾ñðÏŠuë[+Â@Ù#€AnƒyúV¿Æ‹>ø3à+ïøºqµ¬lÙ=ÈR@¯óÚý¯ÿm=câ×ísÿ —‡õ;¨ì`¾H¡(Ća&1Œãý*gMÍ6�’2ÀàH¹LÿFËKëmFÙ/läÅ"†V^A�kÁ~=þÒÿ +g_ Íâ_ˆzœVÂ5ÊÄXosì?Üý�õ˜|Mð+ºí©,·z]¼€·Þ9AÉÇsÞ¿Ÿ_ø+ïìûQ~ÕÿlŸÀ3LúQ UC� œ±nœƒXá!+Õz NUfö=£Å¿ðpìõáû‰,ôý>{‰V¸»)P§UËØ½�Ia°õo­�ôu×üK¡ø_J—Z×.ÚÚYÝŽ­~"~Ó¿ð]Ù›àŒ¿áðį®j�0Ƥ`u;™¹ôé_fü{ð—ÄÚ?ö?{/‡·¬º®­¥Åp« +ìáåWÎ7}kðoö!ÿ‚ë–/¼gûJZIT.¡‰Èr{“QþÑ´€Ÿ ïüyâ ¤Nà�Åp=ñ_çwûu~Ýß¿kÿŠÚ†©£=¶�op1+²†ØIÎ;œÒºaMÍsGcƒ/ËÕMf¢GÁÚ;á¯Çý#ûcáíßÚcTãû¾ÄŽ+Û®®íìàk»·  ³üÂÁºšö¡}á/i÷W&årޤœ•?/÷Ê¿Jÿà¯ß%oÙÛáû‚ÓwŒýÝ»Ž ê=¾µú5ÿÛø‚Iþkz-Ì¡î ­Ã€IÆ4ýzþ5ÛV Rçg£W.å£ÍÔþ›*9¥H#ie8U“èZb¹n~ÁF?l='ö;ø }ã{„^Î�¼dñ¸ñ“ú×%)s4�3EÎj ¯ÁJ>i¿´m·À»F‰ÝåX%ÜÀ8v€¾¼WéÄ7 $nÌ7Wù¦üøó«üOýº¬>#ëlUõ ^Ù£$œƒçF¸EÒ¿ÒO~�–~ü+ƒ×ªðkT³}8\×éíÿbðÇÏ�w?u{‰^+©c•¢’_•p +;gñ¯¸¼Oû | +øsðRð�‡ô 8ã²ÓÝRS–È^[8ÎiP´¥fŽœ<¨)r-O7ýƒ?à¡Þ +ý¨4tÑuk¸­µ¥÷Nvn!FTg©úñâ_ƒ¾xNóÆ~3½ŽÎÆÊ6–Gv…¯ó¥ý˜>'øßá—ü”ø'Á—²&’ÚÒ®Ö9\ç•ý'ÿÁs"øbÓÅ~ºŽîÖî5uxÛ#‘Èü+âïø(?í£mûü.>(�i¼”â=ØÏ~€õé_˜ßð@�‹~'ñgì�Q”ÜG¦¬`î%ŒgËËžîyâ¿?฿·&¡ñ‹ã!øYrÒCo¡!vɰ犃XPÃ9VöWØâ¥€‡¶}‘ôN›ÿ®ý«~5x‰tÿ‡fk‰¢0¨™Œr2çúWê—ìuûYþÞ^?ø™k¦üNÒZ=!�L¡\©<žz`s_�?ðDo�?²ß¿´½Á¿G‘d¸EÛ eB Ÿ×¿5ý¹ü8×>øÿÃö¾/ðÙ糸\Ç$J£…8íèj1•y*{5™¦2º‚qQ=*ÎF–Ù$n¤Uš@µêR^ê>c•QZ ¢¢rAâ—$ŠÏœî˜eAP²’j1j«Š/‘š)ª»WêjWÜiQO™QEŒ¤›ÐŠ(©º¢Š)I]%p¢Š+&ŒÜ +(¢ŽPäaHFF)i?, + ‘"›SÍ.zÊÉ�Ù² +*|-!E4rvaìÙ X¦…QN«‚°œlQE\÷$(¢Š“u°QE (¢Š(¢Š(¢ŠdÿÕþû“îÓ©€…Qš<Åÿ?þªùe¢5¾¢°'ŠñÏ�| ðÀ×~:ñÝäV–¶¨Ïó°]Ä àwü�¯]¹œA JzOå_Ãü#ö×ñßÄ?�Wÿ³ÿ‡.Ú-6Tq×!㞆£Ø*Žìö2̽Öm½‘òí1ãý[þ +/ûußx«á4kWºŠ(B®á¶6n;t¯ïcöZð§ðÃà†¼­|·6viæ ç†â3íŸÃ¥,ðA/ØSÅO‹ã_‰mZÒÏ’„?¼^ç'Ôô¯ëkâÄ áo�¯üaâ +[C“�…†&·=D–Èïͪó¸Ò�üéÿÁÀ¶Gƒ¼3ð½¾è·ÿñ:½BíDä¡9Åßõ¨üm¡ÛÚìye¼„º¸És¸÷ºýiý¨>;|!ý£?mMsQñD‚â͈]›åaÉzb¿0|u/…m¾:Û'ƒ¡òm ¼ŽE•Nîúx¯BÊ0q:¡MA(£ýA?cyŸ³‚ˆRÓ!\sú×Ò,‰'ÎÊ éšøkþ ×ñW¿e ¿†®¢š[+4†tFÃ(Æâ½@5ö¾©©[i:lÚ…ÛŽ.Äû +ó•HòYî|Ö"Ú³ãŸÛ_ö¹øqû+ü%Õ5Ï]ª]Ém"Ã`3) +Øú×ðWà�#Æß¶¯íĺ�‚mšW½¿K—tRt®“ùW³Ág?j¿üyý§õ‡ZUÃM§[Ëä"lüêx¯Øø7ãö"ñÏËûÏŽ>µò º„ MËór0zö¯[ÚÇ ‡²ø™ìÒ„hÓ}Ïéãá?„ ð7Ã]Â�.ϰÙCû¶‚ßøñ5Öë:ΓáÍ6m_W™-íáÝØ€+[•øÃÿ£ý¢®þ ~Î76^Ô>Ë}t +²ƒ‚ÈÃW‡J‡6‡•…¢ëUågàwü/þ +'áÚÅð£|ždL‚7•yË“Æ{dþ•ú·ÿý“üað{á+üJñRßZ@è½Æn=ðy¯å3öý•þ#~Øÿl´Æ·š[;‹Å¸¹™Ærªû‰Ï$×úQ|ðŸðÇá¦�àm23Zu´p€F Ø s[ÖÃÞJ»™ÕT©{(žûgþÍ?ðÔß &øc%ÏÙVv $yHÈ$¡¯åoþ +ÿ­øsû"~Ìöþ+ðÿïîÒeO0`GÌÄ‘ê+ûd= ~Á|íc¸ý�n¼ÃŒ9Ûìq×ò®ŠJP©'¡çdõ«�Ÿðlðhí|Y#ÌU¥ü£ÎAÛƒŸÂ¿i¿à«ÿ¯þ9~ɺö‡§ËrÚÛÉ:—ÀÆý~í~'ÿÁ´ºÓµûufhã¶3K1óÜ q_°ðY/‹zßÂ�ØûW»Ð]’KÄhNÑÎ +žý¸®L4ÚÚ=,oûâ±ü~Êÿ´OÅ¿Ùâíſ¨ÞûWŽBZ8s‚À ž9¯Wý´ÿm¿Ú/ö°š âÕœ¶’³ V™àd€Wé_üÍû8x/ã—Ä�oÅž8±KÙ¬g3JgŽ:¯ÞíëŠýÿƒ„?fÿ…þ ø�ü¢ÛéƒH¡ÞÝù�zg6×·,E9W³�Ìï§5íì÷>¢ÿƒ?f�Káìëwã›Û¸nSÄ/½<¡Ñ× +á�~œWè×üö®Ñÿe‡ˆ.Jµìñ˜í_­Ÿðn·í%à/…Þ$ÿ…®^9Ÿ[Qo‰N n1·ñ¶½\{ýË;1æ¤Ñý´jW�é–rßKȉ { +þ'?à°_´çŽ?l/Þüøek#Zi¢”.JŸ(äŸÄŠþŸ?à ?´å—ìßð Vñ|M™ZÝÄMØ’0+ø¬ý™¿l/Ùø£Å~5ñ´[¯/Y•ˆÏ$~f³ÃÁr#ÏÁÒå\ÏsàÙgG½Ñkÿ é:¼ee²¿�eOW ¤ïÛð¯õ+ðÁá}8§ÚÄGâ‚¿ÊãÂ?-4ïÚºÛâćʴMLÎ_þ™™#JÿK_ØÏãÿ…h�:7Šü=r'xáH&ìC(ã�÷q\˜ÈZjLÏ2NiU�Ã,çóÅ=ŸðY¿ø)F›ðáÍ×Â�‡3%Ʊ¨)IJ6 +çŒ ~¹¯Û_Žß´/†¿ µëWkh"µ�«³Ãm89ȯó}øëmñöÌý­µ-'Á2\ê©vÈ’ ²Æ…»vÇZÓ.ÃFu=¥GdŽœ“ µ©>‡×¿ðEÿÙ#Uý«ÿkVøÃâ‰%�,'³È2Pî'*=ëú¬ÿ‚»|"Ð)þÒ>ñÝLJ?g˜þ×$“2Œ¡m�IÚ +ä?n;ÏÚóƾ1€~Òh³[‰cX�-Á'ºœþˆÿàÛß„žÖ,õ¿kÖÑ]\4¦HK c‘�ÙÎx®¯þTð�…“•Ó•.in§ÂÜ#F$^CWË¿µíeðÛöZð-Ç‹üoq‰I†Ü}éô®óá·ÆxÇÂÆ“¨G²h +ìîÚ3×Þ¿—¯ø8cãm­ÅÎ�à�fuŽwhØÇ~Jà{ +ãÃÕ•ZŠ›90yzušÐý4ýšÿàµ>>|A‹À‚5³žæá`Œ ±ºg®sÓ�Ö¿lm/¢½µK¸9Ie>Æ¿ËsöÖ5{oÚ«EÔ4é+HlälPàçI¿Ó³áÿ‰t]sÁúf¡§LŽ“ÛÆF}£5ž*R£W‘lÊÍ0‘‹½4xGí]ûZxö\ðƒøƒÅ¯²FŒ´y需3õUnA>ã¥|½ÿø®Mà%¶‹!îÀô;³�Ò¿•ø'?‰/4¿Ú›Bo8™d’0N~è%{UQ¢ªRç–åÑÀÓtomOôÖ¶¸Žâž"X |íûP~Òžý˜þMãß ȹ®p y?Jö]Ç}áK+˜ú”g×Wá�üÛÄ×6Ÿ³­®‘):ʲ3arr0zvé\xj­ÉEžN„][Kc´ý�ൿ ¿hO‰Ö¿ôÝ=¬äžR‹6ýÊÃ8R2 ë¨#ñ¯òßý’~0EðgöŽÓüU­ÈaŠ9ðØ9ŽHÆkýþþÝß¼mà ?[¸Ôá‡0¦ü¸ì½Fk¯½””¡³=ôöhöÚÛö¡ð¿ì«ðÑüâ5YŸdHçjœrä‘è¿­~`üÿ‚Öü2øëñb?i�ºtŽ?›å�7sü8ÉÍ~kÿÁqà¡ ÏðcÀÞ²ðo�uK&†Ê�ä–<�£©Á¬ÚûáïÇo€z÷ÃÍ&FYurˆÑ‘“žßC_ÎWŸø$Wííà¿éÚ�ߋŮ›Èï™ä$/bF¯ê—á¾�ªxsÁZv‹­Êf¹·…Fõ*1\ÖKÙ»ž^24éMT§+³øYñ×ü»ûQŧêž1ðÓ/ÚW2Ã0ß�L¼WàwÄ„Ÿ¾üH¸ðˆí– cLŸce€rÊq““ëé_ë’è6ìüžþÞßðF‰jÛÿ�¾ ·…´«ùPÈCíp3ÏËþs[}ió?hiC§~cè_ø _À?Œ?¾Íñâé’×Q· Gª‚w ý;bµ?à´ÿðSg_\|øvS_Ô"uܧ €�óÍ~Õ~ÌŸ %øQð[EðmäB«X%Ç*03ÇüùÿÁW?à�??k¯Ú |aøo¨CEåý�ðã¸ÏRÕ†PXŽz»ô¥“lþ{ÿà–~øeñÏö¥“Äÿ´MêÊ&œL‰;ò[?3z�_ßÏÃ_‹_³·ƒôˇ> ×´ò°(Š(¢•3Ó€5ü†ø'þ Æý§4›¦Ô Õ¢²º íq6ÌgÓo5õ§ì÷ÿ:ý¦þüNÓ¼i⿵Ê[ÏŒ«30Ú„:õ b½|Ê®«sSÔô*Ñ£5ïÈþ—?hoÚ Â¿¾_üI×e_³ÚD\1?/N ¯óÄýº?nŸ~Û¿nu ë¹!ðì7_ê÷}à§}:zW÷ŸûY~Êú×ǿٶûá—så]L±í,~WØ9Vö&¿–¯Á¹?gñê5{h&f%åÜB=qò�×ð¯? ^ +îN +Tag©ûKÿ¦±GÂO‚zgˆ|?«iÖzÌÖÈfyåQ.äŸBkö×Áÿ<ãÛf½ðf©o©ÄŸy­Ü8•%>ÿƒ|~8è@[Mâr˜´*'¶¨ÜÃêk÷§þ Ùûø£öBø?†|U©6¡<Ç©bøÏSZRQSæ�S‹0�).u;³ô{wͶ¿¿à½v·—±ž¥¸,'�^+÷ ¿ò?í«û3GûV|»ø[$«¹a–<;àú×­Q^J}Ž<·VŒ¥±üòÁ¶M ¥†¿¦’¦r 2Ê®çÍ~˜ÿÁq¼¨øÛö<Ô4í2ÞI¦ÙLc8*3Î=kÙà�ðO=öÒõM:Îqt÷Ï»Ì n9ú_¢~5ðN�ãï\xgÄ�-ťʕt`Áú׎éËÛ:É_S»�„±>Ñl Ÿð@ÏÚwÀ³_Å­{Bø›xšm¥ìB6’L¨GÏVã�C_TÁu¿nÿƒÿ´K_„Ÿ 58õ7µQ#IC¸çwö¯¾¾ ÿÁ¿ ü]ãýKÅúv®Ú|7’X`]™çæ<ôöÈx_þ ØøM¤ø¶ßZÕ5i. I•ÝX–, çõôæ½/¬ÐU=£Nç¢ñØ~k}Ngþ ×ð7ˆô)×/-$¶Žñ¢ò„ŠW8î+Ú?jø!¯�¿j/�‡Å-kWki¯Ì`ÈN_МŽ+÷áÁŸ|ðœð-šZ[BЧhÆí£5êØZóÞ¤ªºËFÏ2¾m'6á±þzðT?ø$eÏì?áȾ!øzâ9, +ç%–Pý€<†â“þcûø³ãÇë¹6v:D«tÎ8ߎŠO¥nŸ¶Oì�à�Ûጟ|e'�Œ~Y6†À=r:×—~ÃßðOß~ÄÚuÖ—á;¥»[ƒŸ0¦×Æ1ƒ×�¥tÔ©zN/sXf1önïS¹ý¬¿d ~Ô¿›á_ˆä1) ¬ª2Gc�zŠü8ñçüÉð÷Ã>Ö5O j®÷KnÌ#@rÊ€¶x¯ê[qÏÉàŽîÙí¤û®¥OÐŒW=)J <èfSÙŸåO¯þÏÚ¥§íÿL.�/E¶æêlM¢—üãör´ý›ÿg 3CL›‰aŽixÁoCšùüÿÁ!~Ý|s?õ’ȳ›…Èç,Iéí_¯~‘�¥G¥EÊG�Ÿ@1üª±u}ªI+5ñ‘vå?‹ø-7üÆÞ.ø‡sû=ø&éííà×#çaÀSíŽs^;ÿhøÕû0|Ô.ücñªt·½IÃ.7nÏ'8èkú ý¤ÿà‹h¯ˆóüHÖ'–ÖîèþôpôÎGç5å>ÿƒ}?d�Å’I.ZW?6ì2ãýž„UF¬=›=ʘü;§È��¼~ÁHeÿ‹>'²ðOÃýb;‰î`QÆßLù¯‰à»_¼añoöU™<h÷RÂÞaØ2BŽI¯¨~Á*¾~Îþ+µñO�ÒF’Û§�óè +ý!×|-¢ø�B—Ãú´+5´¨Q•€#b¹9\j©ÒèyF”Ô©Á÷üoöððÿìgâ�cÁßÔÅläÆÛˆ6Ï8ÍCÿ³ÿ‚‚øGöÂÖôÏ|¶šòv‚Å,Ùà'sï_Ò·Œ?à‰²ŒüUâ»ý<‰oyŒp�½p0ZÍð·ü'öZðß‹-üP¶1ÈÖÌoL{šëXš^ÓÛr»›ÔÇДùúž1ÿ"ø 㯅¿²£ÍâÔx/o˜KYᇠ+ÙàßµÇü+ö�ý”¿hÙGˆ¬§-´ÛT¾Jõíœöý/xOÁú‚ôX4À–öð EUp|b¾uý¥¿cß„Ÿµ&�ŸñÉe0$æÇ§¡¯>‚q¬êU�Ó2£�¥ÎùÖ‡ãÄŸø.×Á/ü¼±Ò i5KÛf…ã*U·²ààtÅ~~Áß þ%~Ñ¿¶l?ìì&[A¨-Ü÷ äs_Ö´ðFߨþ5D:O˜±ãåu\êH¯³>þÉÿg› -ü ¦Ç’6|£rŽÀWoµJ�(îv,Ò…*mSÝŸEi0É—l’�®"PG¸´vŸZpëK]ôi¥�—r»¸Ü7­/ÍëKEiÈ„SšÙä<5F,Èî+BŠ9>ÆOSKöAÐãüþzŠ9Í¿üæÂI>¶øÊ©Ýž˜ö¯áâ}_Wðõìz†…#Au!ƒ`‚FqÚ¿¹Ïø/³FŸ -cc´K†?NEÚ¬Z¬«le�˜¬j�[žõ͆“QlúêUœ(Å£ëÍ?þ +!ûSiÚXYërD¶;@;Ûæ#Óú¯ÿÌý™<{ÿñcxëã¥Ü—ö²w9` OS_œ ?à”¿µ—ůÛx·ÂzSKkx¾fÂ!}‰ëô¯Üø'7Å]Sþ �¥ÝxOö�²}!$Ã`@b?1�ʹ1˜ßw’›ÕöÜç©�œ•£¹úµñþ !û4ü+ø{ã/émµiø¥¹Gœc¯ü¦ühÿ‚žþÖþ!jžÑõÙRËN˜Ã‘#¡8Ûž˜?OÆ¿r¿k_ø8átßu=3á¶Ë‰oÑ­áPr Œôï_ÅçŒ|M¬øïÄÚŸ‰µÙ°ú”ÒLË׎EFò-uõ4¥Q¥ï+ŸÔgüá7ˆà§šôÞ1øç¨=Ä6�“Ãg’Kz÷ÅO¿à›Ÿ³/ÀÍu|Eá %È –pÔwüHÁ%ÿà§>ý‰þÓ¦ëÑUù6Ž�“_Ô—ì¿ÿ«øIû@øÝ4·óØ!ôcÀ¬«×’º9+Ê{Eè~íÃp †.@}+ùœÿƒ…l[PømmÈaòÐ30ç#Ò¿¥�:þÛT´ŠþÑ·G*†Sìy¯æwþ¾³ðÿÃý:òø.çÔ÷~teòµX۹ǗÉ£?“ØZîÞÃö™Ð­²�2±$�[ 8¯ôîø_hëà½2V}êöÑ‘Œà£¥“wÉW¿þ3Ù|C³C/ÙˆF ' �ô¯ë«à¿ü;á/xNð÷‰ ?g�WÍg$�Ü W~sƒ�Z©Àôk×u´G¶ÿÁÅ2¬,ì|âlJ§¸ÀÀü´ÿÁ;¼Øk "‡fB7wät÷¯ØÚ'â÷Å�ø,/�>ÃðÎÍÓL´DU¿v@öÆr;Š÷ŸØ—þ;ñ/á�Æý3⟌¯ά‰"ðs“YƬ(ÃØÍê“:ãR1‡,™ýp|(“w€ôÕ=|•'ë�_‚ðp³·Â {{t-•ÞÙäa§½BžÐâðþ“�Ï•OÈWÀ_·ßìy¨~Õ^M&Å‘^4 —9>À•ãaêrMN[qö�Üÿ3ý]äÓ⸼vÚ¬ä€z®}j˵øËñ'AT:.½yl¶Ã¢vÛ�aœb¿¨¯Á¸ÿ|I-ž—¬›[iÌ6ÅAà÷’Ó¸;Ÿø6WÇÒIÕÅÿ™+°êÄ… +1‚Qƒô¯V¦eO¹ê:ñî3¾(Õ.xgOŽÏÃÚëÍxòãUé’é_Û¿ü‹ãW�>)|ó<]t÷G/2?ñz�ì;ø'Mÿƒg­nµhÖ¦ÖðÆø%œ#ûÅG'ó¯è³ö!ý‡ü%ûü5‡ÀZój o3 ’{c'�ÛšÖ®.!É ÉÆb¡n×>ê¹¥¤ÈéK]+k'=‚Š(©t�÷)lQEÎ]ÆQEL£$'+QSi �QRäÑGÿ×þú¿‡4Úwð~4ÚøÅ¡¿R@¡–—`ôýhB®Gƾ=ðŸÃÍOxÆõ,lãë#篠dÕ˖ȸA·duø# 1!jµ7yvÑà¹Ú $óÀµùKãø8Óà6…ªÉkáÍ-õUT‡¾÷uàv隨aêµuЧ”×’æHþ“ºž) +Œò:Wæçìÿð_í¿kpt 2m6[x„§ÌèÇ8etÇPOQÛ×íߊ_<ð‡Â^4ñ�Üv¶¶Ê[ç`»�e÷5„¾.I-NyàêF|–Ôôjp÷¯çwÇßð_ï„>ñ„š…¤5í²É䬡·ÝÎW°¯ÒØïþ + ð—ö·µ0xrd¶ÔTmË ü¤‚?—àjy$µ”t7­–Ö„y¤�Ð0ƒ4í£ªWVö°µÍÄ‹',Ìpã_‡·üÃà§ì­ã”ð>‹?öµÔcù +$]à�Êq–Àã�>Õ×BÓV‚1Ã`ªUvŠ?u(§Wóiû9ÿÁÀ_>'øæ xÎÓì·2b9Ú# +�rváœ�À ûWôCàÿø{Ç~µñW…®’òÆñÇ,dAúwëXÔTݤ…ŠÀT¦½äu*0)sšAK^¥:§ŸÜpRy£cRGJ]í[Ç—vᱨØhÞÔ»Ú� ;±¤cŠJRrsIK‘Â�ŒŠZ1Þ¹êÓ-;«!»€¸ðþ§+¦’ïon%$.Áшž ~Y|Vý¡>,|fñø§âûÝÞK#<İ)µ†¨9À×�Á{9sô3�>Y¹;«LÑÜ­òI€N×ÈçpsU~ܲéŸg²Yy‡;À?ã]oƒ|'Ä߈O�,I¨J±´�Î#8Éïì+ûøý‡àŒ?³Ž‘ðM¸ø…¢ÛêwwIæ+Ì€�­ÐŒŒƒZfåJÍnOy ÿä|)þÁø2Þ-º·+$ÒÏ"a‰b@äûb¿©jò?ƒß¾|ðÄ^øwa…”X dw¯]#äF.¤ÝI­Ï7]IݰfcJ…º5IEjé#‹ÚA=͵ªy·2,kÓ,@™¨õ´±°’òC…‰ èzþ&à ðT?ÚÁ_uO +xOZòí¬]šá× ÜHØ¢Ž1ëUC í$ãsÐÃaDÝÏí²+ˆ' uqê¤åR×áŸü£ö©ñ§Çß„“ßøòé®nã›Xî`„.ß~M~Ük—§NÑ®ïÔàà ɟ÷TŸé\5!Ë'Ð+aœ&¢ú™çŽ|!᫤²×µ-epI$U8Lñɱ[€˜{×öƒûe|'ñOÆ€:÷€< ŠþþÙ㉎1–Ãg ƒ_ÈWí)ÿJÕ~þÊz·ÅïmmsL ,Òno˜7@¿NœÕaekKsèr—N+]ÙõÏü£ñ‡Ä)ñ&«á}rþkÉ«I#<…Ç vIÇ^•ýPüRø…¡ü/ð6¡ãOL°ÛÙÂÏ–8ä{×ñ×ÿãŸ/­@ ##+À+È¿¢ø+mõí—ìcâFÓ�35øYû‰Ú<Š——¶(íÎ>fŒ1¯äSþ ;ðƒHý½?k;Éþ+"ê ÎÒ¿šIç=ø¯êÿö­øc¨|ÿ‚eM£,M,Ðiq¼�P‘ŒŸÊ¿„ŸÙ#ö§ñ¯ì ñ®ë]ðéf¼†è† +›†�ö¬²ŠNxiòîe…›äq�ý&ÿÁn`ς߳Á /âGÂX[OÔ`¸O1A�Ñ€P0zú'þ Ûý¡>%|@ð¾±ðëÆ—Ò]ZÚD&·W%‚Œãä'õ¯çÃþ +ÿeøËûkxr/]Ù²[ȰFEê>µýÿÁ½_³¿‹>|4xŒÆ>Õl*Änç“ëKJT°¿½Þåâ£%†|çôÎrÁ¦å©3ÀRRgÄKre9Zu5>í:º.HQEÞ¶7[Q] c¸QEì ¢Š*\\ì(¢ŠÉîHQE€)AÁÍ%¼vIÉÍ%S¢Š+)7sXÅX(¢Š\ì|ˆ(¢Š’‚Š( Š( Š( çkþ ÿw%¿Â dˆÜ±=1Í?¼iÃ�Z'�õ'ÛS†vô(ÙýËÁ} i> Dû† o]§‚ã_À–¼m¦¸.ÙÚ¬Üôç]4}øq?ÐàÏüóör²ð–—cã�F—ÈDýÛr¬ã_¯_³çí!ðçö�ð±ñOÃëµ¹„¤3€x¯òIñ[Ä…RáüÑó ŒHÉþþïàÜMGW€Jš‹•�Y�+NGé\xÜ :pç[†' MË©ý=óÞŠxeèzÓ ¯6j)]3ç’è|ÝûFþÓŸ ¿fo 7Š~#]yH²¢õ uë_ÎíÍÿîøk«ü2Ô¼ðªÊG½½F�fíÏ8ö­ßø8»ÄPÝiš^“a|É%¬Ež$'“×�5üMjºÅ«ih˜4Ó+•‘@*Äg9¯C ƒŒ©)Øú,-(ÆÖÔ¿ñÅߌüUyâO¾ùnK>P`€yúWWð3övø¯ûFx¢ |.µy^r“:¶Ì ç�8¯¼½ŠM2K»bÄHRsŒ×ö{ÿÝü5ð¾§ðöÏf¦e‘THØ,÷Ï­1¯aO™{+§&~Gü?ÿ‚I~Ô?uË�¾.Òf–-$¬ÅcVeUBNï˜c5ýü5ÿ‚åüø_à-/ž4ˆ»qÒï —+íí_Ð?Åm +Ç]ø{ªi·Q+«Û¾Ppvœ{ÿ1ø(GÃ;ÿü~Öl-¢–îÖiv§‹Jþ&¸©GëZT2õV:£ýdÏÚ÷Àßµgƒÿá'ð³"°8*ŒNy úW×¼ž•ü�ÿÁ»þ=мðõ´¿ß‹fgdT˜íàŽ¿NkúŸ>˜üÈõË6ƪr}1š–Õ)¸t<¼vÓ÷‡£cK±«„O‰þ`¥µ‹T.2ʪ"x«º�¼«+¶—ªÚÜ8c¨ÁO¾_Y‰Çõ9ÿ+,xİ𞥳ï}–\c®všÿ1ßø(Uì—´O‰u+rŤ¹d�u²Ùžâ¿ÒËâg�<-¤x U½¾ÔmâAk.ȧ�‡§<×ù”þ×F|zñ­áûyîlü÷"EBCÇœ +ô2‰sÎHú ¢›Pw?¦Ÿø7‡Ä‹sðú]?wï·`§~ WõQ­Ù>©¤\ØGÖh™9ÿhb¿�?ø7©bÒ4íV}AžÐ�«²a·8bwq_ÖÅÇÄoépƒy¨BŠÞ,ãÔŽ•åcãˈ’fY…9ûDâ¶?›Ú¯þ‘­üuø™'‰´›–µ‡{ž«äíÎ:ëšýRÿ‚rþÂ?ðÆŸ cðÕÝë]Ýnc¼�¸RK�׿$×ÙOûGüŽí¬Oˆ¬üå+¼>•é~ñw‡|YkößÝÇuèÈÀƒùR©^¤â©Ïc,F.³†ªÇLÏj˜¿çÿÕLMJõ¯B”ì�qêME àb–»ÔW-lQE?fÆQEÍ€QE2§ š¸Ï,Ÿÿ]ZÿŸÿ]>ŠàžãHÿÑþû_¥5€ +)ïŒsHFTWÍû2¥¹yeÁ¯Îø+ íý†œ¡ë\Ó¥ïÅ®ç~\¯V'ó­ÿßA¨Mñ?T–xÁL—\»òòO§¥cþ>ð†~%xfãÂ^-¶[«+¥*èàƒ×­ ¿ðmìÉÿ þ­ecƒ°Íæ¹'s ƒ{Í[_>(iŸ¾êõ…/m¦Gæ8Q“Œâ¹±ï›$�[8‹uÒŽçÏ`oÙëösñE׊>iIo-ÑÞP¨Â¹êÃÞ¾Vÿ‚Úki±>³o8 Á*À÷ +2ükؾÁM¿g¯�VŽ÷—ñhó,†5Y¤\1Î1ì}«óçþ �ûM|*—öe½ð%–¡ows8 �X—ÀùHlð:ñÅg„ÃÊ5T¤N•EZ.kcòçþ Íñ¯ˆ?á!ñF�~ �2· )ÂäWçÏüÒƒþ +sa;íjŽ\ôû¬1ƒëšûÃþ ŸÑÞïâ߈­ïÏ�·fNI!8#>ƒÓ½|�ÿêðÅÏÃÿø(8×m1ž¤‘yì­·G=k¥A¼lùv±ïPjX‰[±ýÇþÈì»áIvüÍ6';ŽO*9¯à'þ +­‹ÁFåšÉÀ+¨/ÌÙÉË�¦kû)ý‰nŸ€þÈžmWYŽ;»&%’8f;3…íŒwÍÿ·oÄï +|sý¼çñ_�¤6­~» Gp,¤…<¶Œ£Ï̬rà)Ê5§&»ŸÚ7í7ñ[RøiÿÁ‡_ÑvÆeÑbFaòíf� ¯ä�þÑû(h¶í/­j?¡K‹X. Œo‰ÎxäWõ ÿ ðάÿðIÉM²ù¦-6ÀÎB„ä�ê¯ç#þ õý¢ü5ðSãÝÕ‡�î¡¶‹Q2,BraGÌO¡žià*N8Iòos"ýÜš?Kÿà¶ðNÿ�Ÿgøþ%øM‹M’Ù’ÚÁÏ<ú/ô?ü™ñëÅÿ~ê¾ñÀš=5T¨#¯Ò¼³þ +ý´¾ ø»à›ðïÀºÔZ”— ØÚ¸�;xšÆÿƒgl5K›ox�ì�mc2(�Š�²·qúŠºêr§Uërês}Qóî[´S›­6¡ŸÉ“îÓ©«ÂÓ«qQ]‘—FtQ]kc¸QEÄQEQE`÷¢Š)QEºØŠ(¦EV2ÜÚQRPQEQEQEQEüêÿÁ~ä‘~ Çå›`Àúkøzð��ßâ��4ïiĵ�#³;`çó¯îþ‘£ø-lð¦ç1¾?à$gù×ñŸûF&ý¨¼,%‘=ü (•O3’? +æÀ¿rRó>§ èųúóý”¿à‚³»|5²Õ>!@·sÝ[Æçrõ�KfºßÛ"oÁ&> ›ÿ‚ùP¶A e?CÀãÒ¿{>é³ižÓm›Ø÷p1_Îwüiª$_í¬çV1ª«��ws’}€âay§Usꎛ¨ÖçâÅßü¯ö¦Ô”êwš•ÚÇ! Á¸lÙ$~„Ÿð\�ÚÛO•ïaÕÚÖÆuÙòrG±ß“ùWáå�¢&ŽÒÉ#¤úWó'ÿÓ¢?´.·{¤ÂA á¸nœWõÿêMm7Á·X9hB©Çr@'?Ê»sšoØ_¹é×|”›?§‹ë(u I,§ÎÉS�C_Ÿßà™¿²÷Ä]RMXТkù¹›$·\ñþú ss�³Ý\¨ƒsa_�_?೟³�Á_]xè¼÷v®Qœ‚©‘Á÷ãèkÈ…:‘~æçÏ`£Vø?ÿMø ©~ÁúÊë ®†›§N �P‘óc9Àõ¯Âi¿nÚB_.fñ$¨‘ò¸Èçð"¿Z¿nßÚÃÆ_ðToŠ0xà^™qz-™­Š² ÏË´úS^=cÿþþÙZ¾ŸÖ©Ø ù…c °øq^¯¶P²ªõ=èÉ­±ðjþÞ´È�ˉ¤*�Á›$Ÿ©­½ þ +#ûMiׯkmzRTd®ù-ޤ)Áükï(?àÝïÚsË6Ù÷1z3ŒçÒ¿>?jߨSã—ì­©ÉŠâ nÿ:ÊS%ãpOô¢–"œådkõ‡²g³|$ý¸ÿi/�ŸtïƒÞ:Ö�Õž«0WuÜŒ8ÁŽýq_ÙìÉÿ´ý››áåž«ã 3íWòÆ 7™œ’3“ŽZÿ;ï‡Ûá·Ä]#â-¨fû£jôÎNêþÅÿgßø8áw…¾Xø{Å«_[D±3ÝF7pÁ¬±të]{- Tç?€êà©Ób Yê_Ù´¤š’F„í`CaIÅ->2ý¿¿jÛ¹ÛK²ñMß“ ,Ufe'=7û·þ +{ÿX¹ý«uÙm´;X—O„gr˜ÏñŸ­~jz•õíÇÚ-SΞëlp*ޤà�ƽ4’‚SÜÞ�[«=Ϩ'ý¯þ6j­î«¯^Ms.TŸLðp{à×ömÿ ø×ñ+âçÂË˯J'ŠÞEX™X°*GÌ3í_ÏŸìqÿø÷ûQx×âGŠ%þÊ‚íT…s�´÷Q�½�\WöKÿÐÿ‚|è¿°o¨üar×¶íÛŠŸ¼s’@'ë^vcŽ¥?ÝSz˜b1)E¦~žGÞ‰;R!9Å,�ªit>bkBJ(¢½ˆl0¢Š*€(¢Š(¢ŠLŠ(®7LÿÒþûÈ K�ŒP(¯ ¥s^Up¯�?n_�úÏíû7x‡án‚Ê.o↠Q�zúøœ ×­x÷Á>½M;^Õ­,î$û±Í2#À�kƒ+KC£)FJPÝ„ŸðH/ø&—ÄߨÓÅz—о K�µFâ8צ@‚8"¿w|wà}â/ƒõx¢¸°Ô¡0Í ‚§‘ùé,¯-/íÖêÊUš)UІR=ˆàÕ·8®xòI:’Ü׊�IóËsøÌý«?à†´eÇÅ»æø yåh÷r™áÃ2"¶r ÐÙäs_,x£þ?ÿñF±kŒõ�©D>YÙçyŠFHÉÃ�ÛŽ+û¡ñwÄOxÐÞø»S‚ÁÏï\) z¦¹?ü~ø7ã¹b·ð¯ˆ¬îå˜ád‰ôÁïKûN²^G ³*üº£áoø&wüŸÁ¿°·�ÚÆmZöö«Ž ;¹9ê;zTðQø&OÂïÛ‡Á7ÇÄéݨsÕC“ØsÈæ¿T“îÓ&dEÞÇQ5&¹¢ìÎã*)óÇsø§�þ ãý¯ôÍ!tOøäÁl©°F%ØŠ:àz +úCöÿƒu߃¿ÿá;øùq¤Ö÷>h+óÁ™Ï&¿¥Ýcöˆø- êgEÕ +xÇ_ &øUâ5ŸIšßìþQè£nÜ�q_ɧÅø7[âN£ñ«VñÃ�dXiJd´XÑWb“Á,|ÙÉ œó_ÙVk“ñŒ|)á ayâ�BÞÂ68 3„ÉøÛXðm¼˜K{†‰AÉÚA#<àp=kÒ�E7§CÖÂ%=bgŸðm´²| ÔõRF7$$c’Sj†úué_Ó'Ę$»ð­k !ä·uë’1_Í'üB.­~ë¶·½|è„}²›�üXšþ¡î­á½·{YÆQƯ6«ýäŽ<ÎV¬Ÿcü¿à¡:GŠü?ûNxŠÀÚÜMÓHÐRWˆ¯êƒþ Ððv» üšï^µx¤�„±n]¹RäŸ|×ìoŠ¿àž²·�5yõßør9´ŽØ$–98È8澚øiðƒÀ 4H4Ø%•½ºÑTº:1^�\[ÄARå±¶'5„éòõ:?F_º‚€mäGÊy¯óÿ‚Šx[Å?µ[É´ÙäÝ3I¿i` ê2;f¿ÒšX’hÌRŒ«õ¾Iø�ûþΟugÖü_ Å=Ä‹°°x8Î@ôïXÊÊNlÃ+ÇÓ¤š™üÐÁ½>ð®—s¯ë1Ä—¯¶X„¸Þ%Iç¾Gý}[jz4ϲÚxË‚¾˜¯äÛþ +�à+_Øò/|Ù1Má"m»~l ãä“_�?±OüöŠñ/ÇïøGÆ:”—V÷²³±.AÝÆ¦8¬e„ö‰Ö�èbp�«ï&~[PŒcƒ_Íü ð¯Zñ_�´ý_@µ7(dŒ„Yd8ÛÈ#ýxW}oÂÖwÒ�Ìñ!cêH¬Ÿˆ +<ñ.Ùm|UiÒ)Î$]Ý: t¯2 ¦¥ÊÃVTj5#ü•µ?øªñæ\¼PFÆY|�Óªôê0kÌ#‰ö¼–©±±µ¾§¨¯õý£?`�ÙÓ_øi®ê6ž³µÕÙ¶]$*|•þr¶GÃë_„¿u-ÅTÆ“HK'Ü''¦=îRÅ*º.‡¥Jq¨ÛGÅ.u8®áÓlmfºyNШ +3�ʾ¼ø ð‹âTß4øŸÃ—èPM¹^Tl3´~8ë_µÿðAOØÃÀ_"üG°Žò5 Ѥê9!xÐ×õÑñÏö1ø1ðŸRÒt +ÒÎkx‚HP0yÇNs^f78>D8´�™ƒûü}ø9¡|"Ñ´ïaµ o#Œm"¿B|3ã/ xº7†ï¡»Uëå:¶1Áâ¿Íçö³ø¿ñ;àÇÆ OÃÞÖ¦·µ°�“b± +Áã®*þ‘ÿà€¿>!|\ðmþ§ã9ÚyY‰YÈ+•, õ'œ+(`åM{gÔ19te+êM>j{õáÀÅ-zôè³æXQEéEhES¢Š(¢Š(`QEs�ÿÓþü(£­áT^ï25ûGäÿüãþ + ¥þÈ?'Ó|;w +x—PC sÊ»µø!ðÛöÿ‚€þÛ#ãÕïŠ^$¼µFÒÌA��1œ~•äŸð_�s?¶&›gur12lFcŒîÁÂ×õ‰ÿéÒ¢Ò?cßEñÙ‡#Ó=«ÎÅU•q”7gÖÊ+‡Œàµgâ§ìkûf|Sý�>3Ùkö›¼yây<¸d–@Á2xÆNyÍA?~0Ëðóà^«ñ_ÂÐ�A­lÍă%ò20+øåÿ‚°jöVŸðR{S¨H#T¾�ÈüFÜûWöeð†-Æ_<>o­ãžÚâÂca•?(íïYãb’„Öí\Ç2ŒcËY­OàûÅž<ý·ࣿ´%ç‡ã¸¾�/¦Ä0ýVSÀ>þµô�ÄŸØ÷öÑÿ‚oèvßa×'¾ÒmäV¹ŽW#Ë|qž9õ¿²�à¿Á…š­×Ž4-ÃH�Ô´· ¦ä’{WàüWöÊø1¨~Ï—Â� jö·:”À<À8åGEœŸJÊŽ*uk{.[DÓ[Ú>[hϼà—ßðQíöÊð�Ðu WûkM€4ù,Xã‚K7ð¯ÿ‚ÁÁN×öYðªü,øs oêãÉFSó q€E~?ÿÁ³i{«xçÅ2LnÌ ŒqœcŠùWþ G¯j:ÏücM°¸!Ŭ‘‘±´ã®Eo…¦£^pZ¤iOIWÐú�àoüöÎý¤<,Ÿuo\ÚËzíHÀ6Fà:÷¯eý�oO‹²�íß³WÇK£t…ÖÞ&v8P;×ô×ûƒoû2ø>iÀù´Ø�°1Õ}+ø]ÿ‚¦j2è?ðUuÔlÁŽ9nãd=ËzVe,Kwèc +j¤ålyÿþ<øOá' Ÿ‹ºì‹ö{_´¯<·p¯ãFOÚ?ö®ÿ‚¼þÑz�„~_OcáÛIš8 9�V=ØÉõsÔú×î‡üÃIJÅÿ½{¡‘4úD + Œ´@gé_�?ðlµ¿—ã­NÞEÐA!ߎ>cßßÒ·Ã?öIVê��ðØxÒ¥)­ÎÇã·ì«ûJÁ4t«O�ø�õ;UG$=Aϧ¥AðMŸÛsBý±¾&¥l¯þ˜‰ÛÏß7§Jù³þ Â~ÅWò: !;»+óÿþ ³ñ +·…u� +僱 ÑÝTŒñÚ– ÍKë{ØÎ¬}¶Î]êêŠz¨#&�å�óÿë¯zl��LŠ™#ˆÐ¹è9©Nüg.?º•sc_-6Ñ­wf/ðXø(�ö‡âý�~NÂîñ¼‰¤Bv¨c·�;úW†|ÿ‚êßü gñgSñäz¦¤¢àŽ yœ†çŸ¯µ~mÁE®§Oø( Ëç+±¹GS*€¸¯îoà>¹£Þ|Ðu 9ãdLLæ6.#äñÒ¼lÂs§*N×ZŸqˆ¬èÓ�²?—ÏØÛö ñŸìûS\~Ëž7¿:•Œ—R–líÜØÈç¡ý+ûÒïQ°‚þ?»:,ƒ>ŒοÎ×ö ñí¾µÿ5k«k•*oÒ3&qÀ|Wú +ü%½mGᮃzÇq{9õ zu(¨U„–íjyÙý4á�sÑ_­2žýi•é½Ï’ +(¢�QX=À(¢Š@QEtQEQEÎt ¢Š(¢Š(¢Š(¢Š(¢Š)=€þ|ÿྌáže·v8–*;d 0üE_±¬ÿðÖž È[˜ÆÜ`6\qï_Úü +ó¿Àh-íÆæd“o׿æ+ø¸ý�¼I§xkö–ðÖ·«§’-obÆîÃ8ü+— M}^VßSë0ë÷?ÔÇÀ±|)aÿGŒþ%kÎhÚ7á§ìÕà·ñ¯Ä{±kl3´‘Ø{Õ?„Ÿþø¿Àv:¾ŸªÛ„HNç +~U¡5üÄÿÁÀßµŸ‡|SáØþø6ö)ãµ +ìêüïÏÌáÖ¼ì¾ødyTð\õ¯5¡÷7íUÿÀý˜cøC©é Ô&:�õ»ÅåíÃ�àŒdn=3ÍÖP>öüž}«úZøÏûW| ð¿ÂÍk]oÙ\„µ—Ç(ÞÇiàɯ*·ñG•ŽŒªUØù§ã§ü�àÀßOá}NU¼»G QÈIêyà_i~ο´o‚?hßEãL¤8ËÅŸ™ó¯ó3ý³¾(ÉãÿŽSøßJ¸‘­!‘ÌaŽr3À¿¨Oø7ÿö¥ð•Ç�®¼/âËÿ³Fy”ðÜÕ[šÐúïÅð\/‚¿4û�‡~´Úžë5Îì¸Ç +@Ï_¥~rxÇþƒñö¸»ºø�uqª³³ùnv3çØWàOì}­A«~Ôž·œˆ YÓfx!�¯ôÉø£þÞœÊ>Sg8êvŒšíÅÓXi/gÔôëÊ4!x-Ïæëà¯Áëßø$‡>Ùâv7–Qò6žž•ßx§þ £ð÷Å–³xÂÊÃRÕCÙ»o˜�¸ïšú'þ ®-­ÿgËwP¡äi†;Pàþü$üñÍ·ƒ>)i^#×fyb‚TgÝÆ0Çå]‘ÂÓ­Gž¦æô'6�êZÇþ¥ã?Úæîo‰Þ&½‚Î é¼ÆîS"0ÉûªsÉö¯ßïØ3öðoìIðþ? xno>G•ù<àp8 q‘ŸzâdoÛƒö|ñ7Á� + ïÙØÜ­º&É¥DÜp>îq_¡¾ñ¯†ükfoü7wÜ ýøØ2Ÿ¡ó7©)(Mèqãñ¹ZKÝ;001KEõt©Ù#ç.QEjEPEPER`QE`ÿÔþûÇJSE!8¯ +? ÍVçðAÿûÓíbýº"TA"Ço”¤å·™kûý‚’Eý’|ffÓã$~ülÿÁy^7ÿ‚‚Ax¡—ìopGïϦêþÊ¿`ýBÏPý’<uiŒgFÌ3‘\Ý;R§cësþËÿðY2ÜÿÁJ§¶²ÈtXeQŒ’F09ìkû@ý‘í¦·ý›ü ßxi°ç>ë_Ä/ü_wÿ‚’Kq£NHŒJz*í8÷ÉýµþÊÑØ~ͽզƒL‰ËÈ@ +¡z“éŽk t’�&×B³jìÐKs¥ý£~럾ëðõùÓnõ( )8?s=M _ðRoø$Çǟ٧÷_¼G~|I·�-ГåHËc…oN8¯ôÑüI x�]´;ÈnÄxÜbpøÏLãé_Œ?ð^«Ý"ÓöÖùÕ&gP»�UfT#ð*Ã.ªþ²—s‡-­58Òhùgþ Þ¶ø7¢ü ½¶ðÙ‹þ&/-ì€e‚m�·\Wà—üªâ+_ø)Õ½±Rò$égù‹>ö?ý ÿƒeàŽMsÆÖ¸& mâeýà²0$Ó¨¯Ëïø,…ø´ÿ‚š\\FÇu£ªÌDù‚ðsÔä We+}v¬WcÚö*8‡cûéý’ uý™ü'äÓ"  â¿„¿ø*ä‡þDÑêxŽxÊŸC»µu¿±Æ²5¿Ù¯Â—.»vؤdöF+ø"ÿ‚³jbø(õìöN&Qp9 ó‚?*Œ‰|^‡YüiÜþ—¿à¤rYÁ+¾Ó,„¢év‘‚{nŒt¯Í¿ø6uçˆüIy°‚¨8ãjŸækï�ø)½Åßü;«g2Ç—bòL{…‰X¶=ðkóÓþ ~ñU¼^6ñF�4êDÖhËÐwž=‰'©?öz�/õyµÿð]Ù|�ØkV¹f!VM§Œçp¿.?àÚiä×ÞâD01�ñ‚ |6}zWêüföÒØ«Pµº•ÎsµòÅBýßq_–ðlµÌÞwŠ!™—v1Ÿ› +r3õÜ*ë ùpŠÙ͇vÀÉ]Oì>í:šŸv�^ôv>5î@y9¤¢Šç’Ð#¹ü�ÿÁeàš3¾ñìß´gÃ8.5I.t–Ðd(çJü»Óÿj¯ø(ÇÃO„÷^ ²ðÕþ™e B¹a&ý§�ߎدîËÄß> ZxÖ?†>(Õ-ST˜nŽÞr¿6=2iþ>øqð«ÄžÕ%Ô4Û9me·’W‘0v!ù·Ž1^*Æ%ê}= ÂpIN'ù€ü)Ôu/~ÔÚµã d›R¹Õ]eÈpùÉÜ>µþ£ÿḷøSáø®,,`(ñ¶°Ú§‰µ î ™Àvl~^õúgðþ§ûbü}ð=¿�ì, ¬2�èÍÃ2¿NP+èSÿï~ÙÚœ‹j7ÄRòH¨»p;y¨©Š‹vmhW2î~ +6¡h#ò®¦Ú§€Ø#ŸJÍÔ."{o&Õ·‚0Øãvzƒí_¹Ÿ?àߟÚcáÆ…7‰µI¢¸¶€y�æ¡#rŒ’67 {�_‰þ-ð'‰¼ «ßøwÄGå¬í·ä8=sŠäŸ,þs).Ä~ø‡ã?† ¨ø;SžÐŸ•„mãÒ½ëÄ´‡Æéëe¬êò]DQ@,ä2ã¶Aïë\OÀßÙ»âí'âx<#à =æI±åŒn!±Àõ8¯ÓŸÁ¿l‡~“ÇݶRÚb‘v�È'¯NõÓJPŽ’z˜JkšÍŸ”—ž —Q¶ ­ÈP9çŸs[Ÿ ¾,|Fø[zoü«M`Ò¶Z5<õçŒ×âÍRð¶·uáï*Ç5£2H;C‚¾‘ý‘¿b�ß¶WŠSBøu[EÎëƒÇãòzûU©¯Š{Zjfx¯ö–øëã‹i´¿k—/`ÝmâeQ ÿm‡ÍÏ¥x¼³`G¿Ì-gù†08÷¯ß/ÿÁ»ß´G�|(ºåƧrdPòÜ•D`.v©ÈgÎ;(úWáW�<↞,Ô¼âXçY­YÐ4£¶’QëNšRv�—6§î?üöðíWãÛß|EˆOdþé†~U=yâ¿­‹ðMÙ§GøW«ÿbèVö—Ù•'Š1æªvœãÖ¿†ïø&7ühþŜֺ„7C¹G‹û‡$îö=«ö�â¿ü+yñáÖ¥ðóÁ¨?¶µò Xò_ç½+ÄÄá+Ë¥OáUNecñö‚ý¡~.ü4ø‹©ü3ðæ¿wŒ´1íf +Ø$qôï_ê:·Š¼G­Mw⯵I0ùƒ†n}GÖ¿tþÿÁ¿jÚ÷C?5iÖÊmU¼õóÃ+ÿ0 â½÷ÿðl·ÇY$3k:ÒC*Ÿ¾²ç#¸#ù×ÑÏI¤”‘èÔ”ZÖV? ¿c)§ý¨¼<‰þ³ÏN¼ã,1Ũ¿Á{gƒá¦‘nü¶ŒûäWò×û6Á»~&øKñwKø‡©k›šÎEgó@|rHÃþB¿­/h‘x{E¶ÑámÂÞ5�v1�£åVýä•�1«”S?ÿà¶šU¾¥û8<�æu�F1ÎBœàöÀ¯óÃÔ´± ôѬ¹�\ì®êÿDoø-�Ä–ß³ªJäRG@Hœõ¿Ï/ÄV÷þ ñ“Xè µÔÆ5ò†A òk·O÷|§U(9aâdxÛÃCíúv§9’ÐXÚBxÇÎ�}zV5jI¥ÆöØú*YÂäQšØÿ=Ï…Ÿiø(çíj¾7ñ�q§ÉªL’ÎåÂ0û»†Çï_Þ§�¾x–Ùqþ ø&ô[êvÚ:éö× Àܨ'Û�^ñᯅÿüv×þÒmlç|æH£Unz€@é]°8æ¼ê÷”¹¤a‹Í¥Q«-�àA¿hŸø(ïü‡ãUσ¯¾×¬™¤Ú<ÈÙÒP¤íè1ÎN>µæß·oíáûd~Ý´ð?�¼1y¦i¬Ê ¶DÌ��¤¶�ž�¿/ï«Äß>øÏUþÜñV�e¨]ñûÙãß/NO¥aËû9ü +™ƒ¿…4ì«…Wõ#3íZ<ÅEéNªY½4îã©øÿÿ~¾?ýžü«|Qñ×É7ˆÑ¡ÏÊŠÅ‚Œñõü«à?ø/·ü/âF£ñCRý­üdºŠóbÉ —Qݹ”g8=[Óý«iÚm†‘e›¥Â–öð¨HãŒUQÐ*®·áýÄ–-¦kö‘^[¿XæPê^u'R5]dõfó‰*¾Ñ­àWà/üþ +Aðá=·Ã�#Ã÷wÚe´mI$eÜ)à0b21Ö¼#öoýŽÿi�ø(í2Þ;ø…§\éóO2¼¥“W<ôz’kýåøð‚x¼™|9§•ôòü+¡ð—ßxKÿ†•m§yßë ªÇ©šïX¦“Q�®uVÎi¸µ«>=ý¢c»_‹ÿ±½÷ì×e(…åÓb³�‡#0 ú•¯â;áWÃ�ÛCþ …ûDjº/Ã�òúåK�û—häMÜ7ÊppzgŽõþ�aÿ?ιkïøGQÔ­¨é–³Ü°Úd’$vÇÕ�5…R”y#³8ðy«‚垨þÿjŸÚ#þ +ûuK¦ü6ñ_†nìíÝã?»�ÀÜ0¤·\är}À¯éëþ ÿü›ö;øYý½â€ÃZÖY‘ò�â¡$œ�¥~¸Aàÿ ÚηÚe¬R/!’R>„ +êÆ1Åvá£*�sh�±ù²©HFÄË÷iÔQ_F�¯EW38ŸðU�Ø+⎥â!ûDü ½º¸Ô’Ush¬KoêH#^àvþ_ø7Ä_ðT�x[þ%æ�}¤Ùß/“$ÎÌrŒ6`žOþ5ý”¼i"ì‘CCÍ0XZ«nX�ßh®ªÊÖÐúç²T½›W?!¿àžðM ~ÌÖOãß"ê(Ô›4®£!›?Ë&¿_@`p)HÇVôhrêÝϽw7vQEtQ@QQ)tEÆ7 +(¢¹¥Ê=‚Š(®Ã0¢Š(¢Š+œè +(¢€ +(¢€ +(¢€ +(¢€ +(¢“Øçëþ Ûe#ü[¤¼°Ù_öB±'ó¯á“àv•câ?�Þ�Tp±4ÊÒoÀ‹�‚ON+û‘ÿ‚ù\~¤’bpwãŸç_ÁŠÞêk©X¸I¢û�Œr2A¬p:ÓåógÖá×î"ª§ìë…,¾ xrÇÃÒEåEeUB¹éžÕîáþb§¿½š'Áø+íWðKìš6™¬<–0D³åÙ6ŽBäðAè+ûÿ‚K~Ú^5ý­<>³â‰ÞæE#s± ÉÉ9ôÁ8v¯„pNg|ºê\ýyñ–—aàûû+ØÕákyT« ƒ•=�˜'üëÂóø{ö§ñ-¾ƒl ²–w�ªÇÆÇñÉ?ZÿP/™SÂ7ëËùŒôèküÃࣗWßíUâ�Îà=때^?"Etä�Zܬ½6ÏÜø6“Ãþ ¹�TÕ<@¶°Ü@Ìðî*äüÞ�+úçøÕ”ÿõûÉÞ£ŽÆy76 +à!ägÓµ–Wì÷ûY|\ý‘¼_.«ðþàI¿4�HÍ·w¨ÛƒÅ~‘h?ðTÚ—ö¡ñ‹ðN}Jkk ö7•+…Îzôäðiãpõ]KèMj +o™³óçöÐÒô ~+ëçA·y%iˆi‚’€ÈÜ‘îEX_ðlÇÃhtO†ÚÏ‹.­6\3ciT)ÛŸ¼2+ïïÙ»þñû/j ô¿|G±“R¿Ô#ŽâbÅpîÁl©Ï<ŠýZø#û<ü/ýž<='†þéÑÙÛÊûܪ(bqÜ€c[0ö�öq0Äã!Ù÷Ä=×Yðuý¤Ñ«‘ ²“Ù€È#ñ¯ó ÿ‚�]Çÿ 7â6!§fE#üÌ;}+ý@ü_8‡ÂÚŒŒÛqo''ýÓ_æ;ÿ3Þ~Ðúà’»w'•Æ víÔqÍo”ÂÜÃÀ;ųó+T15ÒÙE™%•JíQ’8éÅ{¯ìÓàæO�¾Öîì.bƒíËç«DsˆÈ'ûWì×üWþ óà/ÚÃÇwž7ø‰h·VvÄŸ%Ç]¬sŒtÆ+úÊø£ÿÂý›¬>_]xGE†ÇS³·wŠu!ÕxobqÍi_ +u=›Ôïö°O•³ìïÙŸÆ~ ¾ø'áé4»»x‡ÙzoQµ�Q×±¯¤ìî­¯#ó­%YSÕ#ô¯óEøÏûP|wø#ñ/]økáÜGoe+ü±ÌT�¬AÛÎ=Áý?Á?j/ˆÿ|©Yx¦þk»hˆ‘RáÌŒ�²1¸õäJxO`•Fôg6;*J¢‘ý#ƒ’Ec]ø‹AÓæû5ýìȆIOÄŠ³}vl¬f¼Æ|¤gÿ¾A5ü!ÿÁJm¯ŽÚÇ�PézÛBQ¤�Cƒ´ ô:ºRu$£ËÁ`ý­Ógë÷üOã�„¼Qð¶?†^ »×ÄK¯lÛÂ3à�qè'Þ¿Ž�‚>ñ•�ÆÝÜh·ÛÇ!Þî‡Ë$Œd1_ÒÏüûᆟû[ÜKã?Šlu%\°Š_š0Àã8ïÆ:þ�d‘A*Ë×ÕGìäé´{5+B’Tdi~ÌßþéŸü?¢ÉªZÀélˆŠÒ*ƒ…ŸJú¯JÖ´­j>“n‡7/å^Ñ3ú)¢”uçcïB7�á7f2Š(­ +(¢€ +(¢¹Î€¢Š(¢Š+œÿÖþü(¢Šñã±qZ…ãš ÀÍ|]û_~Ú¿ ¿dŸKâ]Dnˆ"+v�+3c+Ç$æ¹kÕ×–;�t(J£åŠ>ǹ½´²…®.äXÑFK1À{šðŸþÓŸü¼kþ%²„Æ2ãÍS·ëÏü0~Û_ð^¿Ž¾>Ö®|/á‹áe¦¾õòàà�r'·øGã¿ÛǾ*{ˆ5û¶šc»|“?ζkE–NK÷�#Ò†”ˆÏõHOÛ_öi`Ž|QjA�ÙÊãê+Ù¼+ñoá·�à[� k6׈ÿt£Ž~•þDÖ¿µÅ»KHtÛ]já£UùTÈÜ ô'5÷ŸÀßø)÷Ä�éØz¥ì¡È2¤Ž]c9ÁÀ$ ÏW<òhý™jhðTÍŸêT##�iêyÁ¯æ»þ ëÿ¥Ó~,ßÇàŸŠ.›—b‰È°p±È÷Ükú:е½+ĺTÞ‰:\ÚܨxäB}ÅqÕ¡*R呿âp®›ò70´Òƒ·ùýiËŒqI½i´ºœ£‡SN3ïJ)�š.x$óRîP:×Ëÿ´WícðwögðÔº÷ÄmR+y>\$’îÀg‘_Ë7í#ÿ|Uñv·yá¿€ztëk+ÄQ¸ç:zf·ÃB¤µ‚ùž–*©Qsl�ìÂmFÎÙ‚ÜL‘“Ù˜çVd�nFÈõþ~þý·jψw“kž3›Y�ÎNð%#òþUé~ +ÿ‚µþÓ?6ß>weÝÀ-í�^+ë¥ €}k +X…7dŽŠøIÓI˨´W‰üvø×á¿�¾ “Åž!pð‰œ?_JÒø+ñ[KøËà;_i ²‘�3ŸÖ³�K»èÉCŸ¡ëTÖ$)ÕâHÍMW²2,‚ž”e=?ÏçL Wo20³i=)*S íþJ¯<©[€£4JIubÞˆ}ùÅã?ø(·ÃOüz‹à�ÛG$Ò2¦T�à±À9$)Úý±½·Ôl¢¿µ`ÑÊ¡Ô�CÍfÚêvb0u(¤çÔ·JvãŠç¼Qâ�ÁºLjüGs¥�ª’Yj¨5üï~Ôð\ß ü?ñlÞøk^ÚDæ1tå|µcNR“j(èÂeµ+«ÇcúA¢¿ ?aÿø+÷„¿h�¯„¼W$Ï! ¹Û¸ûóÅ~æÛÜCu +Ü[°tp +�r= )Tå—$ôdã03¡+LšŠ(­ãœv +(¢ °¢Š)=€þzà¿M§ÀHç Ñ*{ƒ_Á4�$×ÖÆ5ÌíˆãÉûÄð1_ßGü¾ÞÞ_€žkòÉ0}Iãü?|$²Òaø½¢êÔ¾]¼7jÒ%Aõê+7»OO3ì0*øtÙôÇÀ¯ø$�í×ûEh¶þ5Ò¼9åè’HÖ;7Ô�ØÈúWö£ÿ†ýˆµ÷ŸÆ¯ø.¿Á�cáþ¥áØ´Ú†£lñ@ƒæÞ¤QÍ|Ïÿãý¡>xÏL²Ñt=F É`…£&6 w·N‡#üŠü,¾²ð¯Æ/ë~#`Ð[Þvl�F{‚q^”h©ÚsZž•:q“æ’?htŸø$'í;ûiÝËñŠ+sd×ɼG�¿»c•Ãdâ¿H?c�ÇÿðG¸g—âÅ‘šÚàŸsª|ˆIÈ<Žõû§û&~ÕŸ³Éø?¤Á»ilÉk˜K*Œí`ßJüUÿ‚éþÕß°ñücá?Ä{UøsðŠÔ7ˆuehaýâ̈ÏÀåxÏÖ¾ø�ÿBøÿûRjâf©yocq¨Ç½¼Ç+.îÆ}«ù¥ý–|wáÿ‡ß4ëc`µ¼Viü¡G|wüëý?fßÛ{à‡�~éÏy­[Ú¼vë³Í‘Tº�ÔklTjaš�£Ý$¨£Êà—ÿðO‹¿Ø�á±ÐüIp—“;dÆr¡OÇ5ú«wj.md·?òÑJþcà–ÿµGìÿ<ÞL^)±Ü}d}3Óõ««ûLüi<µñ%™aÛÌ=k(Rv¼Ó»ÙˆÂÂÒ6{gïq_ÀeŒÐ^FfÁÎqÑkôcþ ÙñëTøñÒÏÄ¥ŒVsO»« ë p¶zÒ¸³.t_.èì„yââÏõ¢FÇ#¡¥|gŠñÏ€Ÿô¯Š¿ +ôŸés$Âx;!Ȇ}³Í{ç¥yXisÅ6x á.Qè;×Ëÿµ×íáŸÙ¯àίñÄr¤bÞÞCg — ò�ž¤ž‚¾ &¿‘Oø8“ãV·®ëZìó¤Ë*­üÑK²,’à­€:í95×FŠ•E±ß—Ðç�ßCâ†ÿ ÿh/ø+×Çû�CVÔg�‘Ì[{p©á�£ tã=kõëâ‡�ÿàœðK¯Ámâ;{½\ V ¢Y c©Ýèy¯µ¿`O�Ú_ì{ûÚx¨Â±ßb‹û‡e·´a”Ìf¿�8|?ñ+þ +‹ÿ»ð¦µ4÷ºSê.rp�ÇÎ8é“[Шë¹=¡è¡zŠM½úwÿƒøXºü—~ðVí)\yn-׿î1�Á¯¸þx§öÿ‚‰i²ø#WÒì­u»øð0‹ѳq”> ×ë÷€?à™ß²—†þX|>Õ¼/iy1(vxÔ18ÁÉ5ø�ñcþ¡ñ+áwíWmñ;ök»’ÃBûZ:¢><¤|3;AéX_;¥xµ³¹çÇMÊÐ>]ý«?à�¿ÿc=y>5|Ô&Ô´ˆœù€¨%s‡U, `^½E~ØÿÁ*ÿà v|+¯ˆ" rЈ`G  ”7®zϧJý^ðWƒ¯%øWmàÿ*Þ»Úˆf~ð>W0=kùøÕ¡]~ŸðR�1tU6ºF¯z’@¬"w °ùFTç;R£?m¥ºÔé¥YbbèÏ¡ý°I�¼úW;á^/x_OÖàq"]ÛÇ*°èC¨9ýjæ»4öú=Ìö瑳ô­ãˆ~Ï™0èûü¬ÑŽö _dR+7 #4·pÛBÓܸ�’Ìpú×áwìSûHøŸÇ?¶?‰~k7Ȱ0¬Å‘G`£°èkîø(¯Ä�|*ý™uÏxfM“ÁPO#'� S§‹¨êrHô'•Z´i_sîûKÛkÈDör,±žŒ¤ùŠ|ã1¸ö5ùGÿ‚ø»ã¿Œ_³$~&ø‡z·—Ò\9]¡@TÏL.1×Ò¿W˜Œ|Ýêª×¼d¥èqâ0þÊ£�cùVøññNø_ÿ²¿ñŽ mtÅœ#oèá�§®kúwðß�|â[('Ð5;k¥‘So—"±;†GCÔ×ðíÿØÕuM'öœŠ×@‘ÅíÔœ2.æR7`xÀ¯%øñ ö÷ý˜âÐþ%øÊk˜ôé"òüíÛJ!ïè@é]±Â©aã(;;mÜú\f5!»;ÔÇüCí0~Í{bÅeyа¹ãñ¯Vÿ‚QÉ<¿±¿‡®î]�åÞIn¹¥|yûküfÓhØ +×Ç(ëö‰ã‰Šî Hcôã5§û|~µø7ÿõ“ÆZˆ,ÚDrº çsè~œ×‘(ÊTáÊõ¹Ã,;tU>§îö»¢é˜•Ü6ÅŽ–EBO¶H©à¿³ºŒMm"Ê¿ÞBüÅ>,ý¤¿n�Û�ãn±â„Ö÷—ºV™)H] dŠ äü¤:õàŸs_°_ðHÿÛ3âm¿Œï?g�¯%® ZQn'bKHœ%²AÈ8çÚº1”jR‡;whÏû*6ÑŸÓœ~ Ðæ¼m>+¸št81‡�ôÆsVîµ (üëÉ’$õrüÍ-ß¶¿í/ñ‹ö@ý­,|M¨LÃÃó\¥Ìžfv˜KŒ�}@WŸþÐðRß~Û<-ðoötœÆ è÷2Û± À‘œ�ýÐ3øW,1\9¤´f¯%Z>mëf9cš1,LHÈ#¥r¾>ÔJðf§¨»lÛÈÛ½0§šÃøU£Ýè~ÒôëÙZib¶Œ3¹Ë´u÷¯ý¶¼{€gm{W•ö+BÊÄvà“�ÈV°Ä)%ȧNÕ”Qü1~Ð?u�SöûþÒº¹alº”+æ(ö =Gþ€Ÿõ{ cán…¨i;,¡Ú}~Q_ç;âÙßãÄ?ê´e�´ñX$Ëu!¾aîR¤`+ûrÿ‚T|m—ã?ìÁ§_ÜcužÛ|g'r¨Ýø]YÍNXFqé¡ífQç‚kd|£ÿÃøý­ø+àË|4ðÜí Þ¯µÓ‚rxæ¿2¿boø"¥÷Çß„ ñâ¶«%¥Ýò ‹n@ÝÈÎO¿Zö?ø,.§7Ž¿kÿü=¹ŒËoźydðûˆÈ?�Mß øsÄw€¬ÒÙǽ[†S·¡÷ÇZò¿Ú‡öZøeû@Oayâ·�fµ•1#�¤~=+ê_‡^ Ò¼á+O èÇ0[ U>¼V”ñN¼¢šÕnrfXÅR’lî(¢Šõícç/p¢Š+p¢Š)K`?Ÿïø/µÃEû8 D»œ÷ë�ú×ð7â ;ˉÄðÊÛÔn\AaÛâ¿Ó›þ + ûOûbüºømk:ÚÉp6ù§¨‚?Zþmî?àÙïêigñ�2  «N:Éä{W-©BÒ>“ �„i(ÍïÃÏÚŸö„ø{¡ i:õäÚ`G6W®z×´é_ðP_ÚžÆe{-Zl:“óHFìp>£ûÅ¡ÿÁ¯Úô’µÎ­âæÚù,7mf㸫§üï«!FÒüQ€•‘›#ÛµrÎtÛ»dº°—ÚGó©ã/Úûö„ñÜÏÄíд-ºHUØïÏ©bsŠùªó†iP”‘²]Ÿ’à÷ɯêœÿÁ¯þ7Œ¬'Äöò"’TòÛ}>óŽõ×üÿãhG™iâ[iL…L¡òNåTîãõ1©MlÍc^œUœ�äÛþÝSìrIk8XÀÉ8ê*ªXÎcr§k oãÓ"¿«ý;þ ~ñìPˆn5øe²G˜û±Ÿ©³qÿÆxɤ�Ää7—µBmõˆÿ0½½>çò9 …̳µ­¦Ù”}âF=>µê> ºñ'�­üÍ6æ[‰Ÿyx\¦rGjþžGüùñ +e/'Š•=7pÃߊַÿƒcüw+Çlþ$¬|;2{ V~ÚÌZ«O¹üóÛþПl)—\¹Pp»£˜äÀ×K/í7ñ‡G�g“ÄWŽ“ 2´§æ>ù9¯èzOø5ö{˜ÚâÉ!@Cc~yÇAÆMb·üõ¬ÝX¼S幘–ÁõÂÕ¼uæü ¾µO¹üÄxçÆúÏŽ®®¼EâRkÛ‡á—#bóÆ0{W›ê�ͦ™\:€cnà�ë_Öý§üúl¬Ä2x¬3ƒó´`üÞã=«^ø6 g’™Ü±‘žGA“ÒŸöŽÖ¿àÎJµiôgòy់Ÿt=#ìV³,1¨%•®üt°µ/øÓÆÞKxÊmûXј³zçq&¿®™ÿà×Ï]X­•ψ]�˜òÇ“óú$S­à× -»Ã«xµ®#_õB0Ѻðy?1éWý¡C¿à\qä·?�[ëU�Ííˬe†TgŸLŠì´‹^=ðÚý‡MÖïxT•¶Ž8Wõ±¤ÿÁ°^¶rú·ˆ%º*pŒîx^À�ŸÏ5ß§üðÀÄ|Fñ�2BîÆIé’iÿiRoAÆ­8ï#øûÿ†ƒø«§Xˆ®|Aqä–ÉŒJßxû“šít�ÚOâ~¡i-…¿ˆ.Ž8üîï“Ö¿®[ø6sàäqÇkw¨ù±©ÜîX–'ïÛÚ»-7þ ›ý˜í.£¹{é'R ù¸ô­qIt'ë”ûŸÈ�Ïíñ£NŒÅ‰æ‰í”8A6ý¬:kúúÿƒ~~8|@ø¡à;›Ÿ\4þoÊϼ°v_Q’Ç9©äÿƒh¿fggWŸÌS¸.Ó´ã nrGã_¯Ÿ±Oì!ðÓö,ðkøgÁ4Í9#m +¡»ì;ô¬gŠ�hòÅjr×ÅÑ«Ÿw'J} ¥¯v +ÈùÆQEP‚Š( šÿv�L~•6EEW :Š(¬ÿÐþý”&«Mþ©¾•a?ÕÔ2(d*{Šòj|Ö›÷�áþñ¶«§üZ¼ð½åÖbŽ1(‹RYxÈê}kùŠpnn%Ϙ�É`xÃC_Ùü‹ðÁ-~'Ùª‡6‰#-’G`vã�­·:£Ï|ÑÈ0’Çãõ®šnÔcÛœ•ŒËåÈ]™�F3žµõìá®x§à �ã g)ip›˜@qØæ¾cKkÝCZ´�GŒÏ*8b dèké߉_¤ºðŒ¿ÒÄw^Z¨8ÀP=*sƾ1øÎOŒßÅÂ0’OVQüG<ÇžÃR[â¾S/9ÝÇ ¨5í>? Ø[ë÷<Œ§h=¸ªW¾(Ôu6h§!J—<`jO�üÚþ£5œ×i +vm<·Œv®ƒN²‡Âž&m-�„qʲ)ë–^@'ÜŠó� kZ½‹Í¨ÛÌU“î‚>S�Zõ¿„Z¥ñ7â�‚ˆMîÉiãŠyNjéè®ÏC ä¢�ôßÿ‚&ëž"ñì9áû�n%Fr]Yycï_¯#­| ÿÇð*ø ö:ð¦ŠmÉü€íu\�€3ÛÒ¾ü +s_?†�îÒÒç‘�½i‡ü³5ücÁe´JÛºö¡›c•LYÁNY?_ǵg¡~\WóñÿÀý�u/xCJøÅ Û‡“JuY6¯ÌYNA,9ÅvRN5<Ú= šªöŽ©úyâks¯~ÄÒÛØ€Zo ¦mÂÏê !_ðDiÞÿ‚€jZv³¶Þy®§‚G~…¤;zã¶F+úEÿ‚hþÓý ?gëo†º½âI{§Û5…Är0³i\mëì2ç_†ß¶üßã�ì}ûC·í ûÄÖÑÁò«ÔÐó(eqMºÏCû|ð—Š´ox~ßĺ —¶ºPñ’1�kùDÿ‚çM£?í3àá¦íþtp7 n8õ­K×Z¿…¿f?�GPñÒÇcáû1¾G;GÈ¿ãøšþFtOêðPÏø( ·Šü=g5î‘§_‰"|¡Q¾÷#uúô¬p2IʯCl²’…IT_ +?°ÿ€‰$?ü7»¾KûÇHìOZôÝY‚i³±ìŒCF‘h,4«kbDÀéò¨S5¸ä—I¸Š%ÜÍ? +®KSå«Ð‘í]Nµà-wRÿ‚~ůY–šÖÕš€dœŽ‚´–‰?0‚Øýƒÿ‚&üð·†eKˆÒ{©»Ç½S »IÝŸ\ä`ÿ�~|þÕ>ѾÁOô_X)X¯/c™âŒm'.™û¸ûÜóï_§ÿðEÝ~ÛVý‘­tô•${yÎà‡îðÏ¿;l}_Bñ¯üÛÃúEœÂW´»�GÉ;ܧ+ÏÂs{ZœÛXç åíd™ôŸüCàv�ñöeµø‡"†—CŒû(* ôþuùÿìüÑücñƒXø…¬Û©—L. ¯þü‰¯×Ÿø-§ÆÏ xöQžUkíAxŒö +æ¿?àß_ÚSCøañ¼®ÚȳkSI˜Ü(Fœz +ôáw‚³:Ú—±iÜñ íí¼+r­µÙ·”f br§×ý™ÿÁ;>9kß?gÍ;Å ‘¥™¼«¶BJsŽôf4ªšO,vÿ?­>Š·J/t$Ù—ô¤òÏ¥OEeõZ}‡ÎÈ<³éG–}*z)}RŸ`çe/Ú�/Ú¬QIà©v´e/Ú�/Ú¬QSõ]ƒÚ2¿—íIåûUš(ú….ÁíWËÇ<ÓªÅ//§Ð~ЯI…«4T¼¾!í +ØZiNr*ÎÅ£bÔK.O¨{B «KSlZ6-U<(:„C­MµEK]°¦‘ °¢Š+AQ@Q@5†E:šßv”¶CEWžö:Š(¬tÿÑþü!l®=iŦ*í\TÁ¾^kÏ…;Å&4ì;ð^ïÙ‹Yø«ð_þ�1DâÕ m +/ÌÀ äŸAÖ¿Î'â‚o|âùô XJ³©ûÛ~BL5²—Å�…þø·à›ßø‘I‚í +‡^Ž}=+ø&ÿ‚­Á,¼KðïX¾µußo34ö×QD@l_OnƱ§&½Éµ©Äþ[<9¬Â'¯G¨(•»ñï^«qá_|jñ z–†‚8íP–w8È•äÿ¾xïán«$^%µdR»'†8ÈëTôOŒ>#ðæš-´¬´{¶8n­ž…ò2Ö¹¨]C$ÞÔahüãÕk–ºO·@$¨ñÁÁ÷«7º­®±~רžW™ÉÏ''©¢ÒÏZÔ.MÐíÞöê_•cA’Äô§ãŒÍm/È—NL*Þlœ(îÕûÕÿrýˆuï�¿,f¸‚h–)Ð8Qò¬y—8äzWÄÿ±çüãâwÅoCkâK7[·`c‰$dôã8â¿Ñsþ ™ûøwödø_a|öb RxÎ,¸`qÓÞ¹±MÔ\‘;cUP^Õî~•x+Ã^ ð¦Ÿá}=vÅcB¿ðk¬UÉÏjk T©÷k¢–A$|ÕJŽrrc0y5çß~øw⟃o|âˆD¶×‘²r2T‘€GÒ½šÀ‘�J½%¦ã¥QÅÝÆ7ÇÙÃö�ÿ‚tüi›âOÃaq6†÷&Fû*±C lþóŒ{šýsøÿ|ýŸ&µŽêÞt(ÊêWãíÿIøñJiõÏ +ÊtË÷CÐ`3cƒ‘Œs\£¿ß-{£è(bèÖ_½vgЯñ?þ ¯ãb5�J} ÈÇ~gUS‘ß¼Ïâ·üŸþ óû0hR[øsU´º‘GËo¦¢áˆã…~|aÿ‚'þÑ^ ñ ®‘á!.¯ †$¤Í€ãž•™ðKþ ùøéñIJ‰Ìº>šWqyÉläôP9-�ÿ]%€ÃGÞ”›;=…Šs¹Kö›ÿ‚Ž|}ÿ‚žø’ÓàßÀ�:kêVˆÛ ËHݽç¥FŸðMø'ç†?cï†V·𬞠»Œ5Ĭ>`ÍÉäŒõ®Ïö"ÿ‚l|ýŒôX—Ãñ-þ«Õ®Yã(9?­~�šé�inUh£ÌÇfI¯gIYŠŠTDÑŸâU’™9&šÃº1UË¡âÆvgòÿjðO�?goÚZËã÷�㔢²³�ÒëÎ;ù/Ç?ðROÚ›öÝ‹LýŸßK6–·RÅ¥ÉLŽqŒó_Ùíû2ü>ý¥< Þñ¬[W$¬ª ·#稯›?gOø&GÀ_Ù÷Ä#Ŷ0¶£¨/Ýy@Âú`cÒ¸"ÚŠN7k©ô´ón–èöïØ³áüø ¤xV@ â0Ò`çƒô¯¬iðA1ˆãPª8qRâ©`o õ<؇)¹ÃÏü3ÿ<+û^Y|J𕼲�1ÖDdBØaÈ=á_1h¿´ïí�ûsÝè³Ò5ܺx‘£) +1ÁÝ�ØW÷}ã߀? >%ßOÇ:®£1]›å@N+›ðì­ðᎾÞ&ðG†¬ì/Ç™G¸÷­eUªj®§» Þ‰5©øÿûCþËMð�ö‹áÖ—nf¿{e‰Ž%ñ–ÏãZ?²·ìŸ©øïöºøeâ2o,YB¯Ul¤\×îÆ·á� ÄvŸa×-Ræî8ȧèžÑ|9d4í +Ù-`ìA�\¯/*8ÿ´—-‘ü iŸ?mßø&v¯¬øOÃ:sç]NÊÓ?Î' +áAÏ=yWÜ?ðLÿ�~<üpºýª~.ÛIP(š)¥\nv;²+ú¡ñ·Àï„_�dñχ¬µFC�gˆ7>þ¿Žk­ðç‚|'á <=ám: (Æ"ô¥J³OMY³Í!mµ?“oÚá/Ä_ÛÓöÆO†²ØÜ�N�Â× Ä,ˆz•4ßÚãö¾ý”¼Wá_üÓ¦�á’/6HÆ¥Bè½WÚ?Ã�h:ĺþ“§Eäç/*¯ÌsïWüCàÏx®5‹Ä‰t©Ê‡ûf·µ}"Öˆ¿íxÝ$´<Ÿà‰µßüÑõÿ¡KÙ-•O$8×óÁeðïÆÿÿà£Ûz¥Œ²iúUîåm§`Øz“_ÔfŸ£Øiv ¦XD"�Tpsº_ÿhš¤šÞ™§Ã Ôijʨ1>§®?©M7¦æx|Î0çk©½in Óã·Œ`"öü“ÿÁC¿f/ˆúoíï |Jð­¬Æ ®Ñæ” ³p<þ½]˜Îkžðlj7×,aºh›rî3D02§ð­Ì09�³mµ¹ùÃûa~ÊSþÓ³¶•§(“S±¶I¡óŠ'ÝŸÖ¿“sûQþܱV¿yà="ˆà‰ÈGŽ#ŒÎ=+ýÚáñ £hP8Ç¥|ÿñöZø1ñ.õ¯üS¤G$®>b8Ͼ*èR�8{7dtÒÌâ×-D +~×hÿÛ¿ãÍ„>$3ÝÉ)_9Xª’~ûòpEtß²ÏÁ÷ø'ðwJðTüMCzú9¥øKû)| +ø%¨O«ü=Р´º¹’R1ý8¯£ãï]ÔÔªÉ6¬‘ÉŽÇs.Hì;;©Çž(¢½4� (¢Š`QEQEQEQEQEQEQEQEQEQEQEQEQEQEQE×û´êkýÚRØkr(¢¼÷±¸QE�ƒ?ÿÒþü�@)TeM4œÔ©÷j ýÔ6:¼«â·Áßüaðì¾ñ¥”WPÈŒ™u @aÏQ^«EsÔ¤§£*qwGòýûPÁ´Ï½ÕÇ��¾©¦ËÊÛÌ KNp§ væ¿™ÏÚþ÷ñÁ:¦ªoôiíâÏ–�o`$Ràž•þœeA¬CÃ:ª¥5 8g®øÔç?Q\µ(VŠ÷ëas5i£üŸì?à˜_nïa³Ó4«Û¨�úÇX(пJýzý‹¿à‰¼OãË;¨ü=5†–UÝ]¨Eõ;¿§Zþþ¬|á-=BÙi–±ÏË +gùWE ¬0.Ø” a@ô¬éCÕ¤mW7�؉ùÛû Á9>~˺{²¥Tg®Õ$œû“ø +ý‰c‰Dq� q�Nù‡ Ò”ç­z0ªž=|Dª;ÈB™9§�ŠZ+¡ì`QE`I�KE0´mSKE+ :RÑE0 +(¢€´vâ”zZ)¨€QEù&-/]À(¢Š(#> /XObject << /I0 10 0 R +/I1 12 0 R >> >> endobj -319 0 obj -<> endobj -320 0 obj -<> endobj -321 0 obj -<> endobj -322 0 obj -<> endobj -323 0 obj -<> endobj 324 0 obj -<> endobj +<> endobj 325 0 obj -<> endobj +<> endobj 326 0 obj -<> endobj +<> endobj 327 0 obj -<> endobj +<> endobj 328 0 obj -<> endobj +<> endobj 329 0 obj -<> endobj +<> endobj 330 0 obj -<> endobj +<> endobj 331 0 obj -<> endobj +<> endobj 332 0 obj -<> endobj +<> endobj 333 0 obj -<> endobj +<> endobj 334 0 obj -<> endobj +<> endobj 335 0 obj -<> endobj +<> endobj 336 0 obj -<> endobj +<> endobj 337 0 obj -<> endobj +<> endobj 338 0 obj -<> endobj +<> endobj 339 0 obj -<> endobj +<> endobj 340 0 obj -<> endobj +<> endobj 341 0 obj -<> endobj +<> endobj 342 0 obj -<> endobj +<> endobj 343 0 obj -<> endobj +<> endobj 344 0 obj -<> endobj +<> endobj 345 0 obj -<> endobj +<> endobj 346 0 obj -<> endobj +<> endobj 347 0 obj -<> endobj +<> endobj 348 0 obj -<> endobj +<> endobj 349 0 obj -<> endobj +<> endobj 350 0 obj -<> endobj +<> endobj 351 0 obj -<> endobj +<> endobj 352 0 obj -<> endobj +<> endobj 353 0 obj -<> endobj +<> endobj 354 0 obj -<> endobj +<> endobj 355 0 obj -<> endobj +<> endobj 356 0 obj -<> endobj +<> endobj 357 0 obj -<> endobj +<> endobj 358 0 obj -<> endobj +<> endobj 359 0 obj -<> endobj +<> endobj 360 0 obj -<> endobj +<> endobj 361 0 obj -<> endobj +<> endobj 362 0 obj -<> endobj +<> endobj 363 0 obj -<> endobj +<> endobj 364 0 obj -<> endobj +<> endobj 365 0 obj -<> endobj +<> endobj 366 0 obj -<> endobj +<> endobj 367 0 obj -<> endobj +<> endobj 368 0 obj -<> endobj +<> endobj 369 0 obj -<> endobj +<> endobj 370 0 obj -<> endobj +<> endobj 371 0 obj -<> endobj +<> endobj 372 0 obj -<> endobj +<> endobj 373 0 obj -<> endobj +<> endobj 374 0 obj -<> endobj +<> endobj 375 0 obj -<> endobj +<> endobj 376 0 obj -<> endobj +<> endobj 377 0 obj -<> endobj +<> endobj 378 0 obj -<> endobj +<> endobj 379 0 obj -<> endobj +<> endobj 380 0 obj -<> endobj +<> endobj 381 0 obj -<> endobj +<> endobj 382 0 obj -<> endobj +<> endobj 383 0 obj -<> endobj +<> endobj 384 0 obj -<> endobj +<> endobj 385 0 obj -<> endobj +<> endobj 386 0 obj -<> endobj +<> endobj 387 0 obj -<> endobj +<> endobj 388 0 obj -<> endobj +<> endobj 389 0 obj -<> endobj +<> endobj 390 0 obj -<> endobj +<> endobj 391 0 obj -<> endobj +<> endobj 392 0 obj -<> endobj +<> endobj 393 0 obj -<> endobj +<> endobj 394 0 obj -<> endobj +<> endobj 395 0 obj -<> endobj +<> endobj 396 0 obj -<> endobj +<> endobj 397 0 obj -<> endobj +<> endobj 398 0 obj -<> endobj +<> endobj 399 0 obj -<> endobj +<> endobj 400 0 obj -<> endobj +<> endobj 401 0 obj -<> endobj +<> endobj 402 0 obj -<> endobj +<> endobj 403 0 obj -<> endobj +<> endobj 404 0 obj -<> endobj +<> endobj 405 0 obj -<> endobj +<> endobj 406 0 obj -<> endobj +<> endobj 407 0 obj -<> endobj +<> endobj 408 0 obj -<> endobj +<> endobj 409 0 obj -<> endobj +<> endobj 410 0 obj -<> endobj +<> endobj 411 0 obj -<> endobj +<> endobj 412 0 obj -<> endobj +<> endobj 413 0 obj -<> endobj +<> endobj 414 0 obj -<> endobj +<> endobj 415 0 obj -<> endobj +<> endobj 416 0 obj -<> endobj +<> endobj 417 0 obj -<> endobj +<> endobj 418 0 obj -<> endobj +<> endobj 419 0 obj -<> endobj +<> endobj 420 0 obj -<> endobj +<> endobj 421 0 obj -<> endobj +<> endobj 422 0 obj -<> endobj +<> endobj 423 0 obj -<> endobj +<> endobj 424 0 obj -<> endobj +<> endobj 425 0 obj -<> endobj +<> endobj 426 0 obj -<> endobj +<> endobj 427 0 obj -<> endobj +<> endobj 428 0 obj -<> endobj +<> endobj 429 0 obj -<> endobj +<> endobj 430 0 obj -<> endobj +<> endobj 431 0 obj -<> endobj +<> endobj 432 0 obj -<> endobj +<> endobj 433 0 obj -<> endobj +<> endobj 434 0 obj -<> endobj +<> endobj 435 0 obj -<> endobj +<> endobj 436 0 obj -<> endobj +<> endobj 437 0 obj -<> endobj +<> endobj 438 0 obj -<> endobj +<> endobj 439 0 obj -<> endobj +<> endobj 440 0 obj -<> endobj +<> endobj 441 0 obj -<> endobj +<> endobj 442 0 obj -<> endobj +<> endobj 443 0 obj -<> endobj +<> endobj 444 0 obj -<> endobj +<> endobj 445 0 obj -<> endobj +<> endobj 446 0 obj -<> endobj +<> endobj 447 0 obj -<> endobj +<> endobj 448 0 obj -<> endobj +<> endobj 449 0 obj -<> endobj +<> endobj 450 0 obj -<> endobj +<> endobj 451 0 obj -<> endobj +<> endobj 452 0 obj -<> endobj +<> endobj 453 0 obj -<> endobj +<> endobj 454 0 obj -<> endobj +<> endobj 455 0 obj -<> endobj +<> endobj 456 0 obj -<> endobj +<> endobj 457 0 obj -<> endobj +<> endobj 458 0 obj -<> endobj +<> endobj 459 0 obj -<> endobj +<> endobj 460 0 obj -<> endobj +<> endobj 461 0 obj -<> endobj +<> endobj 462 0 obj -<> endobj +<> endobj 463 0 obj -<> endobj +<> endobj 464 0 obj -<> endobj +<> endobj 465 0 obj -<> endobj +<> endobj 466 0 obj -<> endobj +<> endobj 467 0 obj -<> endobj +<> endobj 468 0 obj -<> endobj +<> endobj 469 0 obj -<> endobj +<> endobj 470 0 obj -<> endobj +<> endobj 471 0 obj -<> endobj +<> endobj 472 0 obj -<> endobj +<> endobj 473 0 obj -<> endobj +<> endobj 474 0 obj -<> endobj +<> endobj 475 0 obj -<> endobj +<> endobj 476 0 obj -<> endobj +<> endobj 477 0 obj -<> endobj +<> endobj 478 0 obj -<> endobj +<> endobj 479 0 obj -<> endobj +<> endobj 480 0 obj -<> endobj +<> endobj 481 0 obj -<> endobj +<> endobj 482 0 obj -<> endobj +<> endobj 483 0 obj -<> endobj +<> endobj 484 0 obj -<> endobj +<> endobj 485 0 obj -<> endobj +<> endobj 486 0 obj -<> endobj +<> endobj 487 0 obj -<> endobj +<> endobj 488 0 obj -<> endobj +<> endobj 489 0 obj -<> endobj +<> endobj 490 0 obj -<> endobj +<> endobj 491 0 obj -<> endobj +<> endobj 492 0 obj -<> endobj +<> endobj 493 0 obj -<> endobj +<> endobj 494 0 obj -<> endobj +<> endobj 495 0 obj -<> endobj +<> endobj 496 0 obj -<> endobj +<> endobj 497 0 obj -<> endobj +<> endobj 498 0 obj -<> endobj +<> endobj 499 0 obj -<> endobj +<> endobj 500 0 obj -<> endobj +<> endobj 501 0 obj -<> endobj +<> endobj 502 0 obj -<> endobj +<> endobj 503 0 obj -<> endobj +<> endobj 504 0 obj -<> endobj +<> endobj 505 0 obj -<> endobj +<> endobj 506 0 obj -<> endobj +<> endobj 507 0 obj -<> endobj +<> endobj 508 0 obj -<> endobj +<> endobj 509 0 obj -<> endobj +<> endobj 510 0 obj -<> endobj +<> endobj 511 0 obj -<> endobj +<> endobj 512 0 obj -<> endobj +<> endobj 513 0 obj -<> endobj +<> endobj 514 0 obj -<> endobj +<> endobj 515 0 obj -<> endobj +<> endobj 516 0 obj -<> endobj +<> endobj 517 0 obj -<> endobj +<> endobj 518 0 obj -<> endobj +<> endobj 519 0 obj -<> endobj +<> endobj 520 0 obj -<> endobj +<> endobj 521 0 obj -<> endobj +<> endobj 522 0 obj -<> endobj +<> endobj 523 0 obj -<> endobj +<> endobj 524 0 obj -<> endobj +<> endobj 525 0 obj -<> endobj +<> endobj 526 0 obj -<> endobj +<> endobj 527 0 obj -<> endobj +<> endobj 528 0 obj -<> endobj +<> endobj 529 0 obj -<> endobj +<> endobj 530 0 obj -<> endobj +<> endobj 531 0 obj -<> endobj +<> endobj 532 0 obj -<> endobj +<> endobj 533 0 obj -<> endobj +<> endobj 534 0 obj -<> endobj +<> endobj 535 0 obj -<> endobj +<> endobj 536 0 obj -<> endobj +<> endobj 537 0 obj -<> endobj +<> endobj 538 0 obj -<> endobj +<> endobj 539 0 obj -<> endobj +<> endobj 540 0 obj -<> endobj +<> endobj 541 0 obj -<> endobj +<> endobj 542 0 obj -<> endobj +<> endobj 543 0 obj -<> endobj +<> endobj 544 0 obj -<> endobj +<> endobj 545 0 obj -<> endobj +<> endobj 546 0 obj -<> endobj +<> endobj 547 0 obj -<> endobj +<> endobj 548 0 obj -<> endobj +<> endobj 549 0 obj -<> endobj +<> endobj 550 0 obj -<> endobj +<> endobj 551 0 obj -<> endobj +<> endobj 552 0 obj -<> endobj +<> endobj 553 0 obj -<> endobj +<> endobj 554 0 obj -<> endobj +<> endobj 555 0 obj -<> endobj +<> endobj 556 0 obj -<> endobj +<> endobj 557 0 obj -<> endobj +<> endobj 558 0 obj -<> endobj +<> endobj 559 0 obj -<> endobj +<> endobj 560 0 obj -<> endobj +<> endobj 561 0 obj -<> endobj +<> endobj 562 0 obj -<> endobj +<> endobj 563 0 obj -<> endobj +<> endobj 564 0 obj -<> endobj +<> endobj 565 0 obj -<> endobj +<> endobj 566 0 obj -<> endobj +<> endobj 567 0 obj -<> endobj +<> endobj 568 0 obj -<> endobj +<> endobj 569 0 obj -<> endobj +<> endobj 570 0 obj -<> endobj +<> endobj 571 0 obj -<> endobj +<> endobj 572 0 obj -<> endobj +<> endobj 573 0 obj -<> endobj +<> endobj 574 0 obj -<> endobj +<> endobj 575 0 obj -<> endobj +<> endobj 576 0 obj -<> endobj +<> endobj 577 0 obj -<> endobj +<> endobj 578 0 obj -<> endobj +<> endobj 579 0 obj -<> endobj +<> endobj 580 0 obj -<> endobj +<> endobj 581 0 obj -<> endobj +<> endobj 582 0 obj -<> endobj +<> endobj 583 0 obj -<> endobj +<> endobj 584 0 obj -<> endobj +<> endobj 585 0 obj -<> endobj +<> endobj 586 0 obj -<> endobj +<> endobj 587 0 obj -<> endobj +<> endobj 588 0 obj -<> endobj +<> endobj 589 0 obj -<> endobj +<> endobj 590 0 obj -<> endobj +<> endobj 591 0 obj -<> endobj +<> endobj 592 0 obj -<> endobj +<> endobj 593 0 obj -<> endobj +<> endobj 594 0 obj -<> endobj +<> endobj 595 0 obj -<> endobj +<> endobj 596 0 obj -<> endobj +<> endobj 597 0 obj -<> endobj +<> endobj 598 0 obj -<> endobj +<> endobj 599 0 obj -<> endobj +<> endobj 600 0 obj -<> endobj +<> endobj 601 0 obj -<> endobj +<> endobj 602 0 obj -<> endobj +<> endobj 603 0 obj -<> endobj +<> endobj 604 0 obj -<> endobj +<> endobj 605 0 obj -<> endobj +<> endobj 606 0 obj -<> endobj +<> endobj 607 0 obj -<> endobj +<> endobj 608 0 obj -<> endobj +<> endobj 609 0 obj -<> endobj +<> endobj 610 0 obj -<> endobj +<> endobj 611 0 obj -<> endobj +<> endobj 612 0 obj -<> endobj +<> endobj 613 0 obj -<> endobj +<> endobj 614 0 obj -<> endobj +<> endobj 615 0 obj -<> endobj +<> endobj 616 0 obj -<> endobj +<> endobj 617 0 obj -<> endobj +<> endobj 618 0 obj -<> endobj +<> endobj 619 0 obj -<> endobj +<> endobj 620 0 obj -<> endobj +<> endobj 621 0 obj -<> endobj +<> endobj 622 0 obj -<> endobj +<> endobj 623 0 obj -<> endobj +<> endobj 624 0 obj -<> endobj +<> endobj 625 0 obj -<> endobj +<> endobj 626 0 obj -<> endobj +<> endobj 627 0 obj -<> endobj +<> endobj 628 0 obj -<> endobj +<> endobj 629 0 obj -<> endobj +<> endobj 630 0 obj -<> endobj +<> endobj 631 0 obj -<> endobj +<> endobj 632 0 obj -<> endobj +<> endobj 633 0 obj -<> endobj +<> endobj 634 0 obj -<> endobj +<> endobj 635 0 obj -<> endobj +<> endobj 636 0 obj -<> endobj +<> endobj 637 0 obj -<> endobj +<> endobj 638 0 obj -<> endobj +<> endobj 639 0 obj -<> endobj +<> endobj 640 0 obj -<> endobj +<> endobj 641 0 obj -<> endobj +<> endobj 642 0 obj -<> endobj +<> endobj 643 0 obj -<> endobj +<> endobj 644 0 obj -<> endobj +<> endobj 645 0 obj -<> endobj +<> endobj 646 0 obj -<> endobj +<> endobj 647 0 obj -<> endobj +<> endobj 648 0 obj -<> endobj +<> endobj 649 0 obj -<> endobj +<> endobj 650 0 obj -<> endobj +<> endobj 651 0 obj -<> endobj +<> endobj 652 0 obj -<> endobj +<> endobj 653 0 obj -<> endobj +<> endobj 654 0 obj -<> endobj +<> endobj 655 0 obj -<> endobj +<> endobj 656 0 obj -<> endobj +<> endobj 657 0 obj -<> endobj +<> endobj 658 0 obj -<> endobj +<> endobj 659 0 obj -<> endobj +<> endobj 660 0 obj -<> endobj +<> endobj 661 0 obj -<> endobj +<> endobj 662 0 obj -<> endobj +<> endobj 663 0 obj -<> endobj +<> endobj 664 0 obj -<> endobj +<> endobj 665 0 obj -<> endobj +<> endobj 666 0 obj -<> endobj +<> endobj 667 0 obj -<> endobj +<> endobj 668 0 obj -<> endobj +<> endobj 669 0 obj -<> endobj +<> endobj 670 0 obj -<> endobj +<> endobj 671 0 obj -<> endobj +<> endobj 672 0 obj -<> endobj +<> endobj 673 0 obj -<> endobj +<> endobj 674 0 obj -<> endobj +<> endobj 675 0 obj -<> endobj +<> endobj 676 0 obj -<> endobj +<> endobj 677 0 obj -<> endobj +<> endobj 678 0 obj -<> endobj +<> endobj 679 0 obj -<> endobj +<> endobj 680 0 obj -<> endobj +<> endobj 681 0 obj -<> endobj +<> endobj 682 0 obj -<> endobj +<> endobj 683 0 obj -<> endobj +<> endobj 684 0 obj -<> endobj +<> endobj 685 0 obj -<> endobj +<> endobj 686 0 obj -<> endobj +<> endobj 687 0 obj -<> endobj +<> endobj 688 0 obj -<> endobj +<> endobj 689 0 obj -<> endobj +<> endobj 690 0 obj -<> endobj +<> endobj 691 0 obj -<> endobj +<> endobj 692 0 obj -<> endobj +<> endobj 693 0 obj -<> endobj +<> endobj 694 0 obj -<> endobj +<> endobj 695 0 obj -<> endobj +<> endobj 696 0 obj -<> endobj +<> endobj 697 0 obj -<> endobj +<> endobj 698 0 obj -<> endobj +<> endobj 699 0 obj -<> endobj +<> endobj 700 0 obj -<> endobj +<> endobj 701 0 obj -<> endobj +<> endobj 702 0 obj -<> endobj +<> endobj 703 0 obj -<> endobj +<> endobj 704 0 obj -<> endobj +<> endobj 705 0 obj -<> endobj +<> endobj 706 0 obj -<> endobj +<> endobj 707 0 obj -<> endobj +<> endobj 708 0 obj -<> endobj +<> endobj 709 0 obj -<> endobj +<> endobj 710 0 obj -<> endobj +<> endobj 711 0 obj -<> endobj +<> endobj 712 0 obj -<> endobj +<> endobj 713 0 obj -<> endobj +<> endobj 714 0 obj -<> endobj +<> endobj 715 0 obj -<> endobj +<> endobj 716 0 obj -<> endobj +<> endobj 717 0 obj -<> endobj +<> endobj 718 0 obj -<> endobj +<> endobj 719 0 obj -<> endobj +<> endobj 720 0 obj -<> endobj +<> endobj 721 0 obj -<> endobj +<> endobj 722 0 obj -<> endobj +<> endobj 723 0 obj -<> endobj +<> endobj 724 0 obj -<> endobj +<> endobj 725 0 obj -<> endobj +<> endobj 726 0 obj -<> endobj +<> endobj 727 0 obj -<> endobj +<> endobj 728 0 obj -<> endobj +<> endobj 729 0 obj -<> endobj +<> endobj 730 0 obj -<> endobj +<> endobj 731 0 obj -<> endobj +<> endobj 732 0 obj -<> endobj +<> endobj 733 0 obj -<> endobj +<> endobj 734 0 obj -<> endobj +<> endobj 735 0 obj -<> endobj +<> endobj 736 0 obj -<> endobj +<> endobj 737 0 obj -<> endobj +<> endobj 738 0 obj -<> endobj +<> endobj 739 0 obj -<> endobj +<> endobj 740 0 obj -<> endobj +<> endobj 741 0 obj -<> endobj +<> endobj 742 0 obj -<> endobj +<> endobj 743 0 obj -<> endobj +<> endobj 744 0 obj -<> endobj +<> endobj 745 0 obj -<> endobj +<> endobj 746 0 obj -<> endobj +<> endobj 747 0 obj -<> endobj +<> endobj 748 0 obj -<> endobj +<> endobj 749 0 obj -<> endobj +<> endobj 750 0 obj -<> endobj +<> endobj 751 0 obj -<> endobj +<> endobj 752 0 obj -<> endobj +<> endobj 753 0 obj -<> endobj +<> endobj 754 0 obj -<> endobj +<> endobj 755 0 obj -<> endobj +<> endobj 756 0 obj -<> endobj +<> endobj 757 0 obj -<> endobj +<> endobj 758 0 obj -<> endobj +<> endobj 759 0 obj -<> endobj +<> endobj 760 0 obj -<> endobj +<> endobj 761 0 obj -<> endobj +<> endobj 762 0 obj -<> endobj +<> endobj 763 0 obj -<> endobj +<> endobj 764 0 obj -<> endobj +<> endobj 765 0 obj -<> endobj +<> endobj 766 0 obj -<> endobj +<> endobj 767 0 obj -<> endobj +<> endobj 768 0 obj -<> endobj +<> endobj 769 0 obj -<> endobj +<> endobj 770 0 obj -<> endobj +<> endobj 771 0 obj -<> endobj +<> endobj 772 0 obj -<> endobj +<> endobj 773 0 obj -<> endobj +<> endobj 774 0 obj -<> endobj +<> endobj 775 0 obj -<> endobj +<> endobj 776 0 obj -<> endobj +<> endobj 777 0 obj -<> endobj +<> endobj 778 0 obj -<> endobj +<> endobj 779 0 obj -<> endobj +<> endobj 780 0 obj -<> endobj +<> endobj 781 0 obj -<> endobj +<> endobj 782 0 obj -<> endobj +<> endobj 783 0 obj -<> endobj +<> endobj 784 0 obj -<> endobj +<> endobj 785 0 obj -<> endobj +<> endobj 786 0 obj -<> endobj +<> endobj 787 0 obj -<> endobj +<> endobj 788 0 obj -<> endobj +<> endobj 789 0 obj -<> endobj +<> endobj 790 0 obj -<> endobj +<> endobj 791 0 obj -<> endobj +<> endobj 792 0 obj -<> endobj +<> endobj 793 0 obj -<> endobj +<> endobj 794 0 obj -<> endobj +<> endobj 795 0 obj -<> endobj +<> endobj 796 0 obj -<> endobj +<> endobj 797 0 obj -<> endobj +<> endobj 798 0 obj -<> endobj +<> endobj 799 0 obj -<> endobj +<> endobj 800 0 obj -<> endobj +<> endobj 801 0 obj -<> endobj +<> endobj 802 0 obj -<> endobj +<> endobj 803 0 obj -<> endobj +<> endobj 804 0 obj -<> endobj +<> endobj 805 0 obj -<> endobj +<> endobj 806 0 obj -<> endobj +<> endobj 807 0 obj -<> endobj +<> endobj 808 0 obj -<> endobj +<> endobj 809 0 obj -<> endobj +<> endobj 810 0 obj -<> endobj +<> endobj 811 0 obj -<> endobj +<> endobj 812 0 obj -<> endobj +<> endobj 813 0 obj -<> endobj +<> endobj 814 0 obj -<> endobj +<> endobj 815 0 obj -<> endobj +<> endobj 816 0 obj -<> endobj +<> endobj 817 0 obj -<> endobj +<> endobj 818 0 obj -<> endobj +<> endobj 819 0 obj -<> endobj +<> endobj 820 0 obj -<> endobj +<> endobj 821 0 obj -<> endobj +<> endobj 822 0 obj -<> endobj +<> endobj 823 0 obj -<> endobj +<> endobj 824 0 obj -<> endobj +<> endobj 825 0 obj -<> endobj +<> endobj 826 0 obj -<> endobj +<> endobj 827 0 obj -<> endobj +<> endobj 828 0 obj -<> endobj +<> endobj 829 0 obj -<> endobj +<> endobj 830 0 obj -<> endobj +<> endobj 831 0 obj -<> endobj +<> endobj 832 0 obj -<> endobj +<> endobj 833 0 obj -<> endobj +<> endobj 834 0 obj -<> endobj +<> endobj 835 0 obj -<> endobj +<> endobj 836 0 obj -<> endobj +<> endobj 837 0 obj -<> endobj +<> endobj 838 0 obj -<> endobj +<> endobj 839 0 obj -<> endobj +<> endobj 840 0 obj -<> endobj +<> endobj 841 0 obj -<> endobj +<> endobj 842 0 obj -<> endobj +<> endobj 843 0 obj -<> endobj +<> endobj 844 0 obj -<> endobj +<> endobj 845 0 obj -<> endobj +<> endobj 846 0 obj -<> endobj +<> endobj 847 0 obj -<> endobj +<> endobj 848 0 obj -<> endobj +<> endobj 849 0 obj -<> endobj +<> endobj 850 0 obj -<> endobj +<> endobj 851 0 obj -<> endobj +<> endobj 852 0 obj -<> endobj +<> endobj 853 0 obj -<> endobj +<> endobj 854 0 obj -<> endobj +<> endobj 855 0 obj -<> endobj +<> endobj 856 0 obj -<> endobj +<> endobj 857 0 obj -<> endobj +<> endobj 858 0 obj -<> endobj +<> endobj 859 0 obj -<> endobj +<> endobj 860 0 obj +<> endobj +861 0 obj +<> endobj +862 0 obj +<> endobj +863 0 obj +<> endobj +864 0 obj +<> endobj +865 0 obj +<> endobj +866 0 obj +<> endobj +867 0 obj +<> endobj +868 0 obj +<> endobj +869 0 obj +<> endobj +870 0 obj +<> endobj +871 0 obj +<> endobj +872 0 obj +<> endobj +873 0 obj <> endobj -12 0 obj -<< -/Type /Outlines -/First 13 0 R -/Last 15 0 R -/Count 306 ->> -endobj - 13 0 obj << -/Title (Pages) -/Parent 12 0 R -/Next 15 0 R +/Type /Outlines /First 14 0 R -/Last 14 0 R -/Count 1 ->> -endobj - -15 0 obj -<< -/Title (Net) -/Parent 12 0 R -/Prev 13 0 R -/First 16 0 R -/Last 315 0 R -/Count 303 +/Last 16 0 R +/Count 310 >> endobj 14 0 obj << -/Title (SCH_Pro-micro_Pinouts 1-Sheet_1) +/Title (Pages) /Parent 13 0 R -/Dest [3 0 R /XYZ 0 1197.36 0] +/Next 16 0 R +/First 15 0 R +/Last 15 0 R +/Count 1 >> endobj 16 0 obj << -/Title (3V3) -/Parent 15 0 R -/Next 35 0 R +/Title (Net) +/Parent 13 0 R +/Prev 14 0 R /First 17 0 R -/Last 34 0 R -/Count 18 +/Last 320 0 R +/Count 307 >> endobj -35 0 obj +15 0 obj +<< +/Title (SCH_Pro-micro_Pinouts 1-Sheet_1) +/Parent 14 0 R +/Dest [3 0 R /XYZ 0 1197.36 0] +>> +endobj + +17 0 obj +<< +/Title (3V3) +/Parent 16 0 R +/Next 37 0 R +/First 18 0 R +/Last 36 0 R +/Count 19 +>> +endobj + +37 0 obj << /Title (+5V) -/Parent 15 0 R -/Prev 16 0 R -/Next 39 0 R -/First 36 0 R -/Last 38 0 R -/Count 3 ->> -endobj - -39 0 obj -<< -/Title ($1N1) -/Parent 15 0 R -/Prev 35 0 R +/Parent 16 0 R +/Prev 17 0 R /Next 41 0 R -/First 40 0 R +/First 38 0 R /Last 40 0 R -/Count 1 +/Count 3 >> endobj 41 0 obj << -/Title ($1N25) -/Parent 15 0 R -/Prev 39 0 R +/Title ($1N1) +/Parent 16 0 R +/Prev 37 0 R /Next 43 0 R /First 42 0 R /Last 42 0 R @@ -29475,8 +30216,8 @@ endobj 43 0 obj << -/Title ($1N62) -/Parent 15 0 R +/Title ($1N25) +/Parent 16 0 R /Prev 41 0 R /Next 45 0 R /First 44 0 R @@ -29487,153 +30228,153 @@ endobj 45 0 obj << -/Title (ADC) -/Parent 15 0 R +/Title ($1N62) +/Parent 16 0 R /Prev 43 0 R -/Next 50 0 R +/Next 47 0 R /First 46 0 R -/Last 49 0 R +/Last 46 0 R +/Count 1 +>> +endobj + +47 0 obj +<< +/Title (ADC) +/Parent 16 0 R +/Prev 45 0 R +/Next 52 0 R +/First 48 0 R +/Last 51 0 R /Count 4 >> endobj -50 0 obj +52 0 obj << /Title (BATT) -/Parent 15 0 R -/Prev 45 0 R -/Next 57 0 R -/First 51 0 R -/Last 56 0 R +/Parent 16 0 R +/Prev 47 0 R +/Next 59 0 R +/First 53 0 R +/Last 58 0 R /Count 6 >> endobj -57 0 obj +59 0 obj << /Title (BUSY) -/Parent 15 0 R -/Prev 50 0 R -/Next 73 0 R -/First 58 0 R -/Last 72 0 R +/Parent 16 0 R +/Prev 52 0 R +/Next 75 0 R +/First 60 0 R +/Last 74 0 R /Count 15 >> endobj -73 0 obj +75 0 obj << /Title (CS) -/Parent 15 0 R -/Prev 57 0 R -/Next 89 0 R -/First 74 0 R -/Last 88 0 R +/Parent 16 0 R +/Prev 59 0 R +/Next 91 0 R +/First 76 0 R +/Last 90 0 R /Count 15 >> endobj -89 0 obj +91 0 obj << /Title (DIO2) -/Parent 15 0 R -/Prev 73 0 R -/Next 102 0 R -/First 90 0 R -/Last 101 0 R -/Count 12 ->> -endobj - -102 0 obj -<< -/Title (DIO3) -/Parent 15 0 R -/Prev 89 0 R +/Parent 16 0 R +/Prev 75 0 R /Next 104 0 R -/First 103 0 R +/First 92 0 R /Last 103 0 R -/Count 1 +/Count 12 >> endobj 104 0 obj << -/Title (E_INK_BUSY) -/Parent 15 0 R -/Prev 102 0 R -/Next 109 0 R +/Title (DIO3) +/Parent 16 0 R +/Prev 91 0 R +/Next 106 0 R /First 105 0 R -/Last 108 0 R +/Last 105 0 R +/Count 1 +>> +endobj + +106 0 obj +<< +/Title (E_INK_BUSY) +/Parent 16 0 R +/Prev 104 0 R +/Next 111 0 R +/First 107 0 R +/Last 110 0 R /Count 4 >> endobj -109 0 obj +111 0 obj << /Title (E_INK_CS) -/Parent 15 0 R -/Prev 104 0 R -/Next 113 0 R -/First 110 0 R -/Last 112 0 R +/Parent 16 0 R +/Prev 106 0 R +/Next 115 0 R +/First 112 0 R +/Last 114 0 R /Count 3 >> endobj -113 0 obj +115 0 obj << /Title (E_INK_D/C) -/Parent 15 0 R -/Prev 109 0 R -/Next 117 0 R -/First 114 0 R -/Last 116 0 R +/Parent 16 0 R +/Prev 111 0 R +/Next 119 0 R +/First 116 0 R +/Last 118 0 R /Count 3 >> endobj -117 0 obj +119 0 obj << /Title (E_INK_NRST) -/Parent 15 0 R -/Prev 113 0 R -/Next 121 0 R -/First 118 0 R -/Last 120 0 R +/Parent 16 0 R +/Prev 115 0 R +/Next 123 0 R +/First 120 0 R +/Last 122 0 R /Count 3 >> endobj -121 0 obj +123 0 obj << /Title (GND) -/Parent 15 0 R -/Prev 117 0 R -/Next 196 0 R -/First 122 0 R -/Last 195 0 R -/Count 74 ->> -endobj - -196 0 obj -<< -/Title (GPSEN) -/Parent 15 0 R -/Prev 121 0 R +/Parent 16 0 R +/Prev 119 0 R /Next 199 0 R -/First 197 0 R +/First 124 0 R /Last 198 0 R -/Count 2 +/Count 75 >> endobj 199 0 obj << -/Title (GPSRX) -/Parent 15 0 R -/Prev 196 0 R +/Title (GPSEN) +/Parent 16 0 R +/Prev 123 0 R /Next 202 0 R /First 200 0 R /Last 201 0 R @@ -29643,1812 +30384,1826 @@ endobj 202 0 obj << -/Title (GPSTX) -/Parent 15 0 R +/Title (GPSRX) +/Parent 16 0 R /Prev 199 0 R -/Next 205 0 R +/Next 206 0 R /First 203 0 R -/Last 204 0 R -/Count 2 +/Last 205 0 R +/Count 3 >> endobj -205 0 obj +206 0 obj +<< +/Title (GPSTX) +/Parent 16 0 R +/Prev 202 0 R +/Next 210 0 R +/First 207 0 R +/Last 209 0 R +/Count 3 +>> +endobj + +210 0 obj << /Title (IRQ) -/Parent 15 0 R -/Prev 202 0 R -/Next 221 0 R -/First 206 0 R -/Last 220 0 R +/Parent 16 0 R +/Prev 206 0 R +/Next 226 0 R +/First 211 0 R +/Last 225 0 R /Count 15 >> endobj -221 0 obj +226 0 obj << /Title (LORA_ANT) -/Parent 15 0 R -/Prev 205 0 R -/Next 223 0 R -/First 222 0 R -/Last 222 0 R +/Parent 16 0 R +/Prev 210 0 R +/Next 228 0 R +/First 227 0 R +/Last 227 0 R /Count 1 >> endobj -223 0 obj +228 0 obj << /Title (MISO) -/Parent 15 0 R -/Prev 221 0 R -/Next 239 0 R -/First 224 0 R -/Last 238 0 R +/Parent 16 0 R +/Prev 226 0 R +/Next 244 0 R +/First 229 0 R +/Last 243 0 R /Count 15 >> endobj -239 0 obj +244 0 obj << /Title (MOSI) -/Parent 15 0 R -/Prev 223 0 R -/Next 257 0 R -/First 240 0 R -/Last 256 0 R +/Parent 16 0 R +/Prev 228 0 R +/Next 262 0 R +/First 245 0 R +/Last 261 0 R /Count 17 >> endobj -257 0 obj +262 0 obj << /Title (NRST) -/Parent 15 0 R -/Prev 239 0 R -/Next 273 0 R -/First 258 0 R -/Last 272 0 R +/Parent 16 0 R +/Prev 244 0 R +/Next 278 0 R +/First 263 0 R +/Last 277 0 R /Count 15 >> endobj -273 0 obj +278 0 obj << /Title (RBTN) -/Parent 15 0 R -/Prev 257 0 R -/Next 277 0 R -/First 274 0 R -/Last 276 0 R +/Parent 16 0 R +/Prev 262 0 R +/Next 282 0 R +/First 279 0 R +/Last 281 0 R /Count 3 >> endobj -277 0 obj +282 0 obj << /Title (RXEN) -/Parent 15 0 R -/Prev 273 0 R -/Next 285 0 R -/First 278 0 R -/Last 284 0 R +/Parent 16 0 R +/Prev 278 0 R +/Next 290 0 R +/First 283 0 R +/Last 289 0 R /Count 7 >> endobj -285 0 obj +290 0 obj << /Title (SCK) -/Parent 15 0 R -/Prev 277 0 R -/Next 303 0 R -/First 286 0 R -/Last 302 0 R +/Parent 16 0 R +/Prev 282 0 R +/Next 308 0 R +/First 291 0 R +/Last 307 0 R /Count 17 >> endobj -303 0 obj +308 0 obj << /Title (SCL) -/Parent 15 0 R -/Prev 285 0 R -/Next 306 0 R -/First 304 0 R -/Last 305 0 R +/Parent 16 0 R +/Prev 290 0 R +/Next 311 0 R +/First 309 0 R +/Last 310 0 R /Count 2 >> endobj -306 0 obj +311 0 obj << /Title (SDA) -/Parent 15 0 R -/Prev 303 0 R -/Next 309 0 R -/First 307 0 R -/Last 308 0 R +/Parent 16 0 R +/Prev 308 0 R +/Next 314 0 R +/First 312 0 R +/Last 313 0 R /Count 2 >> endobj -309 0 obj +314 0 obj << /Title (SERIAL2RX) -/Parent 15 0 R -/Prev 306 0 R -/Next 312 0 R -/First 310 0 R -/Last 311 0 R +/Parent 16 0 R +/Prev 311 0 R +/Next 317 0 R +/First 315 0 R +/Last 316 0 R /Count 2 >> endobj -312 0 obj +317 0 obj << /Title (SERIAL2TX) -/Parent 15 0 R -/Prev 309 0 R -/Next 315 0 R -/First 313 0 R -/Last 314 0 R +/Parent 16 0 R +/Prev 314 0 R +/Next 320 0 R +/First 318 0 R +/Last 319 0 R /Count 2 >> endobj -315 0 obj +320 0 obj << /Title (UBTN) -/Parent 15 0 R -/Prev 312 0 R -/First 316 0 R -/Last 318 0 R -/Count 3 ->> -endobj - -17 0 obj -<< -/Title ($1N5) /Parent 16 0 R -/Next 18 0 R -/A 319 0 R +/Prev 317 0 R +/First 321 0 R +/Last 323 0 R +/Count 3 >> endobj 18 0 obj << -/Title ($1N29) -/Parent 16 0 R -/Prev 17 0 R +/Title ($1N5) +/Parent 17 0 R /Next 19 0 R -/A 321 0 R +/A 324 0 R >> endobj 19 0 obj << -/Title ($1N35) -/Parent 16 0 R +/Title ($1N29) +/Parent 17 0 R /Prev 18 0 R /Next 20 0 R -/A 323 0 R +/A 326 0 R >> endobj 20 0 obj << -/Title ($1N54) -/Parent 16 0 R +/Title ($1N35) +/Parent 17 0 R /Prev 19 0 R /Next 21 0 R -/A 325 0 R +/A 328 0 R >> endobj 21 0 obj << -/Title ($1N1528) -/Parent 16 0 R +/Title ($1N54) +/Parent 17 0 R /Prev 20 0 R /Next 22 0 R -/A 327 0 R +/A 330 0 R >> endobj 22 0 obj << -/Title ($1N1550) -/Parent 16 0 R +/Title ($1N1528) +/Parent 17 0 R /Prev 21 0 R /Next 23 0 R -/A 329 0 R +/A 332 0 R >> endobj 23 0 obj << -/Title ($1N1574) -/Parent 16 0 R +/Title ($1N1550) +/Parent 17 0 R /Prev 22 0 R /Next 24 0 R -/A 331 0 R +/A 334 0 R >> endobj 24 0 obj << -/Title ($1N1582) -/Parent 16 0 R +/Title ($1N1574) +/Parent 17 0 R /Prev 23 0 R /Next 25 0 R -/A 333 0 R +/A 336 0 R >> endobj 25 0 obj << -/Title ($1N1616) -/Parent 16 0 R +/Title ($1N1582) +/Parent 17 0 R /Prev 24 0 R /Next 26 0 R -/A 335 0 R +/A 338 0 R >> endobj 26 0 obj << -/Title ($1N1618) -/Parent 16 0 R +/Title ($1N1616) +/Parent 17 0 R /Prev 25 0 R /Next 27 0 R -/A 337 0 R +/A 340 0 R >> endobj 27 0 obj << -/Title ($1N1732) -/Parent 16 0 R +/Title ($1N1618) +/Parent 17 0 R /Prev 26 0 R /Next 28 0 R -/A 339 0 R +/A 342 0 R >> endobj 28 0 obj << -/Title ($1N1742) -/Parent 16 0 R +/Title ($1N1732) +/Parent 17 0 R /Prev 27 0 R /Next 29 0 R -/A 341 0 R +/A 344 0 R >> endobj 29 0 obj << -/Title ($1N1776) -/Parent 16 0 R +/Title ($1N1742) +/Parent 17 0 R /Prev 28 0 R /Next 30 0 R -/A 343 0 R +/A 346 0 R >> endobj 30 0 obj << -/Title ($1N1810) -/Parent 16 0 R +/Title ($1N1776) +/Parent 17 0 R /Prev 29 0 R /Next 31 0 R -/A 345 0 R +/A 348 0 R >> endobj 31 0 obj << -/Title ($1N1860) -/Parent 16 0 R +/Title ($1N1810) +/Parent 17 0 R /Prev 30 0 R /Next 32 0 R -/A 347 0 R +/A 350 0 R >> endobj 32 0 obj << -/Title ($1N1874) -/Parent 16 0 R +/Title ($1N1860) +/Parent 17 0 R /Prev 31 0 R /Next 33 0 R -/A 349 0 R +/A 352 0 R >> endobj 33 0 obj << -/Title ($1N5406) -/Parent 16 0 R +/Title ($1N1874) +/Parent 17 0 R /Prev 32 0 R /Next 34 0 R -/A 351 0 R +/A 354 0 R >> endobj 34 0 obj << -/Title ($1N5445) -/Parent 16 0 R +/Title ($1N5406) +/Parent 17 0 R /Prev 33 0 R -/A 353 0 R +/Next 35 0 R +/A 356 0 R +>> +endobj + +35 0 obj +<< +/Title ($1N5445) +/Parent 17 0 R +/Prev 34 0 R +/Next 36 0 R +/A 358 0 R >> endobj 36 0 obj << -/Title ($1N23) -/Parent 35 0 R -/Next 37 0 R -/A 355 0 R ->> -endobj - -37 0 obj -<< -/Title ($1N31) -/Parent 35 0 R -/Prev 36 0 R -/Next 38 0 R -/A 357 0 R +/Title ($1N10678) +/Parent 17 0 R +/Prev 35 0 R +/A 360 0 R >> endobj 38 0 obj << -/Title ($1N1570) -/Parent 35 0 R -/Prev 37 0 R -/A 359 0 R +/Title ($1N23) +/Parent 37 0 R +/Next 39 0 R +/A 362 0 R +>> +endobj + +39 0 obj +<< +/Title ($1N31) +/Parent 37 0 R +/Prev 38 0 R +/Next 40 0 R +/A 364 0 R >> endobj 40 0 obj << -/Title ($1N1) -/Parent 39 0 R -/A 361 0 R +/Title ($1N1570) +/Parent 37 0 R +/Prev 39 0 R +/A 366 0 R >> endobj 42 0 obj << -/Title ($1N25) +/Title ($1N1) /Parent 41 0 R -/A 363 0 R +/A 368 0 R >> endobj 44 0 obj << -/Title ($1N62) +/Title ($1N25) /Parent 43 0 R -/A 365 0 R +/A 370 0 R >> endobj 46 0 obj << -/Title ($1N15) +/Title ($1N62) /Parent 45 0 R -/Next 47 0 R -/A 367 0 R ->> -endobj - -47 0 obj -<< -/Title ($1N44) -/Parent 45 0 R -/Prev 46 0 R -/Next 48 0 R -/A 369 0 R +/A 372 0 R >> endobj 48 0 obj << -/Title ($1N1852) -/Parent 45 0 R -/Prev 47 0 R +/Title ($1N15) +/Parent 47 0 R /Next 49 0 R -/A 371 0 R +/A 374 0 R >> endobj 49 0 obj << -/Title ($1N1856) -/Parent 45 0 R +/Title ($1N44) +/Parent 47 0 R /Prev 48 0 R -/A 373 0 R +/Next 50 0 R +/A 376 0 R +>> +endobj + +50 0 obj +<< +/Title ($1N1852) +/Parent 47 0 R +/Prev 49 0 R +/Next 51 0 R +/A 378 0 R >> endobj 51 0 obj << -/Title ($1N1494) -/Parent 50 0 R -/Next 52 0 R -/A 375 0 R ->> -endobj - -52 0 obj -<< -/Title ($1N1508) -/Parent 50 0 R -/Prev 51 0 R -/Next 53 0 R -/A 377 0 R +/Title ($1N1856) +/Parent 47 0 R +/Prev 50 0 R +/A 380 0 R >> endobj 53 0 obj << -/Title ($1N1578) -/Parent 50 0 R -/Prev 52 0 R +/Title ($1N1494) +/Parent 52 0 R /Next 54 0 R -/A 379 0 R +/A 382 0 R >> endobj 54 0 obj << -/Title ($1N1846) -/Parent 50 0 R +/Title ($1N1508) +/Parent 52 0 R /Prev 53 0 R /Next 55 0 R -/A 381 0 R +/A 384 0 R >> endobj 55 0 obj << -/Title ($1N1848) -/Parent 50 0 R +/Title ($1N1578) +/Parent 52 0 R /Prev 54 0 R /Next 56 0 R -/A 383 0 R +/A 386 0 R >> endobj 56 0 obj << -/Title ($1N1854) -/Parent 50 0 R +/Title ($1N1846) +/Parent 52 0 R /Prev 55 0 R -/A 385 0 R +/Next 57 0 R +/A 388 0 R +>> +endobj + +57 0 obj +<< +/Title ($1N1848) +/Parent 52 0 R +/Prev 56 0 R +/Next 58 0 R +/A 390 0 R >> endobj 58 0 obj << -/Title ($1N12) -/Parent 57 0 R -/Next 59 0 R -/A 387 0 R ->> -endobj - -59 0 obj -<< -/Title ($1N47) -/Parent 57 0 R -/Prev 58 0 R -/Next 60 0 R -/A 389 0 R +/Title ($1N1854) +/Parent 52 0 R +/Prev 57 0 R +/A 392 0 R >> endobj 60 0 obj << -/Title ($1N1540) -/Parent 57 0 R -/Prev 59 0 R +/Title ($1N12) +/Parent 59 0 R /Next 61 0 R -/A 391 0 R +/A 394 0 R >> endobj 61 0 obj << -/Title ($1N1568) -/Parent 57 0 R +/Title ($1N47) +/Parent 59 0 R /Prev 60 0 R /Next 62 0 R -/A 393 0 R +/A 396 0 R >> endobj 62 0 obj << -/Title ($1N1600) -/Parent 57 0 R +/Title ($1N1540) +/Parent 59 0 R /Prev 61 0 R /Next 63 0 R -/A 395 0 R +/A 398 0 R >> endobj 63 0 obj << -/Title ($1N1636) -/Parent 57 0 R +/Title ($1N1568) +/Parent 59 0 R /Prev 62 0 R /Next 64 0 R -/A 397 0 R +/A 400 0 R >> endobj 64 0 obj << -/Title ($1N1660) -/Parent 57 0 R +/Title ($1N1600) +/Parent 59 0 R /Prev 63 0 R /Next 65 0 R -/A 399 0 R +/A 402 0 R >> endobj 65 0 obj << -/Title ($1N1696) -/Parent 57 0 R +/Title ($1N1636) +/Parent 59 0 R /Prev 64 0 R /Next 66 0 R -/A 401 0 R +/A 404 0 R >> endobj 66 0 obj << -/Title ($1N1710) -/Parent 57 0 R +/Title ($1N1660) +/Parent 59 0 R /Prev 65 0 R /Next 67 0 R -/A 403 0 R +/A 406 0 R >> endobj 67 0 obj << -/Title ($1N1736) -/Parent 57 0 R +/Title ($1N1696) +/Parent 59 0 R /Prev 66 0 R /Next 68 0 R -/A 405 0 R +/A 408 0 R >> endobj 68 0 obj << -/Title ($1N1760) -/Parent 57 0 R +/Title ($1N1710) +/Parent 59 0 R /Prev 67 0 R /Next 69 0 R -/A 407 0 R +/A 410 0 R >> endobj 69 0 obj << -/Title ($1N1778) -/Parent 57 0 R +/Title ($1N1736) +/Parent 59 0 R /Prev 68 0 R /Next 70 0 R -/A 409 0 R +/A 412 0 R >> endobj 70 0 obj << -/Title ($1N1814) -/Parent 57 0 R +/Title ($1N1760) +/Parent 59 0 R /Prev 69 0 R /Next 71 0 R -/A 411 0 R +/A 414 0 R >> endobj 71 0 obj << -/Title ($1N1836) -/Parent 57 0 R +/Title ($1N1778) +/Parent 59 0 R /Prev 70 0 R /Next 72 0 R -/A 413 0 R +/A 416 0 R >> endobj 72 0 obj << -/Title ($1N1870) -/Parent 57 0 R +/Title ($1N1814) +/Parent 59 0 R /Prev 71 0 R -/A 415 0 R +/Next 73 0 R +/A 418 0 R +>> +endobj + +73 0 obj +<< +/Title ($1N1836) +/Parent 59 0 R +/Prev 72 0 R +/Next 74 0 R +/A 420 0 R >> endobj 74 0 obj << -/Title ($1N10) -/Parent 73 0 R -/Next 75 0 R -/A 417 0 R ->> -endobj - -75 0 obj -<< -/Title ($1N49) -/Parent 73 0 R -/Prev 74 0 R -/Next 76 0 R -/A 419 0 R +/Title ($1N1870) +/Parent 59 0 R +/Prev 73 0 R +/A 422 0 R >> endobj 76 0 obj << -/Title ($1N1542) -/Parent 73 0 R -/Prev 75 0 R +/Title ($1N10) +/Parent 75 0 R /Next 77 0 R -/A 421 0 R +/A 424 0 R >> endobj 77 0 obj << -/Title ($1N1566) -/Parent 73 0 R +/Title ($1N49) +/Parent 75 0 R /Prev 76 0 R /Next 78 0 R -/A 423 0 R +/A 426 0 R >> endobj 78 0 obj << -/Title ($1N1602) -/Parent 73 0 R +/Title ($1N1542) +/Parent 75 0 R /Prev 77 0 R /Next 79 0 R -/A 425 0 R +/A 428 0 R >> endobj 79 0 obj << -/Title ($1N1626) -/Parent 73 0 R +/Title ($1N1566) +/Parent 75 0 R /Prev 78 0 R /Next 80 0 R -/A 427 0 R +/A 430 0 R >> endobj 80 0 obj << -/Title ($1N1670) -/Parent 73 0 R +/Title ($1N1602) +/Parent 75 0 R /Prev 79 0 R /Next 81 0 R -/A 429 0 R +/A 432 0 R >> endobj 81 0 obj << -/Title ($1N1686) -/Parent 73 0 R +/Title ($1N1626) +/Parent 75 0 R /Prev 80 0 R /Next 82 0 R -/A 431 0 R +/A 434 0 R >> endobj 82 0 obj << -/Title ($1N1718) -/Parent 73 0 R +/Title ($1N1670) +/Parent 75 0 R /Prev 81 0 R /Next 83 0 R -/A 433 0 R +/A 436 0 R >> endobj 83 0 obj << -/Title ($1N1728) -/Parent 73 0 R +/Title ($1N1686) +/Parent 75 0 R /Prev 82 0 R /Next 84 0 R -/A 435 0 R +/A 438 0 R >> endobj 84 0 obj << -/Title ($1N1752) -/Parent 73 0 R +/Title ($1N1718) +/Parent 75 0 R /Prev 83 0 R /Next 85 0 R -/A 437 0 R +/A 440 0 R >> endobj 85 0 obj << -/Title ($1N1788) -/Parent 73 0 R +/Title ($1N1728) +/Parent 75 0 R /Prev 84 0 R /Next 86 0 R -/A 439 0 R +/A 442 0 R >> endobj 86 0 obj << -/Title ($1N1808) -/Parent 73 0 R +/Title ($1N1752) +/Parent 75 0 R /Prev 85 0 R /Next 87 0 R -/A 441 0 R +/A 444 0 R >> endobj 87 0 obj << -/Title ($1N1828) -/Parent 73 0 R +/Title ($1N1788) +/Parent 75 0 R /Prev 86 0 R /Next 88 0 R -/A 443 0 R +/A 446 0 R >> endobj 88 0 obj << -/Title ($1N1876) -/Parent 73 0 R +/Title ($1N1808) +/Parent 75 0 R /Prev 87 0 R -/A 445 0 R +/Next 89 0 R +/A 448 0 R +>> +endobj + +89 0 obj +<< +/Title ($1N1828) +/Parent 75 0 R +/Prev 88 0 R +/Next 90 0 R +/A 450 0 R >> endobj 90 0 obj << -/Title ($1N1624) -/Parent 89 0 R -/Next 91 0 R -/A 447 0 R ->> -endobj - -91 0 obj -<< -/Title ($1N1672) -/Parent 89 0 R -/Prev 90 0 R -/Next 92 0 R -/A 449 0 R +/Title ($1N1876) +/Parent 75 0 R +/Prev 89 0 R +/A 452 0 R >> endobj 92 0 obj << -/Title ($1N1684) -/Parent 89 0 R -/Prev 91 0 R +/Title ($1N1624) +/Parent 91 0 R /Next 93 0 R -/A 451 0 R +/A 454 0 R >> endobj 93 0 obj << -/Title ($1N1698) -/Parent 89 0 R +/Title ($1N1672) +/Parent 91 0 R /Prev 92 0 R /Next 94 0 R -/A 453 0 R +/A 456 0 R >> endobj 94 0 obj << -/Title ($1N1700) -/Parent 89 0 R +/Title ($1N1684) +/Parent 91 0 R /Prev 93 0 R /Next 95 0 R -/A 455 0 R +/A 458 0 R >> endobj 95 0 obj << -/Title ($1N1702) -/Parent 89 0 R +/Title ($1N1698) +/Parent 91 0 R /Prev 94 0 R /Next 96 0 R -/A 457 0 R +/A 460 0 R >> endobj 96 0 obj << -/Title ($1N1744) -/Parent 89 0 R +/Title ($1N1700) +/Parent 91 0 R /Prev 95 0 R /Next 97 0 R -/A 459 0 R +/A 462 0 R >> endobj 97 0 obj << -/Title ($1N1750) -/Parent 89 0 R +/Title ($1N1702) +/Parent 91 0 R /Prev 96 0 R /Next 98 0 R -/A 461 0 R +/A 464 0 R >> endobj 98 0 obj << -/Title ($1N1820) -/Parent 89 0 R +/Title ($1N1744) +/Parent 91 0 R /Prev 97 0 R /Next 99 0 R -/A 463 0 R +/A 466 0 R >> endobj 99 0 obj << -/Title ($1N1822) -/Parent 89 0 R +/Title ($1N1750) +/Parent 91 0 R /Prev 98 0 R /Next 100 0 R -/A 465 0 R +/A 468 0 R >> endobj 100 0 obj << -/Title ($1N1862) -/Parent 89 0 R +/Title ($1N1820) +/Parent 91 0 R /Prev 99 0 R /Next 101 0 R -/A 467 0 R +/A 470 0 R >> endobj 101 0 obj << -/Title ($1N1864) -/Parent 89 0 R +/Title ($1N1822) +/Parent 91 0 R /Prev 100 0 R -/A 469 0 R +/Next 102 0 R +/A 472 0 R +>> +endobj + +102 0 obj +<< +/Title ($1N1862) +/Parent 91 0 R +/Prev 101 0 R +/Next 103 0 R +/A 474 0 R >> endobj 103 0 obj << -/Title ($1N1678) -/Parent 102 0 R -/A 471 0 R +/Title ($1N1864) +/Parent 91 0 R +/Prev 102 0 R +/A 476 0 R >> endobj 105 0 obj << -/Title ($1N1500) +/Title ($1N1678) /Parent 104 0 R -/Next 106 0 R -/A 473 0 R ->> -endobj - -106 0 obj -<< -/Title ($1N1514) -/Parent 104 0 R -/Prev 105 0 R -/Next 107 0 R -/A 475 0 R +/A 478 0 R >> endobj 107 0 obj << -/Title ($1N5294) -/Parent 104 0 R -/Prev 106 0 R +/Title ($1N1500) +/Parent 106 0 R /Next 108 0 R -/A 477 0 R +/A 480 0 R >> endobj 108 0 obj << -/Title ($1N5448) -/Parent 104 0 R +/Title ($1N1514) +/Parent 106 0 R /Prev 107 0 R -/A 479 0 R +/Next 109 0 R +/A 482 0 R +>> +endobj + +109 0 obj +<< +/Title ($1N5294) +/Parent 106 0 R +/Prev 108 0 R +/Next 110 0 R +/A 484 0 R >> endobj 110 0 obj << -/Title ($1N1506) -/Parent 109 0 R -/Next 111 0 R -/A 481 0 R ->> -endobj - -111 0 obj -<< -/Title ($1N5342) -/Parent 109 0 R -/Prev 110 0 R -/Next 112 0 R -/A 483 0 R +/Title ($1N5448) +/Parent 106 0 R +/Prev 109 0 R +/A 486 0 R >> endobj 112 0 obj << -/Title ($1N5457) -/Parent 109 0 R -/Prev 111 0 R -/A 485 0 R +/Title ($1N1506) +/Parent 111 0 R +/Next 113 0 R +/A 488 0 R +>> +endobj + +113 0 obj +<< +/Title ($1N5342) +/Parent 111 0 R +/Prev 112 0 R +/Next 114 0 R +/A 490 0 R >> endobj 114 0 obj << -/Title ($1N1504) -/Parent 113 0 R -/Next 115 0 R -/A 487 0 R ->> -endobj - -115 0 obj -<< -/Title ($1N5326) -/Parent 113 0 R -/Prev 114 0 R -/Next 116 0 R -/A 489 0 R +/Title ($1N5457) +/Parent 111 0 R +/Prev 113 0 R +/A 492 0 R >> endobj 116 0 obj << -/Title ($1N5454) -/Parent 113 0 R -/Prev 115 0 R -/A 491 0 R +/Title ($1N1504) +/Parent 115 0 R +/Next 117 0 R +/A 494 0 R +>> +endobj + +117 0 obj +<< +/Title ($1N5326) +/Parent 115 0 R +/Prev 116 0 R +/Next 118 0 R +/A 496 0 R >> endobj 118 0 obj << -/Title ($1N1502) -/Parent 117 0 R -/Next 119 0 R -/A 493 0 R ->> -endobj - -119 0 obj -<< -/Title ($1N5310) -/Parent 117 0 R -/Prev 118 0 R -/Next 120 0 R -/A 495 0 R +/Title ($1N5454) +/Parent 115 0 R +/Prev 117 0 R +/A 498 0 R >> endobj 120 0 obj << -/Title ($1N5451) -/Parent 117 0 R -/Prev 119 0 R -/A 497 0 R +/Title ($1N1502) +/Parent 119 0 R +/Next 121 0 R +/A 500 0 R +>> +endobj + +121 0 obj +<< +/Title ($1N5310) +/Parent 119 0 R +/Prev 120 0 R +/Next 122 0 R +/A 502 0 R >> endobj 122 0 obj << -/Title ($1N2) -/Parent 121 0 R -/Next 123 0 R -/A 499 0 R ->> -endobj - -123 0 obj -<< -/Title ($1N6) -/Parent 121 0 R -/Prev 122 0 R -/Next 124 0 R -/A 501 0 R +/Title ($1N5451) +/Parent 119 0 R +/Prev 121 0 R +/A 504 0 R >> endobj 124 0 obj << -/Title ($1N22) -/Parent 121 0 R -/Prev 123 0 R +/Title ($1N2) +/Parent 123 0 R /Next 125 0 R -/A 503 0 R +/A 506 0 R >> endobj 125 0 obj << -/Title ($1N24) -/Parent 121 0 R +/Title ($1N6) +/Parent 123 0 R /Prev 124 0 R /Next 126 0 R -/A 505 0 R +/A 508 0 R >> endobj 126 0 obj << -/Title ($1N26) -/Parent 121 0 R +/Title ($1N22) +/Parent 123 0 R /Prev 125 0 R /Next 127 0 R -/A 507 0 R +/A 510 0 R >> endobj 127 0 obj << -/Title ($1N27) -/Parent 121 0 R +/Title ($1N24) +/Parent 123 0 R /Prev 126 0 R /Next 128 0 R -/A 509 0 R +/A 512 0 R >> endobj 128 0 obj << -/Title ($1N28) -/Parent 121 0 R +/Title ($1N26) +/Parent 123 0 R /Prev 127 0 R /Next 129 0 R -/A 511 0 R +/A 514 0 R >> endobj 129 0 obj << -/Title ($1N30) -/Parent 121 0 R +/Title ($1N27) +/Parent 123 0 R /Prev 128 0 R /Next 130 0 R -/A 513 0 R +/A 516 0 R >> endobj 130 0 obj << -/Title ($1N32) -/Parent 121 0 R +/Title ($1N28) +/Parent 123 0 R /Prev 129 0 R /Next 131 0 R -/A 515 0 R +/A 518 0 R >> endobj 131 0 obj << -/Title ($1N36) -/Parent 121 0 R +/Title ($1N30) +/Parent 123 0 R /Prev 130 0 R /Next 132 0 R -/A 517 0 R +/A 520 0 R >> endobj 132 0 obj << -/Title ($1N37) -/Parent 121 0 R +/Title ($1N32) +/Parent 123 0 R /Prev 131 0 R /Next 133 0 R -/A 519 0 R +/A 522 0 R >> endobj 133 0 obj << -/Title ($1N53) -/Parent 121 0 R +/Title ($1N36) +/Parent 123 0 R /Prev 132 0 R /Next 134 0 R -/A 521 0 R +/A 524 0 R >> endobj 134 0 obj << -/Title ($1N58) -/Parent 121 0 R +/Title ($1N37) +/Parent 123 0 R /Prev 133 0 R /Next 135 0 R -/A 523 0 R +/A 526 0 R >> endobj 135 0 obj << -/Title ($1N61) -/Parent 121 0 R +/Title ($1N53) +/Parent 123 0 R /Prev 134 0 R /Next 136 0 R -/A 525 0 R +/A 528 0 R >> endobj 136 0 obj << -/Title ($1N63) -/Parent 121 0 R +/Title ($1N58) +/Parent 123 0 R /Prev 135 0 R /Next 137 0 R -/A 527 0 R +/A 530 0 R >> endobj 137 0 obj << -/Title ($1N1516) -/Parent 121 0 R +/Title ($1N61) +/Parent 123 0 R /Prev 136 0 R /Next 138 0 R -/A 529 0 R +/A 532 0 R >> endobj 138 0 obj << -/Title ($1N1518) -/Parent 121 0 R +/Title ($1N63) +/Parent 123 0 R /Prev 137 0 R /Next 139 0 R -/A 531 0 R +/A 534 0 R >> endobj 139 0 obj << -/Title ($1N1524) -/Parent 121 0 R +/Title ($1N1516) +/Parent 123 0 R /Prev 138 0 R /Next 140 0 R -/A 533 0 R +/A 536 0 R >> endobj 140 0 obj << -/Title ($1N1526) -/Parent 121 0 R +/Title ($1N1518) +/Parent 123 0 R /Prev 139 0 R /Next 141 0 R -/A 535 0 R +/A 538 0 R >> endobj 141 0 obj << -/Title ($1N1530) -/Parent 121 0 R +/Title ($1N1524) +/Parent 123 0 R /Prev 140 0 R /Next 142 0 R -/A 537 0 R +/A 540 0 R >> endobj 142 0 obj << -/Title ($1N1532) -/Parent 121 0 R +/Title ($1N1526) +/Parent 123 0 R /Prev 141 0 R /Next 143 0 R -/A 539 0 R +/A 542 0 R >> endobj 143 0 obj << -/Title ($1N1534) -/Parent 121 0 R +/Title ($1N1530) +/Parent 123 0 R /Prev 142 0 R /Next 144 0 R -/A 541 0 R +/A 544 0 R >> endobj 144 0 obj << -/Title ($1N1552) -/Parent 121 0 R +/Title ($1N1532) +/Parent 123 0 R /Prev 143 0 R /Next 145 0 R -/A 543 0 R +/A 546 0 R >> endobj 145 0 obj << -/Title ($1N1554) -/Parent 121 0 R +/Title ($1N1534) +/Parent 123 0 R /Prev 144 0 R /Next 146 0 R -/A 545 0 R +/A 548 0 R >> endobj 146 0 obj << -/Title ($1N1572) -/Parent 121 0 R +/Title ($1N1552) +/Parent 123 0 R /Prev 145 0 R /Next 147 0 R -/A 547 0 R +/A 550 0 R >> endobj 147 0 obj << -/Title ($1N1576) -/Parent 121 0 R +/Title ($1N1554) +/Parent 123 0 R /Prev 146 0 R /Next 148 0 R -/A 549 0 R +/A 552 0 R >> endobj 148 0 obj << -/Title ($1N1580) -/Parent 121 0 R +/Title ($1N1572) +/Parent 123 0 R /Prev 147 0 R /Next 149 0 R -/A 551 0 R +/A 554 0 R >> endobj 149 0 obj << -/Title ($1N1584) -/Parent 121 0 R +/Title ($1N1576) +/Parent 123 0 R /Prev 148 0 R /Next 150 0 R -/A 553 0 R +/A 556 0 R >> endobj 150 0 obj << -/Title ($1N1586) -/Parent 121 0 R +/Title ($1N1580) +/Parent 123 0 R /Prev 149 0 R /Next 151 0 R -/A 555 0 R +/A 558 0 R >> endobj 151 0 obj << -/Title ($1N1588) -/Parent 121 0 R +/Title ($1N1584) +/Parent 123 0 R /Prev 150 0 R /Next 152 0 R -/A 557 0 R +/A 560 0 R >> endobj 152 0 obj << -/Title ($1N1590) -/Parent 121 0 R +/Title ($1N1586) +/Parent 123 0 R /Prev 151 0 R /Next 153 0 R -/A 559 0 R +/A 562 0 R >> endobj 153 0 obj << -/Title ($1N1592) -/Parent 121 0 R +/Title ($1N1588) +/Parent 123 0 R /Prev 152 0 R /Next 154 0 R -/A 561 0 R +/A 564 0 R >> endobj 154 0 obj << -/Title ($1N1594) -/Parent 121 0 R +/Title ($1N1590) +/Parent 123 0 R /Prev 153 0 R /Next 155 0 R -/A 563 0 R +/A 566 0 R >> endobj 155 0 obj << -/Title ($1N1596) -/Parent 121 0 R +/Title ($1N1592) +/Parent 123 0 R /Prev 154 0 R /Next 156 0 R -/A 565 0 R +/A 568 0 R >> endobj 156 0 obj << -/Title ($1N1598) -/Parent 121 0 R +/Title ($1N1594) +/Parent 123 0 R /Prev 155 0 R /Next 157 0 R -/A 567 0 R +/A 570 0 R >> endobj 157 0 obj << -/Title ($1N1614) -/Parent 121 0 R +/Title ($1N1596) +/Parent 123 0 R /Prev 156 0 R /Next 158 0 R -/A 569 0 R +/A 572 0 R >> endobj 158 0 obj << -/Title ($1N1638) -/Parent 121 0 R +/Title ($1N1598) +/Parent 123 0 R /Prev 157 0 R /Next 159 0 R -/A 571 0 R +/A 574 0 R >> endobj 159 0 obj << -/Title ($1N1640) -/Parent 121 0 R +/Title ($1N1614) +/Parent 123 0 R /Prev 158 0 R /Next 160 0 R -/A 573 0 R +/A 576 0 R >> endobj 160 0 obj << -/Title ($1N1642) -/Parent 121 0 R +/Title ($1N1638) +/Parent 123 0 R /Prev 159 0 R /Next 161 0 R -/A 575 0 R +/A 578 0 R >> endobj 161 0 obj << -/Title ($1N1644) -/Parent 121 0 R +/Title ($1N1640) +/Parent 123 0 R /Prev 160 0 R /Next 162 0 R -/A 577 0 R +/A 580 0 R >> endobj 162 0 obj << -/Title ($1N1646) -/Parent 121 0 R +/Title ($1N1642) +/Parent 123 0 R /Prev 161 0 R /Next 163 0 R -/A 579 0 R +/A 582 0 R >> endobj 163 0 obj << -/Title ($1N1648) -/Parent 121 0 R +/Title ($1N1644) +/Parent 123 0 R /Prev 162 0 R /Next 164 0 R -/A 581 0 R +/A 584 0 R >> endobj 164 0 obj << -/Title ($1N1650) -/Parent 121 0 R +/Title ($1N1646) +/Parent 123 0 R /Prev 163 0 R /Next 165 0 R -/A 583 0 R +/A 586 0 R >> endobj 165 0 obj << -/Title ($1N1652) -/Parent 121 0 R +/Title ($1N1648) +/Parent 123 0 R /Prev 164 0 R /Next 166 0 R -/A 585 0 R +/A 588 0 R >> endobj 166 0 obj << -/Title ($1N1656) -/Parent 121 0 R +/Title ($1N1650) +/Parent 123 0 R /Prev 165 0 R /Next 167 0 R -/A 587 0 R +/A 590 0 R >> endobj 167 0 obj << -/Title ($1N1658) -/Parent 121 0 R +/Title ($1N1652) +/Parent 123 0 R /Prev 166 0 R /Next 168 0 R -/A 589 0 R +/A 592 0 R >> endobj 168 0 obj << -/Title ($1N1708) -/Parent 121 0 R +/Title ($1N1656) +/Parent 123 0 R /Prev 167 0 R /Next 169 0 R -/A 591 0 R +/A 594 0 R >> endobj 169 0 obj << -/Title ($1N1730) -/Parent 121 0 R +/Title ($1N1658) +/Parent 123 0 R /Prev 168 0 R /Next 170 0 R -/A 593 0 R +/A 596 0 R >> endobj 170 0 obj << -/Title ($1N1734) -/Parent 121 0 R +/Title ($1N1708) +/Parent 123 0 R /Prev 169 0 R /Next 171 0 R -/A 595 0 R +/A 598 0 R >> endobj 171 0 obj << -/Title ($1N1740) -/Parent 121 0 R +/Title ($1N1730) +/Parent 123 0 R /Prev 170 0 R /Next 172 0 R -/A 597 0 R +/A 600 0 R >> endobj 172 0 obj << -/Title ($1N1762) -/Parent 121 0 R +/Title ($1N1734) +/Parent 123 0 R /Prev 171 0 R /Next 173 0 R -/A 599 0 R +/A 602 0 R >> endobj 173 0 obj << -/Title ($1N1764) -/Parent 121 0 R +/Title ($1N1740) +/Parent 123 0 R /Prev 172 0 R /Next 174 0 R -/A 601 0 R +/A 604 0 R >> endobj 174 0 obj << -/Title ($1N1766) -/Parent 121 0 R +/Title ($1N1762) +/Parent 123 0 R /Prev 173 0 R /Next 175 0 R -/A 603 0 R +/A 606 0 R >> endobj 175 0 obj << -/Title ($1N1768) -/Parent 121 0 R +/Title ($1N1764) +/Parent 123 0 R /Prev 174 0 R /Next 176 0 R -/A 605 0 R +/A 608 0 R >> endobj 176 0 obj << -/Title ($1N1770) -/Parent 121 0 R +/Title ($1N1766) +/Parent 123 0 R /Prev 175 0 R /Next 177 0 R -/A 607 0 R +/A 610 0 R >> endobj 177 0 obj << -/Title ($1N1774) -/Parent 121 0 R +/Title ($1N1768) +/Parent 123 0 R /Prev 176 0 R /Next 178 0 R -/A 609 0 R +/A 612 0 R >> endobj 178 0 obj << -/Title ($1N1790) -/Parent 121 0 R +/Title ($1N1770) +/Parent 123 0 R /Prev 177 0 R /Next 179 0 R -/A 611 0 R +/A 614 0 R >> endobj 179 0 obj << -/Title ($1N1792) -/Parent 121 0 R +/Title ($1N1774) +/Parent 123 0 R /Prev 178 0 R /Next 180 0 R -/A 613 0 R +/A 616 0 R >> endobj 180 0 obj << -/Title ($1N1796) -/Parent 121 0 R +/Title ($1N1790) +/Parent 123 0 R /Prev 179 0 R /Next 181 0 R -/A 615 0 R +/A 618 0 R >> endobj 181 0 obj << -/Title ($1N1798) -/Parent 121 0 R +/Title ($1N1792) +/Parent 123 0 R /Prev 180 0 R /Next 182 0 R -/A 617 0 R +/A 620 0 R >> endobj 182 0 obj << -/Title ($1N1800) -/Parent 121 0 R +/Title ($1N1796) +/Parent 123 0 R /Prev 181 0 R /Next 183 0 R -/A 619 0 R +/A 622 0 R >> endobj 183 0 obj << -/Title ($1N1824) -/Parent 121 0 R +/Title ($1N1798) +/Parent 123 0 R /Prev 182 0 R /Next 184 0 R -/A 621 0 R +/A 624 0 R >> endobj 184 0 obj << -/Title ($1N1826) -/Parent 121 0 R +/Title ($1N1800) +/Parent 123 0 R /Prev 183 0 R /Next 185 0 R -/A 623 0 R +/A 626 0 R >> endobj 185 0 obj << -/Title ($1N1838) -/Parent 121 0 R +/Title ($1N1824) +/Parent 123 0 R /Prev 184 0 R /Next 186 0 R -/A 625 0 R +/A 628 0 R >> endobj 186 0 obj << -/Title ($1N1840) -/Parent 121 0 R +/Title ($1N1826) +/Parent 123 0 R /Prev 185 0 R /Next 187 0 R -/A 627 0 R +/A 630 0 R >> endobj 187 0 obj << -/Title ($1N1842) -/Parent 121 0 R +/Title ($1N1838) +/Parent 123 0 R /Prev 186 0 R /Next 188 0 R -/A 629 0 R +/A 632 0 R >> endobj 188 0 obj << -/Title ($1N1844) -/Parent 121 0 R +/Title ($1N1840) +/Parent 123 0 R /Prev 187 0 R /Next 189 0 R -/A 631 0 R +/A 634 0 R >> endobj 189 0 obj << -/Title ($1N1850) -/Parent 121 0 R +/Title ($1N1842) +/Parent 123 0 R /Prev 188 0 R /Next 190 0 R -/A 633 0 R +/A 636 0 R >> endobj 190 0 obj << -/Title ($1N1858) -/Parent 121 0 R +/Title ($1N1844) +/Parent 123 0 R /Prev 189 0 R /Next 191 0 R -/A 635 0 R +/A 638 0 R >> endobj 191 0 obj << -/Title ($1N1866) -/Parent 121 0 R +/Title ($1N1850) +/Parent 123 0 R /Prev 190 0 R /Next 192 0 R -/A 637 0 R +/A 640 0 R >> endobj 192 0 obj << -/Title ($1N1884) -/Parent 121 0 R +/Title ($1N1858) +/Parent 123 0 R /Prev 191 0 R /Next 193 0 R -/A 639 0 R +/A 642 0 R >> endobj 193 0 obj << -/Title ($1N1886) -/Parent 121 0 R +/Title ($1N1866) +/Parent 123 0 R /Prev 192 0 R /Next 194 0 R -/A 641 0 R +/A 644 0 R >> endobj 194 0 obj << -/Title ($1N5390) -/Parent 121 0 R +/Title ($1N1884) +/Parent 123 0 R /Prev 193 0 R /Next 195 0 R -/A 643 0 R +/A 646 0 R >> endobj 195 0 obj << -/Title ($1N5442) -/Parent 121 0 R +/Title ($1N1886) +/Parent 123 0 R /Prev 194 0 R -/A 645 0 R +/Next 196 0 R +/A 648 0 R +>> +endobj + +196 0 obj +<< +/Title ($1N5390) +/Parent 123 0 R +/Prev 195 0 R +/Next 197 0 R +/A 650 0 R >> endobj 197 0 obj << -/Title ($1N19) -/Parent 196 0 R +/Title ($1N5442) +/Parent 123 0 R +/Prev 196 0 R /Next 198 0 R -/A 647 0 R +/A 652 0 R >> endobj 198 0 obj << -/Title ($1N40) -/Parent 196 0 R +/Title ($1N10681) +/Parent 123 0 R /Prev 197 0 R -/A 649 0 R +/A 654 0 R >> endobj 200 0 obj << -/Title ($1N21) +/Title ($1N19) /Parent 199 0 R /Next 201 0 R -/A 651 0 R +/A 656 0 R >> endobj 201 0 obj << -/Title ($1N38) +/Title ($1N40) /Parent 199 0 R /Prev 200 0 R -/A 653 0 R +/A 658 0 R >> endobj @@ -31457,1890 +32212,1941 @@ endobj /Title ($1N20) /Parent 202 0 R /Next 204 0 R -/A 655 0 R +/A 660 0 R >> endobj 204 0 obj << -/Title ($1N39) +/Title ($1N8820) /Parent 202 0 R /Prev 203 0 R -/A 657 0 R +/Next 205 0 R +/A 662 0 R >> endobj -206 0 obj +205 0 obj << -/Title ($1N11) -/Parent 205 0 R -/Next 207 0 R -/A 659 0 R +/Title ($1N10687) +/Parent 202 0 R +/Prev 204 0 R +/A 664 0 R >> endobj 207 0 obj << -/Title ($1N33) -/Parent 205 0 R -/Prev 206 0 R +/Title ($1N21) +/Parent 206 0 R /Next 208 0 R -/A 661 0 R +/A 666 0 R >> endobj 208 0 obj << -/Title ($1N48) -/Parent 205 0 R +/Title ($1N8817) +/Parent 206 0 R /Prev 207 0 R /Next 209 0 R -/A 663 0 R +/A 668 0 R >> endobj 209 0 obj << -/Title ($1N1538) -/Parent 205 0 R +/Title ($1N10684) +/Parent 206 0 R /Prev 208 0 R -/Next 210 0 R -/A 665 0 R ->> -endobj - -210 0 obj -<< -/Title ($1N1556) -/Parent 205 0 R -/Prev 209 0 R -/Next 211 0 R -/A 667 0 R +/A 670 0 R >> endobj 211 0 obj << -/Title ($1N1610) -/Parent 205 0 R -/Prev 210 0 R +/Title ($1N11) +/Parent 210 0 R /Next 212 0 R -/A 669 0 R +/A 672 0 R >> endobj 212 0 obj << -/Title ($1N1622) -/Parent 205 0 R +/Title ($1N33) +/Parent 210 0 R /Prev 211 0 R /Next 213 0 R -/A 671 0 R +/A 674 0 R >> endobj 213 0 obj << -/Title ($1N1674) -/Parent 205 0 R +/Title ($1N48) +/Parent 210 0 R /Prev 212 0 R /Next 214 0 R -/A 673 0 R +/A 676 0 R >> endobj 214 0 obj << -/Title ($1N1682) -/Parent 205 0 R +/Title ($1N1538) +/Parent 210 0 R /Prev 213 0 R /Next 215 0 R -/A 675 0 R +/A 678 0 R >> endobj 215 0 obj << -/Title ($1N1706) -/Parent 205 0 R +/Title ($1N1556) +/Parent 210 0 R /Prev 214 0 R /Next 216 0 R -/A 677 0 R +/A 680 0 R >> endobj 216 0 obj << -/Title ($1N1738) -/Parent 205 0 R +/Title ($1N1610) +/Parent 210 0 R /Prev 215 0 R /Next 217 0 R -/A 679 0 R +/A 682 0 R >> endobj 217 0 obj << -/Title ($1N1748) -/Parent 205 0 R +/Title ($1N1622) +/Parent 210 0 R /Prev 216 0 R /Next 218 0 R -/A 681 0 R +/A 684 0 R >> endobj 218 0 obj << -/Title ($1N1772) -/Parent 205 0 R +/Title ($1N1674) +/Parent 210 0 R /Prev 217 0 R /Next 219 0 R -/A 683 0 R +/A 686 0 R >> endobj 219 0 obj << -/Title ($1N1816) -/Parent 205 0 R +/Title ($1N1682) +/Parent 210 0 R /Prev 218 0 R /Next 220 0 R -/A 685 0 R +/A 688 0 R >> endobj 220 0 obj << -/Title ($1N1868) -/Parent 205 0 R +/Title ($1N1706) +/Parent 210 0 R /Prev 219 0 R -/A 687 0 R +/Next 221 0 R +/A 690 0 R +>> +endobj + +221 0 obj +<< +/Title ($1N1738) +/Parent 210 0 R +/Prev 220 0 R +/Next 222 0 R +/A 692 0 R >> endobj 222 0 obj << -/Title ($1N1818) -/Parent 221 0 R -/A 689 0 R +/Title ($1N1748) +/Parent 210 0 R +/Prev 221 0 R +/Next 223 0 R +/A 694 0 R +>> +endobj + +223 0 obj +<< +/Title ($1N1772) +/Parent 210 0 R +/Prev 222 0 R +/Next 224 0 R +/A 696 0 R >> endobj 224 0 obj << -/Title ($1N7) -/Parent 223 0 R +/Title ($1N1816) +/Parent 210 0 R +/Prev 223 0 R /Next 225 0 R -/A 691 0 R +/A 698 0 R >> endobj 225 0 obj << -/Title ($1N52) -/Parent 223 0 R +/Title ($1N1868) +/Parent 210 0 R /Prev 224 0 R -/Next 226 0 R -/A 693 0 R ->> -endobj - -226 0 obj -<< -/Title ($1N1548) -/Parent 223 0 R -/Prev 225 0 R -/Next 227 0 R -/A 695 0 R +/A 700 0 R >> endobj 227 0 obj << -/Title ($1N1560) -/Parent 223 0 R -/Prev 226 0 R -/Next 228 0 R -/A 697 0 R ->> -endobj - -228 0 obj -<< -/Title ($1N1608) -/Parent 223 0 R -/Prev 227 0 R -/Next 229 0 R -/A 699 0 R +/Title ($1N1818) +/Parent 226 0 R +/A 702 0 R >> endobj 229 0 obj << -/Title ($1N1630) -/Parent 223 0 R -/Prev 228 0 R +/Title ($1N7) +/Parent 228 0 R /Next 230 0 R -/A 701 0 R +/A 704 0 R >> endobj 230 0 obj << -/Title ($1N1666) -/Parent 223 0 R +/Title ($1N52) +/Parent 228 0 R /Prev 229 0 R /Next 231 0 R -/A 703 0 R +/A 706 0 R >> endobj 231 0 obj << -/Title ($1N1690) -/Parent 223 0 R +/Title ($1N1548) +/Parent 228 0 R /Prev 230 0 R /Next 232 0 R -/A 705 0 R +/A 708 0 R >> endobj 232 0 obj << -/Title ($1N1714) -/Parent 223 0 R +/Title ($1N1560) +/Parent 228 0 R /Prev 231 0 R /Next 233 0 R -/A 707 0 R +/A 710 0 R >> endobj 233 0 obj << -/Title ($1N1720) -/Parent 223 0 R +/Title ($1N1608) +/Parent 228 0 R /Prev 232 0 R /Next 234 0 R -/A 709 0 R +/A 712 0 R >> endobj 234 0 obj << -/Title ($1N1756) -/Parent 223 0 R +/Title ($1N1630) +/Parent 228 0 R /Prev 233 0 R /Next 235 0 R -/A 711 0 R +/A 714 0 R >> endobj 235 0 obj << -/Title ($1N1782) -/Parent 223 0 R +/Title ($1N1666) +/Parent 228 0 R /Prev 234 0 R /Next 236 0 R -/A 713 0 R +/A 716 0 R >> endobj 236 0 obj << -/Title ($1N1804) -/Parent 223 0 R +/Title ($1N1690) +/Parent 228 0 R /Prev 235 0 R /Next 237 0 R -/A 715 0 R +/A 718 0 R >> endobj 237 0 obj << -/Title ($1N1834) -/Parent 223 0 R +/Title ($1N1714) +/Parent 228 0 R /Prev 236 0 R /Next 238 0 R -/A 717 0 R +/A 720 0 R >> endobj 238 0 obj << -/Title ($1N1880) -/Parent 223 0 R +/Title ($1N1720) +/Parent 228 0 R /Prev 237 0 R -/A 719 0 R +/Next 239 0 R +/A 722 0 R +>> +endobj + +239 0 obj +<< +/Title ($1N1756) +/Parent 228 0 R +/Prev 238 0 R +/Next 240 0 R +/A 724 0 R >> endobj 240 0 obj << -/Title ($1N8) -/Parent 239 0 R +/Title ($1N1782) +/Parent 228 0 R +/Prev 239 0 R /Next 241 0 R -/A 721 0 R +/A 726 0 R >> endobj 241 0 obj << -/Title ($1N51) -/Parent 239 0 R +/Title ($1N1804) +/Parent 228 0 R /Prev 240 0 R /Next 242 0 R -/A 723 0 R +/A 728 0 R >> endobj 242 0 obj << -/Title ($1N1546) -/Parent 239 0 R +/Title ($1N1834) +/Parent 228 0 R /Prev 241 0 R /Next 243 0 R -/A 725 0 R +/A 730 0 R >> endobj 243 0 obj << -/Title ($1N1562) -/Parent 239 0 R +/Title ($1N1880) +/Parent 228 0 R /Prev 242 0 R -/Next 244 0 R -/A 727 0 R ->> -endobj - -244 0 obj -<< -/Title ($1N1606) -/Parent 239 0 R -/Prev 243 0 R -/Next 245 0 R -/A 729 0 R +/A 732 0 R >> endobj 245 0 obj << -/Title ($1N1628) -/Parent 239 0 R -/Prev 244 0 R +/Title ($1N8) +/Parent 244 0 R /Next 246 0 R -/A 731 0 R +/A 734 0 R >> endobj 246 0 obj << -/Title ($1N1668) -/Parent 239 0 R +/Title ($1N51) +/Parent 244 0 R /Prev 245 0 R /Next 247 0 R -/A 733 0 R +/A 736 0 R >> endobj 247 0 obj << -/Title ($1N1688) -/Parent 239 0 R +/Title ($1N1546) +/Parent 244 0 R /Prev 246 0 R /Next 248 0 R -/A 735 0 R +/A 738 0 R >> endobj 248 0 obj << -/Title ($1N1716) -/Parent 239 0 R +/Title ($1N1562) +/Parent 244 0 R /Prev 247 0 R /Next 249 0 R -/A 737 0 R +/A 740 0 R >> endobj 249 0 obj << -/Title ($1N1722) -/Parent 239 0 R +/Title ($1N1606) +/Parent 244 0 R /Prev 248 0 R /Next 250 0 R -/A 739 0 R +/A 742 0 R >> endobj 250 0 obj << -/Title ($1N1754) -/Parent 239 0 R +/Title ($1N1628) +/Parent 244 0 R /Prev 249 0 R /Next 251 0 R -/A 741 0 R +/A 744 0 R >> endobj 251 0 obj << -/Title ($1N1784) -/Parent 239 0 R +/Title ($1N1668) +/Parent 244 0 R /Prev 250 0 R /Next 252 0 R -/A 743 0 R +/A 746 0 R >> endobj 252 0 obj << -/Title ($1N1802) -/Parent 239 0 R +/Title ($1N1688) +/Parent 244 0 R /Prev 251 0 R /Next 253 0 R -/A 745 0 R +/A 748 0 R >> endobj 253 0 obj << -/Title ($1N1832) -/Parent 239 0 R +/Title ($1N1716) +/Parent 244 0 R /Prev 252 0 R /Next 254 0 R -/A 747 0 R +/A 750 0 R >> endobj 254 0 obj << -/Title ($1N1882) -/Parent 239 0 R +/Title ($1N1722) +/Parent 244 0 R /Prev 253 0 R /Next 255 0 R -/A 749 0 R +/A 752 0 R >> endobj 255 0 obj << -/Title ($1N5374) -/Parent 239 0 R +/Title ($1N1754) +/Parent 244 0 R /Prev 254 0 R /Next 256 0 R -/A 751 0 R +/A 754 0 R >> endobj 256 0 obj << -/Title ($1N5481) -/Parent 239 0 R +/Title ($1N1784) +/Parent 244 0 R /Prev 255 0 R -/A 753 0 R +/Next 257 0 R +/A 756 0 R +>> +endobj + +257 0 obj +<< +/Title ($1N1802) +/Parent 244 0 R +/Prev 256 0 R +/Next 258 0 R +/A 758 0 R >> endobj 258 0 obj << -/Title ($1N13) -/Parent 257 0 R +/Title ($1N1832) +/Parent 244 0 R +/Prev 257 0 R /Next 259 0 R -/A 755 0 R +/A 760 0 R >> endobj 259 0 obj << -/Title ($1N34) -/Parent 257 0 R +/Title ($1N1882) +/Parent 244 0 R /Prev 258 0 R /Next 260 0 R -/A 757 0 R +/A 762 0 R >> endobj 260 0 obj << -/Title ($1N46) -/Parent 257 0 R +/Title ($1N5374) +/Parent 244 0 R /Prev 259 0 R /Next 261 0 R -/A 759 0 R +/A 764 0 R >> endobj 261 0 obj << -/Title ($1N1536) -/Parent 257 0 R +/Title ($1N5481) +/Parent 244 0 R /Prev 260 0 R -/Next 262 0 R -/A 761 0 R ->> -endobj - -262 0 obj -<< -/Title ($1N1558) -/Parent 257 0 R -/Prev 261 0 R -/Next 263 0 R -/A 763 0 R +/A 766 0 R >> endobj 263 0 obj << -/Title ($1N1612) -/Parent 257 0 R -/Prev 262 0 R +/Title ($1N13) +/Parent 262 0 R /Next 264 0 R -/A 765 0 R +/A 768 0 R >> endobj 264 0 obj << -/Title ($1N1620) -/Parent 257 0 R +/Title ($1N34) +/Parent 262 0 R /Prev 263 0 R /Next 265 0 R -/A 767 0 R +/A 770 0 R >> endobj 265 0 obj << -/Title ($1N1676) -/Parent 257 0 R +/Title ($1N46) +/Parent 262 0 R /Prev 264 0 R /Next 266 0 R -/A 769 0 R +/A 772 0 R >> endobj 266 0 obj << -/Title ($1N1680) -/Parent 257 0 R +/Title ($1N1536) +/Parent 262 0 R /Prev 265 0 R /Next 267 0 R -/A 771 0 R +/A 774 0 R >> endobj 267 0 obj << -/Title ($1N1704) -/Parent 257 0 R +/Title ($1N1558) +/Parent 262 0 R /Prev 266 0 R /Next 268 0 R -/A 773 0 R +/A 776 0 R >> endobj 268 0 obj << -/Title ($1N1726) -/Parent 257 0 R +/Title ($1N1612) +/Parent 262 0 R /Prev 267 0 R /Next 269 0 R -/A 775 0 R +/A 778 0 R >> endobj 269 0 obj << -/Title ($1N1746) -/Parent 257 0 R +/Title ($1N1620) +/Parent 262 0 R /Prev 268 0 R /Next 270 0 R -/A 777 0 R +/A 780 0 R >> endobj 270 0 obj << -/Title ($1N1780) -/Parent 257 0 R +/Title ($1N1676) +/Parent 262 0 R /Prev 269 0 R /Next 271 0 R -/A 779 0 R +/A 782 0 R >> endobj 271 0 obj << -/Title ($1N1812) -/Parent 257 0 R +/Title ($1N1680) +/Parent 262 0 R /Prev 270 0 R /Next 272 0 R -/A 781 0 R +/A 784 0 R >> endobj 272 0 obj << -/Title ($1N1872) -/Parent 257 0 R +/Title ($1N1704) +/Parent 262 0 R /Prev 271 0 R -/A 783 0 R +/Next 273 0 R +/A 786 0 R +>> +endobj + +273 0 obj +<< +/Title ($1N1726) +/Parent 262 0 R +/Prev 272 0 R +/Next 274 0 R +/A 788 0 R >> endobj 274 0 obj << -/Title ($1N14) -/Parent 273 0 R +/Title ($1N1746) +/Parent 262 0 R +/Prev 273 0 R /Next 275 0 R -/A 785 0 R +/A 790 0 R >> endobj 275 0 obj << -/Title ($1N45) -/Parent 273 0 R +/Title ($1N1780) +/Parent 262 0 R /Prev 274 0 R /Next 276 0 R -/A 787 0 R +/A 792 0 R >> endobj 276 0 obj << -/Title ($1N1520) -/Parent 273 0 R +/Title ($1N1812) +/Parent 262 0 R /Prev 275 0 R -/A 789 0 R +/Next 277 0 R +/A 794 0 R >> endobj -278 0 obj +277 0 obj << -/Title ($1N4) -/Parent 277 0 R -/Next 279 0 R -/A 791 0 R +/Title ($1N1872) +/Parent 262 0 R +/Prev 276 0 R +/A 796 0 R >> endobj 279 0 obj << -/Title ($1N56) -/Parent 277 0 R -/Prev 278 0 R +/Title ($1N14) +/Parent 278 0 R /Next 280 0 R -/A 793 0 R +/A 798 0 R >> endobj 280 0 obj << -/Title ($1N57) -/Parent 277 0 R +/Title ($1N45) +/Parent 278 0 R /Prev 279 0 R /Next 281 0 R -/A 795 0 R +/A 800 0 R >> endobj 281 0 obj << -/Title ($1N1634) -/Parent 277 0 R +/Title ($1N1520) +/Parent 278 0 R /Prev 280 0 R -/Next 282 0 R -/A 797 0 R ->> -endobj - -282 0 obj -<< -/Title ($1N1662) -/Parent 277 0 R -/Prev 281 0 R -/Next 283 0 R -/A 799 0 R +/A 802 0 R >> endobj 283 0 obj << -/Title ($1N1694) -/Parent 277 0 R -/Prev 282 0 R +/Title ($1N4) +/Parent 282 0 R /Next 284 0 R -/A 801 0 R +/A 804 0 R >> endobj 284 0 obj << -/Title ($1N1794) -/Parent 277 0 R +/Title ($1N56) +/Parent 282 0 R /Prev 283 0 R -/A 803 0 R +/Next 285 0 R +/A 806 0 R +>> +endobj + +285 0 obj +<< +/Title ($1N57) +/Parent 282 0 R +/Prev 284 0 R +/Next 286 0 R +/A 808 0 R >> endobj 286 0 obj << -/Title ($1N9) -/Parent 285 0 R +/Title ($1N1634) +/Parent 282 0 R +/Prev 285 0 R /Next 287 0 R -/A 805 0 R +/A 810 0 R >> endobj 287 0 obj << -/Title ($1N50) -/Parent 285 0 R +/Title ($1N1662) +/Parent 282 0 R /Prev 286 0 R /Next 288 0 R -/A 807 0 R +/A 812 0 R >> endobj 288 0 obj << -/Title ($1N1544) -/Parent 285 0 R +/Title ($1N1694) +/Parent 282 0 R /Prev 287 0 R /Next 289 0 R -/A 809 0 R +/A 814 0 R >> endobj 289 0 obj << -/Title ($1N1564) -/Parent 285 0 R +/Title ($1N1794) +/Parent 282 0 R /Prev 288 0 R -/Next 290 0 R -/A 811 0 R ->> -endobj - -290 0 obj -<< -/Title ($1N1604) -/Parent 285 0 R -/Prev 289 0 R -/Next 291 0 R -/A 813 0 R +/A 816 0 R >> endobj 291 0 obj << -/Title ($1N1632) -/Parent 285 0 R -/Prev 290 0 R +/Title ($1N9) +/Parent 290 0 R /Next 292 0 R -/A 815 0 R +/A 818 0 R >> endobj 292 0 obj << -/Title ($1N1664) -/Parent 285 0 R +/Title ($1N50) +/Parent 290 0 R /Prev 291 0 R /Next 293 0 R -/A 817 0 R +/A 820 0 R >> endobj 293 0 obj << -/Title ($1N1692) -/Parent 285 0 R +/Title ($1N1544) +/Parent 290 0 R /Prev 292 0 R /Next 294 0 R -/A 819 0 R +/A 822 0 R >> endobj 294 0 obj << -/Title ($1N1712) -/Parent 285 0 R +/Title ($1N1564) +/Parent 290 0 R /Prev 293 0 R /Next 295 0 R -/A 821 0 R +/A 824 0 R >> endobj 295 0 obj << -/Title ($1N1724) -/Parent 285 0 R +/Title ($1N1604) +/Parent 290 0 R /Prev 294 0 R /Next 296 0 R -/A 823 0 R +/A 826 0 R >> endobj 296 0 obj << -/Title ($1N1758) -/Parent 285 0 R +/Title ($1N1632) +/Parent 290 0 R /Prev 295 0 R /Next 297 0 R -/A 825 0 R +/A 828 0 R >> endobj 297 0 obj << -/Title ($1N1786) -/Parent 285 0 R +/Title ($1N1664) +/Parent 290 0 R /Prev 296 0 R /Next 298 0 R -/A 827 0 R +/A 830 0 R >> endobj 298 0 obj << -/Title ($1N1806) -/Parent 285 0 R +/Title ($1N1692) +/Parent 290 0 R /Prev 297 0 R /Next 299 0 R -/A 829 0 R +/A 832 0 R >> endobj 299 0 obj << -/Title ($1N1830) -/Parent 285 0 R +/Title ($1N1712) +/Parent 290 0 R /Prev 298 0 R /Next 300 0 R -/A 831 0 R +/A 834 0 R >> endobj 300 0 obj << -/Title ($1N1878) -/Parent 285 0 R +/Title ($1N1724) +/Parent 290 0 R /Prev 299 0 R /Next 301 0 R -/A 833 0 R +/A 836 0 R >> endobj 301 0 obj << -/Title ($1N5358) -/Parent 285 0 R +/Title ($1N1758) +/Parent 290 0 R /Prev 300 0 R /Next 302 0 R -/A 835 0 R +/A 838 0 R >> endobj 302 0 obj << -/Title ($1N5478) -/Parent 285 0 R +/Title ($1N1786) +/Parent 290 0 R /Prev 301 0 R -/A 837 0 R +/Next 303 0 R +/A 840 0 R +>> +endobj + +303 0 obj +<< +/Title ($1N1806) +/Parent 290 0 R +/Prev 302 0 R +/Next 304 0 R +/A 842 0 R >> endobj 304 0 obj << -/Title ($1N17) -/Parent 303 0 R +/Title ($1N1830) +/Parent 290 0 R +/Prev 303 0 R /Next 305 0 R -/A 839 0 R +/A 844 0 R >> endobj 305 0 obj << -/Title ($1N42) -/Parent 303 0 R +/Title ($1N1878) +/Parent 290 0 R /Prev 304 0 R -/A 841 0 R +/Next 306 0 R +/A 846 0 R +>> +endobj + +306 0 obj +<< +/Title ($1N5358) +/Parent 290 0 R +/Prev 305 0 R +/Next 307 0 R +/A 848 0 R >> endobj 307 0 obj << -/Title ($1N16) -/Parent 306 0 R -/Next 308 0 R -/A 843 0 R +/Title ($1N5478) +/Parent 290 0 R +/Prev 306 0 R +/A 850 0 R >> endobj -308 0 obj +309 0 obj << -/Title ($1N43) -/Parent 306 0 R -/Prev 307 0 R -/A 845 0 R +/Title ($1N17) +/Parent 308 0 R +/Next 310 0 R +/A 852 0 R >> endobj 310 0 obj << -/Title ($1N1498) -/Parent 309 0 R -/Next 311 0 R -/A 847 0 R +/Title ($1N42) +/Parent 308 0 R +/Prev 309 0 R +/A 854 0 R >> endobj -311 0 obj +312 0 obj << -/Title ($1N1512) -/Parent 309 0 R -/Prev 310 0 R -/A 849 0 R +/Title ($1N16) +/Parent 311 0 R +/Next 313 0 R +/A 856 0 R >> endobj 313 0 obj << -/Title ($1N1496) -/Parent 312 0 R -/Next 314 0 R -/A 851 0 R +/Title ($1N43) +/Parent 311 0 R +/Prev 312 0 R +/A 858 0 R >> endobj -314 0 obj +315 0 obj << -/Title ($1N1510) -/Parent 312 0 R -/Prev 313 0 R -/A 853 0 R +/Title ($1N1498) +/Parent 314 0 R +/Next 316 0 R +/A 860 0 R >> endobj 316 0 obj << -/Title ($1N18) -/Parent 315 0 R -/Next 317 0 R -/A 855 0 R ->> -endobj - -317 0 obj -<< -/Title ($1N41) -/Parent 315 0 R -/Prev 316 0 R -/Next 318 0 R -/A 857 0 R +/Title ($1N1512) +/Parent 314 0 R +/Prev 315 0 R +/A 862 0 R >> endobj 318 0 obj << -/Title ($1N1522) -/Parent 315 0 R -/Prev 317 0 R -/A 859 0 R +/Title ($1N1496) +/Parent 317 0 R +/Next 319 0 R +/A 864 0 R >> endobj -861 0 obj +319 0 obj +<< +/Title ($1N1510) +/Parent 317 0 R +/Prev 318 0 R +/A 866 0 R +>> +endobj + +321 0 obj +<< +/Title ($1N18) +/Parent 320 0 R +/Next 322 0 R +/A 868 0 R +>> +endobj + +322 0 obj +<< +/Title ($1N41) +/Parent 320 0 R +/Prev 321 0 R +/Next 323 0 R +/A 870 0 R +>> +endobj + +323 0 obj +<< +/Title ($1N1522) +/Parent 320 0 R +/Prev 322 0 R +/A 872 0 R +>> +endobj + +874 0 obj << /Producer (jsPDF 0.0.0) -/CreationDate (D:20251108000128-00'00') +/CreationDate (D:20251204143819-00'00') >> endobj -862 0 obj +875 0 obj << /Type /Catalog /Pages 1 0 R /OpenAction [3 0 R /FitH null] /PageLayout /OneColumn -/Outlines 12 0 R +/Outlines 13 0 R >> endobj xref -0 863 +0 876 0000000000 65535 f -0000334529 00000 n -0000396345 00000 n +0000339911 00000 n +0000469171 00000 n 0000000015 00000 n 0000000125 00000 n -0000334586 00000 n -0000334751 00000 n -0000334930 00000 n -0000335046 00000 n -0000335215 00000 n -0000336259 00000 n -0000393129 00000 n -0000527015 00000 n -0000527093 00000 n -0000527300 00000 n -0000527196 00000 n -0000527411 00000 n -0000531215 00000 n -0000531292 00000 n -0000531383 00000 n -0000531474 00000 n -0000531565 00000 n -0000531658 00000 n -0000531751 00000 n -0000531844 00000 n -0000531937 00000 n +0000339968 00000 n +0000340133 00000 n +0000340312 00000 n +0000340428 00000 n +0000340597 00000 n +0000341641 00000 n +0000398511 00000 n +0000401727 00000 n +0000601787 00000 n +0000601865 00000 n +0000602072 00000 n +0000601968 00000 n +0000602183 00000 n +0000605987 00000 n +0000606064 00000 n +0000606155 00000 n +0000606246 00000 n +0000606337 00000 n +0000606430 00000 n +0000606523 00000 n +0000606616 00000 n +0000606709 00000 n +0000606802 00000 n +0000606895 00000 n +0000606988 00000 n +0000607081 00000 n +0000607174 00000 n +0000607267 00000 n +0000607360 00000 n +0000607453 00000 n +0000607546 00000 n +0000607639 00000 n +0000602285 00000 n +0000607720 00000 n +0000607798 00000 n +0000607889 00000 n +0000602399 00000 n +0000607969 00000 n +0000602514 00000 n +0000608033 00000 n +0000602630 00000 n +0000608098 00000 n +0000602746 00000 n +0000608163 00000 n +0000608241 00000 n +0000608332 00000 n +0000608425 00000 n +0000602860 00000 n +0000608505 00000 n +0000608585 00000 n +0000608678 00000 n +0000608771 00000 n +0000608864 00000 n +0000608957 00000 n +0000602975 00000 n +0000609037 00000 n +0000609115 00000 n +0000609206 00000 n +0000609299 00000 n +0000609392 00000 n +0000609485 00000 n +0000609578 00000 n +0000609671 00000 n +0000609764 00000 n +0000609857 00000 n +0000609950 00000 n +0000610043 00000 n +0000610136 00000 n +0000610229 00000 n +0000610322 00000 n +0000603091 00000 n +0000610402 00000 n +0000610480 00000 n +0000610571 00000 n +0000610664 00000 n +0000610757 00000 n +0000610850 00000 n +0000610943 00000 n +0000611036 00000 n +0000611129 00000 n +0000611222 00000 n +0000611315 00000 n +0000611408 00000 n +0000611501 00000 n +0000611594 00000 n +0000611687 00000 n +0000603205 00000 n +0000611767 00000 n +0000611847 00000 n +0000611940 00000 n +0000612033 00000 n +0000612126 00000 n +0000612219 00000 n +0000612312 00000 n +0000612405 00000 n +0000612499 00000 n +0000612594 00000 n +0000612690 00000 n +0000612786 00000 n +0000603323 00000 n +0000612868 00000 n +0000603442 00000 n +0000612937 00000 n +0000613020 00000 n +0000613117 00000 n +0000613214 00000 n +0000603568 00000 n +0000613297 00000 n +0000613380 00000 n +0000613477 00000 n +0000603692 00000 n +0000613560 00000 n +0000613643 00000 n +0000613740 00000 n +0000603817 00000 n +0000613823 00000 n +0000613906 00000 n +0000614003 00000 n +0000603943 00000 n +0000614086 00000 n +0000614166 00000 n +0000614260 00000 n +0000614355 00000 n +0000614450 00000 n +0000614545 00000 n +0000614640 00000 n +0000614735 00000 n +0000614830 00000 n +0000614925 00000 n +0000615020 00000 n +0000615115 00000 n +0000615210 00000 n +0000615305 00000 n +0000615400 00000 n +0000615495 00000 n +0000615592 00000 n +0000615689 00000 n +0000615786 00000 n +0000615883 00000 n +0000615980 00000 n +0000616077 00000 n +0000616174 00000 n +0000616271 00000 n +0000616368 00000 n +0000616465 00000 n +0000616562 00000 n +0000616659 00000 n +0000616756 00000 n +0000616853 00000 n +0000616950 00000 n +0000617047 00000 n +0000617144 00000 n +0000617241 00000 n +0000617338 00000 n +0000617435 00000 n +0000617532 00000 n +0000617629 00000 n +0000617726 00000 n +0000617823 00000 n +0000617920 00000 n +0000618017 00000 n +0000618114 00000 n +0000618211 00000 n +0000618308 00000 n +0000618405 00000 n +0000618502 00000 n +0000618599 00000 n +0000618696 00000 n +0000618793 00000 n +0000618890 00000 n +0000618987 00000 n +0000619084 00000 n +0000619181 00000 n +0000619278 00000 n +0000619375 00000 n +0000619472 00000 n +0000619569 00000 n +0000619666 00000 n +0000619763 00000 n +0000619860 00000 n +0000619957 00000 n +0000620054 00000 n +0000620151 00000 n +0000620248 00000 n +0000620345 00000 n +0000620442 00000 n +0000620539 00000 n +0000620636 00000 n +0000620733 00000 n +0000620830 00000 n +0000620927 00000 n +0000621024 00000 n +0000621121 00000 n +0000621218 00000 n +0000604063 00000 n +0000621302 00000 n +0000621383 00000 n +0000604184 00000 n +0000621464 00000 n +0000621545 00000 n +0000621642 00000 n +0000604305 00000 n +0000621726 00000 n +0000621807 00000 n +0000621904 00000 n +0000604426 00000 n +0000621988 00000 n +0000622069 00000 n +0000622164 00000 n +0000622259 00000 n +0000622356 00000 n +0000622453 00000 n +0000622550 00000 n +0000622647 00000 n +0000622744 00000 n +0000622841 00000 n +0000622938 00000 n +0000623035 00000 n +0000623132 00000 n +0000623229 00000 n +0000623326 00000 n +0000604546 00000 n +0000623409 00000 n +0000604670 00000 n +0000623478 00000 n +0000623558 00000 n +0000623653 00000 n +0000623750 00000 n +0000623847 00000 n +0000623944 00000 n +0000624041 00000 n +0000624138 00000 n +0000624235 00000 n +0000624332 00000 n +0000624429 00000 n +0000624526 00000 n +0000624623 00000 n +0000624720 00000 n +0000624817 00000 n +0000604791 00000 n +0000624900 00000 n +0000624980 00000 n +0000625075 00000 n +0000625172 00000 n +0000625269 00000 n +0000625366 00000 n +0000625463 00000 n +0000625560 00000 n +0000625657 00000 n +0000625754 00000 n +0000625851 00000 n +0000625948 00000 n +0000626045 00000 n +0000626142 00000 n +0000626239 00000 n +0000626336 00000 n +0000626433 00000 n +0000604912 00000 n +0000626516 00000 n +0000626597 00000 n +0000626692 00000 n +0000626787 00000 n +0000626884 00000 n +0000626981 00000 n +0000627078 00000 n +0000627175 00000 n +0000627272 00000 n +0000627369 00000 n +0000627466 00000 n +0000627563 00000 n +0000627660 00000 n +0000627757 00000 n +0000627854 00000 n +0000605033 00000 n +0000627937 00000 n +0000628018 00000 n +0000628113 00000 n +0000605153 00000 n +0000628196 00000 n +0000628276 00000 n +0000628371 00000 n +0000628466 00000 n +0000628563 00000 n +0000628660 00000 n +0000628757 00000 n +0000605273 00000 n +0000628840 00000 n +0000628920 00000 n +0000629015 00000 n +0000629112 00000 n +0000629209 00000 n +0000629306 00000 n +0000629403 00000 n +0000629500 00000 n +0000629597 00000 n +0000629694 00000 n +0000629791 00000 n +0000629888 00000 n +0000629985 00000 n +0000630082 00000 n +0000630179 00000 n +0000630276 00000 n +0000630373 00000 n +0000605393 00000 n +0000630456 00000 n +0000630537 00000 n +0000605512 00000 n +0000630618 00000 n +0000630699 00000 n +0000605631 00000 n +0000630780 00000 n +0000630863 00000 n +0000605756 00000 n +0000630946 00000 n +0000631029 00000 n +0000605881 00000 n +0000631112 00000 n +0000631193 00000 n +0000631288 00000 n +0000469307 00000 n +0000469371 00000 n +0000469817 00000 n +0000469881 00000 n +0000470275 00000 n +0000470339 00000 n +0000470748 00000 n +0000470812 00000 n +0000471245 00000 n +0000471309 00000 n +0000471715 00000 n +0000471779 00000 n +0000472172 00000 n +0000472236 00000 n +0000472668 00000 n +0000472732 00000 n +0000473125 00000 n +0000473189 00000 n +0000473633 00000 n +0000473697 00000 n +0000474141 00000 n +0000474205 00000 n +0000474634 00000 n +0000474698 00000 n +0000475105 00000 n +0000475169 00000 n +0000475564 00000 n +0000475628 00000 n +0000476035 00000 n +0000476099 00000 n +0000476503 00000 n +0000476567 00000 n +0000477007 00000 n +0000477071 00000 n +0000477529 00000 n +0000477593 00000 n +0000478051 00000 n +0000478115 00000 n +0000478555 00000 n +0000478619 00000 n +0000479015 00000 n +0000479079 00000 n +0000479497 00000 n +0000479561 00000 n +0000479956 00000 n +0000480020 00000 n +0000480450 00000 n +0000480514 00000 n +0000480958 00000 n +0000481022 00000 n +0000481453 00000 n +0000481517 00000 n +0000481937 00000 n +0000482001 00000 n +0000482422 00000 n +0000482486 00000 n +0000482894 00000 n +0000482958 00000 n +0000483366 00000 n +0000483430 00000 n +0000483828 00000 n +0000483892 00000 n +0000484288 00000 n +0000484352 00000 n +0000484759 00000 n +0000484823 00000 n +0000485258 00000 n +0000485322 00000 n +0000485732 00000 n +0000485796 00000 n +0000486206 00000 n +0000486270 00000 n +0000486679 00000 n +0000486743 00000 n +0000487154 00000 n +0000487218 00000 n +0000487624 00000 n +0000487688 00000 n +0000488083 00000 n +0000488147 00000 n +0000488562 00000 n +0000488626 00000 n +0000489055 00000 n +0000489119 00000 n +0000489524 00000 n +0000489588 00000 n +0000490017 00000 n +0000490081 00000 n +0000490536 00000 n +0000490600 00000 n +0000491029 00000 n +0000491093 00000 n +0000491510 00000 n +0000491574 00000 n +0000491969 00000 n +0000492033 00000 n +0000492440 00000 n +0000492504 00000 n +0000492897 00000 n +0000492961 00000 n +0000493390 00000 n +0000493454 00000 n +0000493897 00000 n +0000493961 00000 n +0000494406 00000 n +0000494470 00000 n +0000494865 00000 n +0000494929 00000 n +0000495346 00000 n +0000495410 00000 n +0000495827 00000 n +0000495891 00000 n +0000496320 00000 n +0000496384 00000 n +0000496789 00000 n +0000496853 00000 n +0000497271 00000 n +0000497335 00000 n +0000497790 00000 n +0000497854 00000 n +0000498249 00000 n +0000498313 00000 n +0000498730 00000 n +0000498794 00000 n +0000499189 00000 n +0000499253 00000 n +0000499660 00000 n +0000499724 00000 n +0000500119 00000 n +0000500183 00000 n +0000500600 00000 n +0000500664 00000 n +0000501093 00000 n +0000501157 00000 n +0000501623 00000 n +0000501687 00000 n +0000502105 00000 n +0000502169 00000 n +0000502613 00000 n +0000502677 00000 n +0000503084 00000 n +0000503148 00000 n +0000503592 00000 n +0000503656 00000 n +0000504051 00000 n +0000504115 00000 n +0000504522 00000 n +0000504586 00000 n +0000504981 00000 n +0000505045 00000 n +0000505463 00000 n +0000505527 00000 n +0000505954 00000 n +0000506018 00000 n +0000506447 00000 n +0000506511 00000 n +0000506906 00000 n +0000506970 00000 n +0000507377 00000 n +0000507441 00000 n +0000507859 00000 n +0000507923 00000 n +0000508318 00000 n +0000508382 00000 n +0000508814 00000 n +0000508878 00000 n +0000509283 00000 n +0000509347 00000 n +0000509742 00000 n +0000509806 00000 n +0000510238 00000 n +0000510302 00000 n +0000510746 00000 n +0000510810 00000 n +0000511242 00000 n +0000511306 00000 n +0000511775 00000 n +0000511839 00000 n +0000512283 00000 n +0000512347 00000 n +0000512740 00000 n +0000512804 00000 n +0000513234 00000 n +0000513298 00000 n +0000513694 00000 n +0000513758 00000 n +0000514181 00000 n +0000514245 00000 n +0000514642 00000 n +0000514706 00000 n +0000515161 00000 n +0000515225 00000 n +0000515622 00000 n +0000515686 00000 n +0000516132 00000 n +0000516196 00000 n +0000516615 00000 n +0000516679 00000 n +0000517111 00000 n +0000517175 00000 n +0000517605 00000 n +0000517669 00000 n +0000518102 00000 n +0000518166 00000 n +0000518574 00000 n +0000518638 00000 n +0000519048 00000 n +0000519112 00000 n +0000519553 00000 n +0000519617 00000 n +0000520020 00000 n +0000520084 00000 n +0000520504 00000 n +0000520568 00000 n +0000521000 00000 n +0000521064 00000 n +0000521533 00000 n +0000521597 00000 n +0000522028 00000 n +0000522092 00000 n +0000522507 00000 n +0000522571 00000 n +0000522964 00000 n +0000523028 00000 n +0000523457 00000 n +0000523521 00000 n +0000523927 00000 n +0000523991 00000 n +0000524408 00000 n +0000524472 00000 n +0000524887 00000 n +0000524951 00000 n +0000525383 00000 n +0000525447 00000 n +0000525842 00000 n +0000525906 00000 n +0000526347 00000 n +0000526411 00000 n +0000526826 00000 n +0000526890 00000 n +0000527307 00000 n +0000527371 00000 n +0000527788 00000 n +0000527852 00000 n +0000528269 00000 n +0000528333 00000 n +0000528728 00000 n +0000528792 00000 n +0000529187 00000 n +0000529251 00000 n +0000529646 00000 n +0000529710 00000 n +0000530103 00000 n +0000530167 00000 n +0000530574 00000 n +0000530638 00000 n +0000531067 00000 n +0000531131 00000 n +0000531548 00000 n +0000531612 00000 n 0000532030 00000 n -0000532123 00000 n -0000532216 00000 n -0000532309 00000 n -0000532402 00000 n -0000532495 00000 n -0000532588 00000 n -0000532681 00000 n -0000532774 00000 n -0000527513 00000 n -0000532854 00000 n -0000532932 00000 n -0000533023 00000 n -0000527627 00000 n -0000533103 00000 n -0000527742 00000 n -0000533167 00000 n -0000527858 00000 n -0000533232 00000 n -0000527974 00000 n -0000533297 00000 n -0000533375 00000 n -0000533466 00000 n -0000533559 00000 n -0000528088 00000 n -0000533639 00000 n -0000533719 00000 n -0000533812 00000 n -0000533905 00000 n -0000533998 00000 n -0000534091 00000 n -0000528203 00000 n -0000534171 00000 n -0000534249 00000 n -0000534340 00000 n -0000534433 00000 n -0000534526 00000 n -0000534619 00000 n -0000534712 00000 n -0000534805 00000 n -0000534898 00000 n -0000534991 00000 n -0000535084 00000 n -0000535177 00000 n -0000535270 00000 n -0000535363 00000 n -0000535456 00000 n -0000528319 00000 n -0000535536 00000 n -0000535614 00000 n -0000535705 00000 n -0000535798 00000 n -0000535891 00000 n -0000535984 00000 n -0000536077 00000 n -0000536170 00000 n -0000536263 00000 n -0000536356 00000 n -0000536449 00000 n -0000536542 00000 n -0000536635 00000 n -0000536728 00000 n -0000536821 00000 n -0000528433 00000 n -0000536901 00000 n -0000536981 00000 n -0000537074 00000 n -0000537167 00000 n -0000537260 00000 n -0000537353 00000 n -0000537446 00000 n -0000537539 00000 n -0000537632 00000 n -0000537725 00000 n -0000537819 00000 n -0000537914 00000 n -0000528551 00000 n -0000537996 00000 n -0000528670 00000 n -0000538065 00000 n -0000538148 00000 n -0000538245 00000 n -0000538342 00000 n -0000528796 00000 n -0000538425 00000 n -0000538508 00000 n -0000538605 00000 n -0000528920 00000 n -0000538688 00000 n -0000538771 00000 n -0000538868 00000 n -0000529045 00000 n -0000538951 00000 n -0000539034 00000 n -0000539131 00000 n -0000529171 00000 n -0000539214 00000 n -0000539294 00000 n -0000539388 00000 n -0000539483 00000 n -0000539578 00000 n -0000539673 00000 n -0000539768 00000 n -0000539863 00000 n -0000539958 00000 n -0000540053 00000 n -0000540148 00000 n -0000540243 00000 n -0000540338 00000 n -0000540433 00000 n -0000540528 00000 n -0000540623 00000 n -0000540720 00000 n -0000540817 00000 n -0000540914 00000 n -0000541011 00000 n -0000541108 00000 n -0000541205 00000 n -0000541302 00000 n -0000541399 00000 n -0000541496 00000 n -0000541593 00000 n -0000541690 00000 n -0000541787 00000 n -0000541884 00000 n -0000541981 00000 n -0000542078 00000 n -0000542175 00000 n -0000542272 00000 n -0000542369 00000 n -0000542466 00000 n -0000542563 00000 n -0000542660 00000 n -0000542757 00000 n -0000542854 00000 n -0000542951 00000 n -0000543048 00000 n -0000543145 00000 n -0000543242 00000 n -0000543339 00000 n -0000543436 00000 n -0000543533 00000 n -0000543630 00000 n -0000543727 00000 n -0000543824 00000 n -0000543921 00000 n -0000544018 00000 n -0000544115 00000 n -0000544212 00000 n -0000544309 00000 n -0000544406 00000 n -0000544503 00000 n -0000544600 00000 n -0000544697 00000 n -0000544794 00000 n -0000544891 00000 n -0000544988 00000 n -0000545085 00000 n -0000545182 00000 n -0000545279 00000 n -0000545376 00000 n -0000545473 00000 n -0000545570 00000 n -0000545667 00000 n -0000545764 00000 n -0000545861 00000 n -0000545958 00000 n -0000546055 00000 n -0000546152 00000 n -0000546249 00000 n -0000529291 00000 n -0000546332 00000 n -0000546413 00000 n -0000529412 00000 n -0000546494 00000 n -0000546575 00000 n -0000529533 00000 n -0000546656 00000 n -0000546737 00000 n -0000529654 00000 n -0000546818 00000 n -0000546899 00000 n -0000546994 00000 n -0000547089 00000 n -0000547186 00000 n -0000547283 00000 n -0000547380 00000 n -0000547477 00000 n -0000547574 00000 n -0000547671 00000 n -0000547768 00000 n -0000547865 00000 n -0000547962 00000 n -0000548059 00000 n -0000548156 00000 n -0000529774 00000 n -0000548239 00000 n -0000529898 00000 n -0000548308 00000 n -0000548388 00000 n -0000548483 00000 n -0000548580 00000 n -0000548677 00000 n -0000548774 00000 n -0000548871 00000 n -0000548968 00000 n -0000549065 00000 n -0000549162 00000 n -0000549259 00000 n -0000549356 00000 n -0000549453 00000 n -0000549550 00000 n -0000549647 00000 n -0000530019 00000 n -0000549730 00000 n -0000549810 00000 n -0000549905 00000 n -0000550002 00000 n -0000550099 00000 n -0000550196 00000 n -0000550293 00000 n -0000550390 00000 n -0000550487 00000 n -0000550584 00000 n -0000550681 00000 n -0000550778 00000 n -0000550875 00000 n -0000550972 00000 n -0000551069 00000 n -0000551166 00000 n -0000551263 00000 n -0000530140 00000 n -0000551346 00000 n -0000551427 00000 n -0000551522 00000 n -0000551617 00000 n -0000551714 00000 n -0000551811 00000 n -0000551908 00000 n -0000552005 00000 n -0000552102 00000 n -0000552199 00000 n -0000552296 00000 n -0000552393 00000 n -0000552490 00000 n -0000552587 00000 n -0000552684 00000 n -0000530261 00000 n -0000552767 00000 n -0000552848 00000 n -0000552943 00000 n -0000530381 00000 n -0000553026 00000 n -0000553106 00000 n -0000553201 00000 n -0000553296 00000 n -0000553393 00000 n -0000553490 00000 n -0000553587 00000 n -0000530501 00000 n -0000553670 00000 n -0000553750 00000 n -0000553845 00000 n -0000553942 00000 n -0000554039 00000 n -0000554136 00000 n -0000554233 00000 n -0000554330 00000 n -0000554427 00000 n -0000554524 00000 n -0000554621 00000 n -0000554718 00000 n -0000554815 00000 n -0000554912 00000 n -0000555009 00000 n -0000555106 00000 n -0000555203 00000 n -0000530621 00000 n -0000555286 00000 n -0000555367 00000 n -0000530740 00000 n -0000555448 00000 n -0000555529 00000 n -0000530859 00000 n -0000555610 00000 n -0000555693 00000 n -0000530984 00000 n -0000555776 00000 n -0000555859 00000 n -0000531109 00000 n -0000555942 00000 n -0000556023 00000 n -0000556118 00000 n -0000396470 00000 n -0000396534 00000 n -0000396980 00000 n -0000397044 00000 n -0000397438 00000 n -0000397502 00000 n -0000397911 00000 n -0000397975 00000 n -0000398408 00000 n -0000398472 00000 n -0000398878 00000 n -0000398942 00000 n -0000399335 00000 n -0000399399 00000 n -0000399831 00000 n -0000399895 00000 n -0000400288 00000 n -0000400352 00000 n -0000400796 00000 n -0000400860 00000 n -0000401304 00000 n -0000401368 00000 n -0000401797 00000 n -0000401861 00000 n -0000402268 00000 n -0000402332 00000 n -0000402727 00000 n -0000402791 00000 n -0000403198 00000 n -0000403262 00000 n -0000403666 00000 n -0000403730 00000 n -0000404170 00000 n -0000404234 00000 n -0000404692 00000 n -0000404756 00000 n -0000405214 00000 n -0000405278 00000 n -0000405674 00000 n -0000405738 00000 n -0000406156 00000 n -0000406220 00000 n -0000406615 00000 n -0000406679 00000 n -0000407109 00000 n -0000407173 00000 n -0000407617 00000 n -0000407681 00000 n -0000408112 00000 n -0000408176 00000 n -0000408596 00000 n -0000408660 00000 n -0000409081 00000 n -0000409145 00000 n -0000409553 00000 n -0000409617 00000 n -0000410025 00000 n -0000410089 00000 n -0000410487 00000 n -0000410551 00000 n -0000410947 00000 n -0000411011 00000 n -0000411418 00000 n -0000411482 00000 n -0000411917 00000 n -0000411981 00000 n -0000412391 00000 n -0000412455 00000 n -0000412865 00000 n -0000412929 00000 n -0000413338 00000 n -0000413402 00000 n -0000413813 00000 n -0000413877 00000 n -0000414283 00000 n -0000414347 00000 n -0000414742 00000 n -0000414806 00000 n -0000415221 00000 n -0000415285 00000 n -0000415714 00000 n -0000415778 00000 n -0000416183 00000 n -0000416247 00000 n -0000416676 00000 n -0000416740 00000 n -0000417195 00000 n -0000417259 00000 n -0000417688 00000 n -0000417752 00000 n -0000418169 00000 n -0000418233 00000 n -0000418628 00000 n -0000418692 00000 n -0000419099 00000 n -0000419163 00000 n -0000419556 00000 n -0000419620 00000 n -0000420049 00000 n -0000420113 00000 n -0000420556 00000 n -0000420620 00000 n -0000421065 00000 n -0000421129 00000 n -0000421524 00000 n -0000421588 00000 n -0000422005 00000 n -0000422069 00000 n -0000422486 00000 n -0000422550 00000 n -0000422979 00000 n -0000423043 00000 n -0000423448 00000 n -0000423512 00000 n -0000423930 00000 n -0000423994 00000 n -0000424449 00000 n -0000424513 00000 n -0000424908 00000 n -0000424972 00000 n -0000425389 00000 n -0000425453 00000 n -0000425848 00000 n -0000425912 00000 n -0000426319 00000 n -0000426383 00000 n -0000426778 00000 n -0000426842 00000 n -0000427259 00000 n -0000427323 00000 n -0000427752 00000 n -0000427816 00000 n -0000428282 00000 n -0000428346 00000 n -0000428764 00000 n -0000428828 00000 n -0000429272 00000 n -0000429336 00000 n -0000429743 00000 n -0000429807 00000 n -0000430251 00000 n -0000430315 00000 n -0000430710 00000 n -0000430774 00000 n -0000431181 00000 n -0000431245 00000 n -0000431640 00000 n -0000431704 00000 n -0000432122 00000 n -0000432186 00000 n -0000432613 00000 n -0000432677 00000 n -0000433106 00000 n -0000433170 00000 n -0000433565 00000 n -0000433629 00000 n -0000434036 00000 n -0000434100 00000 n -0000434518 00000 n -0000434582 00000 n -0000434977 00000 n -0000435041 00000 n -0000435473 00000 n -0000435537 00000 n -0000435942 00000 n -0000436006 00000 n -0000436401 00000 n -0000436465 00000 n -0000436897 00000 n -0000436961 00000 n -0000437405 00000 n -0000437469 00000 n -0000437901 00000 n -0000437965 00000 n -0000438434 00000 n -0000438498 00000 n -0000438942 00000 n -0000439006 00000 n -0000439399 00000 n -0000439463 00000 n -0000439893 00000 n -0000439957 00000 n -0000440353 00000 n -0000440417 00000 n -0000440840 00000 n -0000440904 00000 n -0000441301 00000 n -0000441365 00000 n -0000441820 00000 n -0000441884 00000 n -0000442281 00000 n -0000442345 00000 n -0000442791 00000 n -0000442855 00000 n -0000443274 00000 n -0000443338 00000 n -0000443770 00000 n -0000443834 00000 n -0000444264 00000 n -0000444328 00000 n -0000444761 00000 n -0000444825 00000 n -0000445233 00000 n -0000445297 00000 n -0000445707 00000 n -0000445771 00000 n -0000446212 00000 n -0000446276 00000 n -0000446679 00000 n -0000446743 00000 n -0000447163 00000 n -0000447227 00000 n -0000447659 00000 n -0000447723 00000 n -0000448192 00000 n -0000448256 00000 n -0000448687 00000 n -0000448751 00000 n -0000449166 00000 n -0000449230 00000 n -0000449623 00000 n -0000449687 00000 n -0000450116 00000 n -0000450180 00000 n -0000450586 00000 n -0000450650 00000 n -0000451067 00000 n -0000451131 00000 n -0000451546 00000 n -0000451610 00000 n -0000452042 00000 n -0000452106 00000 n -0000452501 00000 n -0000452565 00000 n -0000453006 00000 n -0000453070 00000 n -0000453485 00000 n -0000453549 00000 n -0000453966 00000 n -0000454030 00000 n -0000454447 00000 n -0000454511 00000 n -0000454928 00000 n -0000454992 00000 n -0000455387 00000 n -0000455451 00000 n -0000455846 00000 n -0000455910 00000 n -0000456305 00000 n -0000456369 00000 n -0000456762 00000 n -0000456826 00000 n -0000457233 00000 n -0000457297 00000 n -0000457726 00000 n -0000457790 00000 n -0000458207 00000 n -0000458271 00000 n -0000458689 00000 n -0000458753 00000 n -0000459193 00000 n -0000459257 00000 n -0000459712 00000 n -0000459776 00000 n -0000460231 00000 n -0000460295 00000 n -0000460724 00000 n -0000460788 00000 n -0000461204 00000 n -0000461268 00000 n -0000461661 00000 n -0000461725 00000 n -0000462130 00000 n -0000462194 00000 n -0000462626 00000 n -0000462690 00000 n -0000463097 00000 n -0000463161 00000 n -0000463568 00000 n -0000463632 00000 n -0000464024 00000 n -0000464088 00000 n -0000464505 00000 n -0000464569 00000 n -0000465009 00000 n -0000465073 00000 n -0000465480 00000 n -0000465544 00000 n -0000465973 00000 n -0000466037 00000 n -0000466432 00000 n -0000466496 00000 n -0000466891 00000 n -0000466955 00000 n -0000467350 00000 n -0000467414 00000 n -0000467809 00000 n -0000467873 00000 n -0000468268 00000 n -0000468332 00000 n -0000468739 00000 n -0000468803 00000 n -0000469198 00000 n -0000469262 00000 n -0000469657 00000 n -0000469721 00000 n -0000470116 00000 n -0000470180 00000 n -0000470575 00000 n -0000470639 00000 n -0000471032 00000 n -0000471096 00000 n -0000471489 00000 n -0000471553 00000 n -0000472022 00000 n -0000472086 00000 n -0000472530 00000 n -0000472594 00000 n -0000473038 00000 n -0000473102 00000 n -0000473553 00000 n -0000473617 00000 n -0000474036 00000 n -0000474100 00000 n -0000474507 00000 n -0000474571 00000 n -0000474964 00000 n -0000475028 00000 n -0000475458 00000 n -0000475522 00000 n -0000475957 00000 n -0000476021 00000 n -0000476452 00000 n -0000476516 00000 n -0000476928 00000 n -0000476992 00000 n -0000477400 00000 n -0000477464 00000 n -0000477887 00000 n -0000477951 00000 n -0000478370 00000 n -0000478434 00000 n -0000478843 00000 n -0000478907 00000 n -0000479340 00000 n -0000479404 00000 n -0000479815 00000 n -0000479879 00000 n -0000480272 00000 n -0000480336 00000 n -0000480731 00000 n -0000480795 00000 n -0000481190 00000 n -0000481254 00000 n -0000481671 00000 n -0000481735 00000 n -0000482128 00000 n -0000482192 00000 n -0000482599 00000 n -0000482663 00000 n -0000483070 00000 n -0000483134 00000 n -0000483541 00000 n -0000483605 00000 n -0000484045 00000 n -0000484109 00000 n -0000484504 00000 n -0000484568 00000 n -0000484975 00000 n -0000485039 00000 n -0000485490 00000 n -0000485554 00000 n -0000485961 00000 n -0000486025 00000 n -0000486445 00000 n -0000486509 00000 n -0000486931 00000 n -0000486995 00000 n -0000487388 00000 n -0000487452 00000 n -0000487906 00000 n -0000487970 00000 n -0000488387 00000 n -0000488451 00000 n -0000488880 00000 n -0000488944 00000 n -0000489349 00000 n -0000489413 00000 n -0000489820 00000 n -0000489884 00000 n -0000490328 00000 n -0000490392 00000 n -0000490799 00000 n -0000490863 00000 n -0000491280 00000 n -0000491344 00000 n -0000491776 00000 n -0000491840 00000 n -0000492247 00000 n -0000492311 00000 n -0000492706 00000 n -0000492770 00000 n -0000493199 00000 n -0000493263 00000 n -0000493695 00000 n -0000493759 00000 n -0000494193 00000 n -0000494257 00000 n -0000494686 00000 n -0000494750 00000 n -0000495165 00000 n -0000495229 00000 n -0000495683 00000 n -0000495747 00000 n -0000496187 00000 n -0000496251 00000 n -0000496667 00000 n -0000496731 00000 n -0000497126 00000 n -0000497190 00000 n -0000497622 00000 n -0000497686 00000 n -0000498115 00000 n -0000498179 00000 n -0000498608 00000 n -0000498672 00000 n -0000499067 00000 n -0000499131 00000 n -0000499526 00000 n -0000499590 00000 n -0000499985 00000 n -0000500049 00000 n -0000500500 00000 n -0000500564 00000 n -0000500959 00000 n -0000501023 00000 n -0000501455 00000 n -0000501519 00000 n -0000501939 00000 n -0000502003 00000 n -0000502424 00000 n -0000502488 00000 n -0000502910 00000 n -0000502974 00000 n -0000503380 00000 n -0000503444 00000 n -0000503876 00000 n -0000503940 00000 n -0000504335 00000 n -0000504399 00000 n -0000504850 00000 n -0000504914 00000 n -0000505341 00000 n -0000505405 00000 n -0000505837 00000 n -0000505901 00000 n -0000506308 00000 n -0000506372 00000 n -0000506790 00000 n -0000506854 00000 n -0000507283 00000 n -0000507347 00000 n -0000507742 00000 n -0000507806 00000 n -0000508201 00000 n -0000508265 00000 n -0000508705 00000 n -0000508769 00000 n -0000509201 00000 n -0000509265 00000 n -0000509699 00000 n -0000509763 00000 n -0000510153 00000 n -0000510217 00000 n -0000510611 00000 n -0000510675 00000 n -0000511082 00000 n -0000511146 00000 n -0000511556 00000 n -0000511620 00000 n -0000512038 00000 n -0000512102 00000 n -0000512557 00000 n -0000512621 00000 n -0000513087 00000 n -0000513151 00000 n -0000513558 00000 n -0000513622 00000 n -0000514029 00000 n -0000514093 00000 n -0000514502 00000 n -0000514566 00000 n -0000514972 00000 n -0000515036 00000 n -0000515453 00000 n -0000515517 00000 n -0000515934 00000 n -0000515998 00000 n -0000516415 00000 n -0000516479 00000 n -0000516872 00000 n -0000516936 00000 n -0000517343 00000 n -0000517407 00000 n -0000517873 00000 n -0000517937 00000 n -0000518344 00000 n -0000518408 00000 n -0000518848 00000 n -0000518912 00000 n -0000519305 00000 n -0000519369 00000 n -0000519787 00000 n -0000519851 00000 n -0000520283 00000 n -0000520347 00000 n -0000520776 00000 n -0000520840 00000 n -0000521235 00000 n -0000521299 00000 n -0000521731 00000 n -0000521795 00000 n -0000522205 00000 n -0000522269 00000 n -0000522675 00000 n -0000522739 00000 n -0000523151 00000 n -0000523215 00000 n -0000523623 00000 n -0000523687 00000 n -0000524083 00000 n -0000524147 00000 n -0000524554 00000 n -0000524618 00000 n -0000525025 00000 n -0000525089 00000 n -0000525507 00000 n -0000525571 00000 n -0000526017 00000 n -0000526081 00000 n -0000526523 00000 n -0000526587 00000 n -0000556201 00000 n -0000556288 00000 n +0000532094 00000 n +0000532534 00000 n +0000532598 00000 n +0000533053 00000 n +0000533117 00000 n +0000533572 00000 n +0000533636 00000 n +0000534065 00000 n +0000534129 00000 n +0000534545 00000 n +0000534609 00000 n +0000535002 00000 n +0000535066 00000 n +0000535471 00000 n +0000535535 00000 n +0000535967 00000 n +0000536031 00000 n +0000536438 00000 n +0000536502 00000 n +0000536909 00000 n +0000536973 00000 n +0000537365 00000 n +0000537429 00000 n +0000537846 00000 n +0000537910 00000 n +0000538350 00000 n +0000538414 00000 n +0000538821 00000 n +0000538885 00000 n +0000539314 00000 n +0000539378 00000 n +0000539773 00000 n +0000539837 00000 n +0000540232 00000 n +0000540296 00000 n +0000540691 00000 n +0000540755 00000 n +0000541150 00000 n +0000541214 00000 n +0000541609 00000 n +0000541673 00000 n +0000542080 00000 n +0000542144 00000 n +0000542539 00000 n +0000542603 00000 n +0000542998 00000 n +0000543062 00000 n +0000543457 00000 n +0000543521 00000 n +0000543916 00000 n +0000543980 00000 n +0000544373 00000 n +0000544437 00000 n +0000544830 00000 n +0000544894 00000 n +0000545363 00000 n +0000545427 00000 n +0000545871 00000 n +0000545935 00000 n +0000546379 00000 n +0000546443 00000 n +0000546894 00000 n +0000546958 00000 n +0000547377 00000 n +0000547441 00000 n +0000547848 00000 n +0000547912 00000 n +0000548305 00000 n +0000548369 00000 n +0000548799 00000 n +0000548863 00000 n +0000549306 00000 n +0000549370 00000 n +0000549805 00000 n +0000549869 00000 n +0000550300 00000 n +0000550364 00000 n +0000550787 00000 n +0000550851 00000 n +0000551270 00000 n +0000551334 00000 n +0000551726 00000 n +0000551790 00000 n +0000552202 00000 n +0000552266 00000 n +0000552674 00000 n +0000552738 00000 n +0000553142 00000 n +0000553206 00000 n +0000553615 00000 n +0000553679 00000 n +0000554112 00000 n +0000554176 00000 n +0000554587 00000 n +0000554651 00000 n +0000555044 00000 n +0000555108 00000 n +0000555503 00000 n +0000555567 00000 n +0000555962 00000 n +0000556026 00000 n +0000556443 00000 n +0000556507 00000 n +0000556900 00000 n +0000556964 00000 n +0000557371 00000 n +0000557435 00000 n +0000557842 00000 n +0000557906 00000 n +0000558313 00000 n +0000558377 00000 n +0000558817 00000 n +0000558881 00000 n +0000559276 00000 n +0000559340 00000 n +0000559747 00000 n +0000559811 00000 n +0000560262 00000 n +0000560326 00000 n +0000560733 00000 n +0000560797 00000 n +0000561217 00000 n +0000561281 00000 n +0000561703 00000 n +0000561767 00000 n +0000562160 00000 n +0000562224 00000 n +0000562678 00000 n +0000562742 00000 n +0000563159 00000 n +0000563223 00000 n +0000563652 00000 n +0000563716 00000 n +0000564121 00000 n +0000564185 00000 n +0000564592 00000 n +0000564656 00000 n +0000565100 00000 n +0000565164 00000 n +0000565571 00000 n +0000565635 00000 n +0000566052 00000 n +0000566116 00000 n +0000566548 00000 n +0000566612 00000 n +0000567019 00000 n +0000567083 00000 n +0000567478 00000 n +0000567542 00000 n +0000567971 00000 n +0000568035 00000 n +0000568467 00000 n +0000568531 00000 n +0000568965 00000 n +0000569029 00000 n +0000569458 00000 n +0000569522 00000 n +0000569937 00000 n +0000570001 00000 n +0000570455 00000 n +0000570519 00000 n +0000570959 00000 n +0000571023 00000 n +0000571439 00000 n +0000571503 00000 n +0000571898 00000 n +0000571962 00000 n +0000572394 00000 n +0000572458 00000 n +0000572887 00000 n +0000572951 00000 n +0000573380 00000 n +0000573444 00000 n +0000573839 00000 n +0000573903 00000 n +0000574298 00000 n +0000574362 00000 n +0000574757 00000 n +0000574821 00000 n +0000575272 00000 n +0000575336 00000 n +0000575731 00000 n +0000575795 00000 n +0000576227 00000 n +0000576291 00000 n +0000576711 00000 n +0000576775 00000 n +0000577196 00000 n +0000577260 00000 n +0000577682 00000 n +0000577746 00000 n +0000578152 00000 n +0000578216 00000 n +0000578648 00000 n +0000578712 00000 n +0000579107 00000 n +0000579171 00000 n +0000579622 00000 n +0000579686 00000 n +0000580113 00000 n +0000580177 00000 n +0000580609 00000 n +0000580673 00000 n +0000581080 00000 n +0000581144 00000 n +0000581562 00000 n +0000581626 00000 n +0000582055 00000 n +0000582119 00000 n +0000582514 00000 n +0000582578 00000 n +0000582973 00000 n +0000583037 00000 n +0000583477 00000 n +0000583541 00000 n +0000583973 00000 n +0000584037 00000 n +0000584471 00000 n +0000584535 00000 n +0000584925 00000 n +0000584989 00000 n +0000585383 00000 n +0000585447 00000 n +0000585854 00000 n +0000585918 00000 n +0000586328 00000 n +0000586392 00000 n +0000586810 00000 n +0000586874 00000 n +0000587329 00000 n +0000587393 00000 n +0000587859 00000 n +0000587923 00000 n +0000588330 00000 n +0000588394 00000 n +0000588801 00000 n +0000588865 00000 n +0000589274 00000 n +0000589338 00000 n +0000589744 00000 n +0000589808 00000 n +0000590225 00000 n +0000590289 00000 n +0000590706 00000 n +0000590770 00000 n +0000591187 00000 n +0000591251 00000 n +0000591644 00000 n +0000591708 00000 n +0000592115 00000 n +0000592179 00000 n +0000592645 00000 n +0000592709 00000 n +0000593116 00000 n +0000593180 00000 n +0000593620 00000 n +0000593684 00000 n +0000594077 00000 n +0000594141 00000 n +0000594559 00000 n +0000594623 00000 n +0000595055 00000 n +0000595119 00000 n +0000595548 00000 n +0000595612 00000 n +0000596007 00000 n +0000596071 00000 n +0000596503 00000 n +0000596567 00000 n +0000596977 00000 n +0000597041 00000 n +0000597447 00000 n +0000597511 00000 n +0000597923 00000 n +0000597987 00000 n +0000598395 00000 n +0000598459 00000 n +0000598855 00000 n +0000598919 00000 n +0000599326 00000 n +0000599390 00000 n +0000599797 00000 n +0000599861 00000 n +0000600279 00000 n +0000600343 00000 n +0000600789 00000 n +0000600853 00000 n +0000601295 00000 n +0000601359 00000 n +0000631371 00000 n +0000631458 00000 n trailer << -/Size 863 -/Root 862 0 R -/Info 861 0 R -/ID [ ] +/Size 876 +/Root 875 0 R +/Info 874 0 R +/ID [ ] >> startxref -556410 +631580 %%EOF diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini index 61a6eda07..006f5c875 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini @@ -1,13 +1,21 @@ ; Promicro + E22(0)-xxxM / HT-RA62 modules board variant - DIY - with TCXO [env:nrf52_promicro_diy_tcxo] +custom_meshtastic_hw_model = 63 +custom_meshtastic_hw_model_slug = NRF52_PROMICRO_DIY +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = NRF52 Pro-micro DIY +custom_meshtastic_images = promicro.svg +custom_meshtastic_tags = DIY +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = promicro-nrf52840 build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D NRF52_PROMICRO_DIY build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink ; NRF52 ProMicro w/ E-Ink display diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md index de76286b2..4ffe625cc 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/readme.md @@ -2,9 +2,11 @@ # Notes +News 2025-12-04 - The GPS pin definitions have been changed!!! This has no material effect on current builds, but future builders may wish to review how they are using the wires. + ## General -The pinout is contained in the variant.h file, and a [generic schematic](./Schematic_Pro-Micro_Pinouts.pdf) is located in this directory. +The pinout is contained in the variant.h file, and a [generic schematic](./Schematic_Pro-Micro_Pinouts_2025-12-04.pdf) is located in this directory. This variant is suitable for both TCXO and XTAL types of modules. The old XTAL variant has been removed to reduce confusion. diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h index fee8ee88e..63af1fe79 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/variant.h @@ -30,8 +30,8 @@ NRF52 PRO MICRO PIN ASSIGNMENT | Gnd   |             |   | reset   |             | | | Gnd   |             |   | ext_vcc | *see 0.13   | | | P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | -| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | -| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | +| P0.20 | GPS_TX     |   | P0.29   | BUSY         | DIO0 | +| P0.22 | GPS_RX     |   | P0.02   | MISO | MISO | | P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | | P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | | P0.11 | SCL         |   | P1.11   | SCK         | SCK | @@ -90,16 +90,16 @@ NRF52 PRO MICRO PIN ASSIGNMENT #define BUTTON_PIN (32 + 0) // P1.00 // GPS -#define PIN_GPS_TX (0 + 22) // P0.22 -#define PIN_GPS_RX (0 + 20) // P0.20 +#define GPS_TX_PIN (0 + 20) // P0.20 - This is data from the MCU +#define GPS_RX_PIN (0 + 22) // P0.22 - This is data from the GNSS #define PIN_GPS_EN (0 + 24) // P0.24 #define GPS_UBLOX // define GPS_DEBUG // UART interfaces -#define PIN_SERIAL1_RX PIN_GPS_TX -#define PIN_SERIAL1_TX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL2_RX (0 + 6) // P0.06 #define PIN_SERIAL2_TX (0 + 8) // P0.08 diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini index 2df31d23c..10eab2aa4 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini @@ -10,6 +10,4 @@ build_flags = ${nrf52840_base.build_flags} -Isrc/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/feather_diy/platformio.ini b/variants/nrf52840/feather_diy/platformio.ini index a17e418a2..5540021b3 100644 --- a/variants/nrf52840/feather_diy/platformio.ini +++ b/variants/nrf52840/feather_diy/platformio.ini @@ -6,8 +6,6 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/feather_diy -Dfeather_diy build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/feather_diy> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink \ No newline at end of file diff --git a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini index c6cd23314..e7eede80f 100644 --- a/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini +++ b/variants/nrf52840/gat562_mesh_trial_tracker/platformio.ini @@ -12,5 +12,3 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/gat562_mesh_trial_tracker> -lib_deps = - ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini index 2641a507d..fbdc999a3 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/platformio.ini @@ -14,7 +14,6 @@ build_src_filter = lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 extra_scripts = ${env.extra_scripts} variants/nrf52840/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays diff --git a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h index 143d20459..14170d5f3 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/variant.h @@ -124,9 +124,6 @@ No longer populated on PCB #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - /* * SPI Interfaces */ @@ -163,7 +160,6 @@ No longer populated on PCB #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (4.90F) -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index c49dadd56..a39872205 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -1,5 +1,14 @@ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-node-t114] +custom_meshtastic_hw_model = 69 +custom_meshtastic_hw_model_slug = HELTEC_MESH_NODE_T114 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec Mesh Node T114 +custom_meshtastic_images = heltec-mesh-node-t114.svg, heltec-mesh-node-t114-case.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_node_t114 board_level = pr @@ -13,5 +22,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip \ No newline at end of file + # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main + https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index 3493577bc..bad488b35 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -175,9 +175,6 @@ No longer populated on PCB #define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_SERIAL1_TX GPS_TX_PIN -// PCF8563 RTC Module -#define PCF8563_RTC 0x51 - /* * SPI Interfaces */ @@ -211,7 +208,8 @@ No longer populated on PCB #define ADC_MULTIPLIER (4.916F) // rf52840 AIN2 = Pin 4 -#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_2 +// commented out due to power leakage of 2.9mA in shutdown state see reported issue #8801 +// #define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_2 //UNSAFE // We have AIN2 with a VBAT divider so AIN2 = VBAT * (100/490) // We have the device going deep sleep under 3.1V, which is AIN2 = 0.63V diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 2fb852226..4dc8b78e7 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -1,8 +1,20 @@ ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-5000] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_mesh_pocket.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink +custom_device_hw_model = 94 +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 5000mAh +custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -25,13 +37,19 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d - + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 5000mAh InkHUD +custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} @@ -46,9 +64,20 @@ lib_deps = ; First prototype nrf52840/sx1262 device [env:heltec-mesh-pocket-10000] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = heltec_mesh_pocket.svg +custom_meshtastic_tags = Heltec + extends = nrf52840_base board = heltec_mesh_pocket debug_tool = jlink +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 10000mAh +custom_meshtastic_key = heltec_mesh_pocket # add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. build_flags = ${nrf52840_base.build_flags} @@ -71,13 +100,19 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d - + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket +custom_meshtastic_hw_model = 94 +custom_meshtastic_hw_model_slug = HELTEC_MESH_POCKET +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Heltec Mesh Pocket +custom_meshtastic_actively_supported = true +custom_meshtastic_variant = 10000mAh InkHUD +custom_meshtastic_key = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_pocket> ${inkhud.build_src_filter} build_flags = ${inkhud.build_flags} diff --git a/variants/nrf52840/heltec_mesh_pocket/variant.h b/variants/nrf52840/heltec_mesh_pocket/variant.h index e765dab66..7ec9b88ea 100644 --- a/variants/nrf52840/heltec_mesh_pocket/variant.h +++ b/variants/nrf52840/heltec_mesh_pocket/variant.h @@ -122,9 +122,14 @@ No longer populated on PCB #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (4.6425F) +#if defined(HELTEC_MESH_POCKET_BATTERY_5000) +#define OCV_ARRAY 4300, 4240, 4120, 4000, 3888, 3800, 3740, 3698, 3655, 3580, 3400 +#elif defined(HELTEC_MESH_POCKET_BATTERY_10000) +#define OCV_ARRAY 4100, 4060, 3960, 3840, 3729, 3625, 3550, 3500, 3420, 3345, 3100 +#endif + #undef HAS_GPS #define HAS_GPS 0 -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 36a7904d6..2ad699544 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -13,10 +13,21 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar> lib_deps = ${nrf52840_base.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 - lewisxhe/PCF8563_Library@^1.0.1 - ArduinoJson@6.21.4 + # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson + bblanchon/ArduinoJson@6.21.4 + [env:heltec-mesh-solar] +custom_meshtastic_hw_model = 108 +custom_meshtastic_hw_model_slug = HELTEC_MESH_SOLAR +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Heltec MeshSolar +custom_meshtastic_images = heltec-mesh-solar.svg +custom_meshtastic_tags = Heltec + extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} -DSPI_INTERFACES_COUNT=1 @@ -46,8 +57,17 @@ build_flags = ${heltec_mesh_solar_base.build_flags} -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -DENABLE_MESSAGE_PERSISTENCE=0 ; Disable flash persistence for space-limited build + -DMESHTASTIC_EXCLUDE_WIFI=1 + -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 + -DMESHTASTIC_EXCLUDE_STOREFORWARD=1 + -DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -DMESHTASTIC_EXCLUDE_WAYPOINT=1 lib_deps = ${heltec_mesh_solar_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip [env:heltec-mesh-solar-inkhud] @@ -71,7 +91,6 @@ lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${heltec_mesh_solar_base.lib_deps} - [env:heltec-mesh-solar-oled] extends = heltec_mesh_solar_base build_flags = ${heltec_mesh_solar_base.build_flags} @@ -112,4 +131,5 @@ build_flags = ${heltec_mesh_solar_base.build_flags} -DPIN_SPI1_SCK=ST7789_SCK 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/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h index 7a8fc579f..112bcd8b3 100644 --- a/variants/nrf52840/heltec_mesh_solar/variant.h +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -142,7 +142,6 @@ No longer populated on PCB #define BQ4050_SCL_PIN (32 + 0) // I2C clock line pin #define BQ4050_EMERGENCY_SHUTDOWN_PIN (32 + 3) // Emergency shutdown pin -#define HAS_RTC 0 #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index 2a4e27fe8..26a999fbb 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -9,6 +9,28 @@ board_level = extra build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/meshlink -D MESHLINK + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> +debug_tool = jlink +; 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 + +[env:meshlink_eink] +extends = nrf52840_base +board = meshlink +board_level = extra +;board_check = true +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/meshlink + -D MESHLINK + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 + -D USE_EINK -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 -D EINK_WIDTH=250 -D EINK_HEIGHT=122 @@ -24,6 +46,7 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/meshlink/variant.h b/variants/nrf52840/meshlink/variant.h index 54df03691..d1dba574f 100644 --- a/variants/nrf52840/meshlink/variant.h +++ b/variants/nrf52840/meshlink/variant.h @@ -121,8 +121,8 @@ static const uint8_t SCK = PIN_SPI_SCK; #define PIN_GPS_PPS (26) // Pulse per second input from the GPS -#define GPS_TX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the CPU -#define GPS_RX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the GPS +#define GPS_TX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the GPS // #define GPS_THREAD_INTERVAL 50 diff --git a/variants/nrf52840/meshlink_eink/platformio.ini b/variants/nrf52840/meshlink_eink/platformio.ini deleted file mode 100644 index c0c0cb1dd..000000000 --- a/variants/nrf52840/meshlink_eink/platformio.ini +++ /dev/null @@ -1,31 +0,0 @@ -; MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog -; https://www.loraitalia.it -; firmware for boards with a 250x122 e-ink display -[env:meshlink_eink] -extends = nrf52840_base -board = meshlink -board_level = extra -;board_check = true -build_flags = ${nrf52840_base.build_flags} - -I variants/nrf52840/meshlink_eink - -D MESHLINK - -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 - -D EINK_WIDTH=250 - -D EINK_HEIGHT=122 - -D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk - -D EINK_LIMIT_FASTREFRESH=5 ; How many consecutive fast-refreshes are permitted - -D EINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates - -D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates - -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated - -D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. - -D EINK_HASQUIRK_VICIOUSFASTREFRESH ; Identify that pixels drawn by fast-refresh are harder to clear - - -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlink_eink> -lib_deps = - ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip -debug_tool = jlink -; 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 \ No newline at end of file diff --git a/variants/nrf52840/meshlink_eink/variant.cpp b/variants/nrf52840/meshlink_eink/variant.cpp deleted file mode 100644 index 81a5097c4..000000000 --- a/variants/nrf52840/meshlink_eink/variant.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // P0 - 0, 1, 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() -{ - pinMode(PIN_LED1, OUTPUT); - digitalWrite(PIN_LED1, HIGH); // turn off the white led while booting - // otherwise it will stay lit for several seconds (could be annoying) - -#ifdef PIN_WD_EN - pinMode(PIN_WD_EN, OUTPUT); - digitalWrite(PIN_WD_EN, HIGH); // Enable the Watchdog at boot -#endif -} \ No newline at end of file diff --git a/variants/nrf52840/meshlink_eink/variant.h b/variants/nrf52840/meshlink_eink/variant.h deleted file mode 100644 index b605d7082..000000000 --- a/variants/nrf52840/meshlink_eink/variant.h +++ /dev/null @@ -1,153 +0,0 @@ -#ifndef _VARIANT_MESHLINK_ -#define _VARIANT_MESHLINK_ -#ifndef MESHLINK -#define MESHLINK -#endif -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -// #define USE_LFXO // Board uses 32khz crystal for LF -#define USE_LFRC // Board uses RC for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -// Number of pins defined in PinDescription array -#define PINS_COUNT (48) -#define NUM_DIGITAL_PINS (48) -#define NUM_ANALOG_INPUTS (2) -#define NUM_ANALOG_OUTPUTS (0) - -#define BUTTON_PIN (-1) // If defined, this will be used for user button presses, -#define BUTTON_NEED_PULLUP - -// LEDs -#define PIN_LED1 (24) // Built in white led for status -#define LED_BLUE PIN_LED1 -#define LED_BUILTIN PIN_LED1 - -#define LED_STATE_ON 0 // State when LED is litted -#define LED_INVERTED 1 - -// Testing USB detection -// #define NRF_APM - -/* - * Analog pins - */ -#define PIN_A1 (3) // P0.03/AIN1 -#define ADC_RESOLUTION 14 - -// Other pins -// #define PIN_AREF (2) -// static const uint8_t AREF = PIN_AREF; - -/* - * Serial interfaces - */ -#define PIN_SERIAL1_RX (32 + 8) -#define PIN_SERIAL1_TX (7) - -/* - * SPI Interfaces - */ -#define SPI_INTERFACES_COUNT 2 - -#define PIN_SPI_MISO (8) -#define PIN_SPI_MOSI (32 + 9) -#define PIN_SPI_SCK (11) - -#define PIN_SPI1_MISO (23) -#define PIN_SPI1_MOSI (21) -#define PIN_SPI1_SCK (19) - -static const uint8_t SS = 12; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -/* - * eink display pins - */ -#define USE_EINK - -#define PIN_EINK_CS (15) -#define PIN_EINK_BUSY (16) -#define PIN_EINK_DC (14) -#define PIN_EINK_RES (17) -#define PIN_EINK_SCLK (19) -#define PIN_EINK_MOSI (21) // also called SDI - -/* - * Wire Interfaces - */ -#define WIRE_INTERFACES_COUNT 1 - -#define PIN_WIRE_SDA (1) -#define PIN_WIRE_SCL (27) - -// QSPI Pins -#define PIN_QSPI_SCK 19 -#define PIN_QSPI_CS 22 -#define PIN_QSPI_IO0 21 -#define PIN_QSPI_IO1 23 -#define PIN_QSPI_IO2 32 -#define PIN_QSPI_IO3 20 - -// On-board QSPI Flash -#define EXTERNAL_FLASH_DEVICES W25Q16JVUXIQ -#define EXTERNAL_FLASH_USE_QSPI - -#define USE_SX1262 -#define SX126X_CS (12) -#define SX126X_DIO1 (32 + 1) -#define SX126X_BUSY (32 + 3) -#define SX126X_RESET (6) -// #define SX126X_RXEN (13) -// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 - -// pin 25 is used to enable or disable the watchdog. This pin has to be disabled when cpu is put to sleep -// otherwise the timer will expire and wd will reboot the cpu -#define PIN_WD_EN (25) - -#define PIN_GPS_PPS (26) // Pulse per second input from the GPS - -#define GPS_TX_PIN PIN_SERIAL1_RX // This is for bits going TOWARDS the CPU -#define GPS_RX_PIN PIN_SERIAL1_TX // This is for bits going TOWARDS the GPS - -// #define GPS_THREAD_INTERVAL 50 - -// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press -#define PIN_GPS_EN (0) -#define GPS_EN_ACTIVE LOW - -#define PIN_BUZZER (31) // P0.31/AIN7 - -// Battery -// The battery sense is hooked to pin A0 (2) -#define BATTERY_PIN (2) -// and has 12 bit resolution -#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 1.42 // fine tuning of voltage - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file diff --git a/variants/nrf52840/meshtiny/platformio.ini b/variants/nrf52840/meshtiny/platformio.ini index 5f03f5cb2..7e1e2e510 100644 --- a/variants/nrf52840/meshtiny/platformio.ini +++ b/variants/nrf52840/meshtiny/platformio.ini @@ -7,5 +7,3 @@ build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHT -D USE_PIN_BUZZER -D MESHTASTIC_EXCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny> -lib_deps = - ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h index 55aabe930..8d634ba60 100644 --- a/variants/nrf52840/meshtiny/variant.h +++ b/variants/nrf52840/meshtiny/variant.h @@ -64,6 +64,7 @@ extern "C" { #define INPUTDRIVER_ENCODER_UP 26 #define INPUTDRIVER_ENCODER_DOWN 4 #define INPUTDRIVER_ENCODER_BTN 28 +#define UPDOWN_LONG_PRESS_REPEAT_INTERVAL 150 #define CANNED_MESSAGE_MODULE_ENABLE 1 diff --git a/variants/nrf52840/monteops_hw1/platformio.ini b/variants/nrf52840/monteops_hw1/platformio.ini index 5426aee7f..e783e130e 100644 --- a/variants/nrf52840/monteops_hw1/platformio.ini +++ b/variants/nrf52840/monteops_hw1/platformio.ini @@ -10,6 +10,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/monteop lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/muzi_base/platformio.ini b/variants/nrf52840/muzi_base/platformio.ini index 49393f4e0..52c558ff1 100644 --- a/variants/nrf52840/muzi_base/platformio.ini +++ b/variants/nrf52840/muzi_base/platformio.ini @@ -1,4 +1,13 @@ [env:muzi-base] +custom_meshtastic_hw_model = 93 +custom_meshtastic_hw_model_slug = MUZI_BASE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = muzi BASE +custom_meshtastic_images = muzi_base.svg +custom_meshtastic_tags = muzi + extends = nrf52840_base board = muzi-base build_flags = ${nrf52840_base.build_flags} @@ -10,6 +19,5 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52840_base.build_src_filter} +<../variants/nrf52840/muzi_base> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 - - diff --git a/variants/nrf52840/nano-g2-ultra/platformio.ini b/variants/nrf52840/nano-g2-ultra/platformio.ini index f697a90dd..0748b7e38 100644 --- a/variants/nrf52840/nano-g2-ultra/platformio.ini +++ b/variants/nrf52840/nano-g2-ultra/platformio.ini @@ -1,5 +1,14 @@ ; First prototype eink/nrf52840/sx1262 device [env:nano-g2-ultra] +custom_meshtastic_hw_model = 18 +custom_meshtastic_hw_model_slug = NANO_G2_ULTRA +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = Nano G2 Ultra +custom_meshtastic_images = nano-g2-ultra.svg +custom_meshtastic_tags = B&Q + extends = nrf52840_base board = nano-g2-ultra debug_tool = jlink @@ -10,5 +19,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/nano-g2-ultra> lib_deps = ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 ;upload_protocol = fs diff --git a/variants/nrf52840/nano-g2-ultra/variant.h b/variants/nrf52840/nano-g2-ultra/variant.h index fd51cf9a1..d8f41a68c 100644 --- a/variants/nrf52840/nano-g2-ultra/variant.h +++ b/variants/nrf52840/nano-g2-ultra/variant.h @@ -87,8 +87,6 @@ static const uint8_t A4 = PIN_A4; #define PIN_WIRE_SDA (0 + 17) #define PIN_WIRE_SCL (0 + 15) -#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC - /* External serial flash W25Q16JV_IQ */ @@ -132,16 +130,18 @@ External serial flash W25Q16JV_IQ #define GPS_L76K #define PIN_GPS_STANDBY (0 + 13) // An output to wake GPS, low means allow sleep, high means force wake STANDBY -#define PIN_GPS_TX (0 + 9) // This is for bits going TOWARDS the CPU -#define PIN_GPS_RX (0 + 10) // This is for bits going TOWARDS the GPS +#define GPS_TX_PIN (0 + 10) // This is for bits going FROM the CPU +#define GPS_RX_PIN (0 + 9) // This is for bits going FROM the GPS // #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_RX PIN_GPS_TX -#define PIN_SERIAL1_TX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN // PCF8563 RTC Module +#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 +#define HAS_RTC 1 /* * SPI Interfaces @@ -169,8 +169,6 @@ External serial flash W25Q16JV_IQ #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) -#define HAS_RTC 1 - /** OLED Screen Model */ diff --git a/arch/nrf52/nrf52.ini b/variants/nrf52840/nrf52.ini similarity index 83% rename from arch/nrf52/nrf52.ini rename to variants/nrf52840/nrf52.ini index e60d47ce7..a07fefb2f 100644 --- a/arch/nrf52/nrf52.ini +++ b/variants/nrf52840/nrf52.ini @@ -2,7 +2,7 @@ ; 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.8.0 + platformio/nordicnrf52@10.10.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR @@ -11,16 +11,30 @@ platform_packages = ; Don't renovate toolchain-gccarmnoneeabi platformio/toolchain-gccarmnoneeabi@~1.90301.0 -build_type = debug +extra_scripts = + ${env.extra_scripts} + extra_scripts/nrf52_extra.py + +build_type = release build_flags = - -include arch/nrf52/cpp_overrides/lfs_util.h + -include variants/nrf52840/cpp_overrides/lfs_util.h ${arduino_base.build_flags} - -DSERIAL_BUFFER_SIZE=1024 -Wno-unused-variable -Isrc/platform/nrf52 -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 -DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 + -Os +build_unflags = + -Ofast + -Og + -ggdb3 + -ggdb2 + -g3 + -g2 + -g + -g1 + -g0 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/variants/nrf52840/nrf52832.ini b/variants/nrf52840/nrf52832.ini new file mode 100644 index 000000000..b106fe7d4 --- /dev/null +++ b/variants/nrf52840/nrf52832.ini @@ -0,0 +1,6 @@ +[nrf52832_base] +extends = nrf52_base + +build_flags = + ${nrf52_base.build_flags} + -DSERIAL_BUFFER_SIZE=1024 diff --git a/arch/nrf52/nrf52840.ini b/variants/nrf52840/nrf52840.ini similarity index 97% rename from arch/nrf52/nrf52840.ini rename to variants/nrf52840/nrf52840.ini index e13443152..09b2ef97d 100644 --- a/arch/nrf52/nrf52840.ini +++ b/variants/nrf52840/nrf52840.ini @@ -1,7 +1,9 @@ [nrf52840_base] extends = nrf52_base -build_flags = ${nrf52_base.build_flags} +build_flags = + ${nrf52_base.build_flags} + -DSERIAL_BUFFER_SIZE=4096 lib_deps = ${nrf52_base.lib_deps} diff --git a/variants/nrf52840/r1-neo/platformio.ini b/variants/nrf52840/r1-neo/platformio.ini index 60f1f6ae1..85fe49cf1 100644 --- a/variants/nrf52840/r1-neo/platformio.ini +++ b/variants/nrf52840/r1-neo/platformio.ini @@ -1,5 +1,14 @@ ; The R1 Neo board [env:r1-neo] +custom_meshtastic_hw_model = 101 +custom_meshtastic_hw_model_slug = MUZI_R1_NEO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = muzi R1 Neo +custom_meshtastic_images = muzi_r1_neo.svg +custom_meshtastic_tags = muzi + extends = nrf52840_base board = r1-neo board_check = true @@ -13,6 +22,9 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/r1-neo> lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=ArtronShop_RX8130CE packageName=artronshop/library/ArtronShop_RX8130CE artronshop/ArtronShop_RX8130CE@1.0.0 diff --git a/variants/nrf52840/rak2560/platformio.ini b/variants/nrf52840/rak2560/platformio.ini index 021e6d03b..1703a13ae 100644 --- a/variants/nrf52840/rak2560/platformio.ini +++ b/variants/nrf52840/rak2560/platformio.ini @@ -1,5 +1,14 @@ ; Firmware for the WisMesh HUB RAK2560, including a onewire module to talk to the RAK 9154 solar battery. [env:rak2560] +custom_meshtastic_hw_model = 22 +custom_meshtastic_hw_model_slug = WISMESH_HUB +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Repeater +custom_meshtastic_images = rak2560.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -13,8 +22,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak2560> + + + lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK-OneWireSerial packageName=beegee-tokyo/RAK-OneWireSerial https://github.com/beegee-tokyo/RAK-OneWireSerial/archive/0.0.2.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini index 1a915a6b3..bb8fa28df 100644 --- a/variants/nrf52840/rak3401_1watt/platformio.ini +++ b/variants/nrf52840/rak3401_1watt/platformio.ini @@ -1,5 +1,15 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak3401-1watt] +custom_meshtastic_hw_model = 117 +custom_meshtastic_hw_model_slug = RAK3401 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK3401 1W +custom_meshtastic_images = rak3401.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -16,9 +26,13 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401 lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # 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 + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 + # 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 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/rak4631/platformio.ini b/variants/nrf52840/rak4631/platformio.ini index 205966529..4a96fc8d9 100644 --- a/variants/nrf52840/rak4631/platformio.ini +++ b/variants/nrf52840/rak4631/platformio.ini @@ -1,26 +1,45 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak4631] +custom_meshtastic_hw_model = 9 +custom_meshtastic_hw_model_slug = RAK4631 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisBlock 4631 +custom_meshtastic_images = rak4631.svg, rak4631_case.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_level = pr board_check = true +build_type = release build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak4631 -D RAK_4631 - -DEINK_DISPLAY_MODEL=GxEPD2_213_BN - -DEINK_WIDTH=250 - -DEINK_HEIGHT=122 + -DMESHTASTIC_USE_EINK_UI=0 -DRADIOLIB_EXCLUDE_SX128X=1 -DRADIOLIB_EXCLUDE_SX127X=1 -DRADIOLIB_EXCLUDE_LR11X0=1 -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631> + + + +build_src_filter = ${nrf52_base.build_src_filter} \ + +<../variants/nrf52840/rak4631> \ + + \ + + \ + + \ + - \ + - \ + - lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 # 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 @@ -36,7 +55,7 @@ extends = env:rak4631 board_level = extra ; if the builtin version of openocd has a buggy version of semihosting, so use the external version -; platform_packages = platformio/tool-openocd@^3.1200.0 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631.build_flags} @@ -44,6 +63,7 @@ build_flags = lib_deps = ${env:rak4631.lib_deps} + # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. @@ -53,6 +73,6 @@ lib_deps = ; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. upload_protocol = stlink -; eventually use platformio/tool-pyocd@^2.3600.0 instad +; eventually use platformio/tool-pyocd@2.3600.0 instad ;upload_protocol = custom ;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index 704520f8d..bc21b7912 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -14,11 +14,16 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - melopero/Melopero RV3028@^1.1.0 - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - beegee-tokyo/RAKwireless RAK12034@^1.0.0 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 + # 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 + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index e0156668b..d0bca377b 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -16,11 +16,16 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_epaper_onrxtx> lib_deps = ${nrf52840_base.lib_deps} - zinggjm/GxEPD2@^1.6.2 - melopero/Melopero RV3028@^1.1.0 - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - beegee-tokyo/RAKwireless RAK12034@^1.0.0 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 + zinggjm/GxEPD2@1.6.5 + # 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 + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index 3c61e3498..bfa4924ce 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -27,12 +27,16 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 # 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 - bblanchon/ArduinoJson @ 6.21.4 + # renovate: datasource=custom.pio depName=ArduinoJson packageName=bblanchon/library/ArduinoJson + bblanchon/ArduinoJson@6.21.4 ; 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 @@ -45,7 +49,7 @@ extends = env:rak4631 board_level = extra ; if the builtin version of openocd has a buggy version of semihosting, so use the external version -; platform_packages = platformio/tool-openocd@^3.1200.0 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631_eth_gw.build_flags} @@ -53,6 +57,7 @@ build_flags = lib_deps = ${env:rak4631_eth_gw.lib_deps} + # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. @@ -62,6 +67,6 @@ lib_deps = ; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. upload_protocol = stlink -; eventually use platformio/tool-pyocd@^2.3600.0 instad +; eventually use platformio/tool-pyocd@2.3600.0 instad ;upload_protocol = custom ;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini index d7dab2678..07d763df3 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini @@ -1,5 +1,14 @@ ; NomadStar Meteor Pro based on RAK4631 with RGBW LED LP5562 support [env:rak4631_nomadstar_meteor_pro] +custom_meshtastic_hw_model = 96 +custom_meshtastic_hw_model_slug = NOMADSTAR_METEOR_PRO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = NomadStar Meteor Pro +custom_meshtastic_images = meteor_pro.svg +custom_meshtastic_tags = NomadStar + extends = nrf52840_base board = wiscore_rak4631 board_check = true @@ -15,6 +24,7 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631_nomadstar_meteor_pro> + + lib_deps = ${nrf52840_base.lib_deps} + # TODO renovate https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library.git#9c366c8 ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) @@ -29,7 +39,7 @@ extends = env:rak4631_nomadstar_meteor_pro board_level = extra ; if the builtin version of openocd has a buggy version of semihosting, so use the external version -; platform_packages = platformio/tool-openocd@^3.1200.0 +; platform_packages = platformio/tool-openocd@3.1200.0 build_flags = ${env:rak4631.build_flags} @@ -37,6 +47,7 @@ build_flags = lib_deps = ${env:rak4631.lib_deps} + # TODO renovate https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip ; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. @@ -46,6 +57,6 @@ lib_deps = ; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. upload_protocol = stlink -; eventually use platformio/tool-pyocd@^2.3600.0 instad +; eventually use platformio/tool-pyocd@2.3600.0 instad ;upload_protocol = custom -;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/nrf52840/rak_wismeshtag/platformio.ini b/variants/nrf52840/rak_wismeshtag/platformio.ini index f04d1f186..1e6e63e60 100644 --- a/variants/nrf52840/rak_wismeshtag/platformio.ini +++ b/variants/nrf52840/rak_wismeshtag/platformio.ini @@ -1,8 +1,17 @@ ; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 [env:rak_wismeshtag] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = rak_wismesh_tag.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 board_check = true +custom_meshtastic_hw_model = 105 +custom_meshtastic_hw_model_slug = WISMESH_TAG +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = RAK WisMesh Tag +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/rak_wismeshtag -D WISMESH_TAG @@ -12,5 +21,3 @@ build_flags = ${nrf52840_base.build_flags} -DRADIOLIB_EXCLUDE_LR11X0=1 -DMESHTASTIC_EXCLUDE_WIFI=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtag> -lib_deps = - ${nrf52840_base.lib_deps} \ No newline at end of file diff --git a/variants/nrf52840/rak_wismeshtag/variant.h b/variants/nrf52840/rak_wismeshtag/variant.h index eba910dc1..159cabf07 100644 --- a/variants/nrf52840/rak_wismeshtag/variant.h +++ b/variants/nrf52840/rak_wismeshtag/variant.h @@ -230,6 +230,7 @@ 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 RAK_4631 1 diff --git a/variants/nrf52840/rak_wismeshtap/platformio.ini b/variants/nrf52840/rak_wismeshtap/platformio.ini index 3369f9c77..f058d9153 100644 --- a/variants/nrf52840/rak_wismeshtap/platformio.ini +++ b/variants/nrf52840/rak_wismeshtap/platformio.ini @@ -1,5 +1,14 @@ ; The very slick RAK wireless RAK10701 Field Tester device. Note you will have to flash to Arduino bootloader to use this firmware. Be aware touch is not currently working. [env:rak_wismeshtap] +custom_meshtastic_hw_model = 84 +custom_meshtastic_hw_model_slug = WISMESH_TAP +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = RAK WisMesh Tap +custom_meshtastic_images = rak-wismeshtap.svg +custom_meshtastic_tags = RAK + extends = nrf52840_base board = wiscore_rak4631 build_flags = ${nrf52840_base.build_flags} @@ -17,14 +26,21 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak_wismeshtap> + + + lib_deps = ${nrf52840_base.lib_deps} - ${nrf52_networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_base.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip - rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 - bodmer/TFT_eSPI - beegee-tokyo/RAKwireless RAK12034@^1.0.0 - beegee-tokyo/RAK14014-FT6336U @ 1.0.1 - beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library + rakwireless/RAKwireless NCP5623 RGB LED library@1.0.3 + # renovate: datasource=custom.pio depName=TFT_eSPI packageName=bodmer/library/TFT_eSPI + bodmer/TFT_eSPI@2.5.43 + # renovate: datasource=custom.pio depName=RAK12034 packageName=beegee-tokyo/library/RAKwireless RAK12034 + beegee-tokyo/RAKwireless RAK12034@1.0.0 + # renovate: datasource=custom.pio depName=RAK14014-FT6336U packageName=beegee-tokyo/library/RAK14014-FT6336U + beegee-tokyo/RAK14014-FT6336U@1.0.1 + # renovate: datasource=custom.pio depName=RAK12035_SoilMoisture packageName=beegee-tokyo/library/RAK12035_SoilMoisture + beegee-tokyo/RAK12035_SoilMoisture@1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/nrf52840/rak_wismeshtap/variant.h b/variants/nrf52840/rak_wismeshtap/variant.h index f961ddf6e..a7b9290a5 100644 --- a/variants/nrf52840/rak_wismeshtap/variant.h +++ b/variants/nrf52840/rak_wismeshtap/variant.h @@ -300,6 +300,7 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define SPI_FREQUENCY 50000000 #define TFT_SPI_PORT SPI1 #define ST7789_CS WB_SPI_CS // Adds compatibility with the rest of the checking for a ST7789 TFT. +#define USE_TFTDISPLAY 1 #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/nrf52840/seeed_solar_node/platformio.ini b/variants/nrf52840/seeed_solar_node/platformio.ini index b2a128c57..18894c049 100644 --- a/variants/nrf52840/seeed_solar_node/platformio.ini +++ b/variants/nrf52840/seeed_solar_node/platformio.ini @@ -1,4 +1,13 @@ [env:seeed_solar_node] +custom_meshtastic_hw_model = 95 +custom_meshtastic_hw_model_slug = SEEED_SOLAR_NODE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed SenseCAP Solar Node +custom_meshtastic_images = seeed_solar.svg +custom_meshtastic_tags = Seeed + board = seeed_solar_node extends = nrf52840_base ;board_level = extra @@ -9,6 +18,4 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_solar_node> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/seeed_solar_node/variant.h b/variants/nrf52840/seeed_solar_node/variant.h index 30d5c5888..b2a1e6dff 100644 --- a/variants/nrf52840/seeed_solar_node/variant.h +++ b/variants/nrf52840/seeed_solar_node/variant.h @@ -110,18 +110,19 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define ADC_MULTIPLIER 3.3 #define BATTERY_PIN PIN_VBAT // PIN_A7 #define AREF_VOLTAGE 3.3 +#define OCV_ARRAY 4200, 3986, 3922, 3812, 3734, 3645, 3527, 3420, 3281, 3087, 2786 // ���������������������������������������� // GPS L76KB // ���������������������������������������� #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 // 44 -#define PIN_GPS_TX D7 // 43 +#define GPS_TX_PIN D6 // 44 +#define GPS_RX_PIN D7 // 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 +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_GPS_STANDBY D0 #define GPS_EN D18 // P1.05 #endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini index 6c137384d..d5b56b7bf 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1/platformio.ini @@ -1,4 +1,14 @@ [env:seeed_wio_tracker_L1] +custom_meshtastic_hw_model = 99 +custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Wio Tracker L1 +custom_meshtastic_images = wio_tracker_l1_case.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true + board = seeed_wio_tracker_L1 extends = nrf52840_base build_flags = ${nrf52840_base.build_flags} @@ -8,6 +18,4 @@ build_flags = ${nrf52840_base.build_flags} -I src/platform/nrf52/softdevice/nrf52 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink diff --git a/variants/nrf52840/seeed_wio_tracker_L1/variant.h b/variants/nrf52840/seeed_wio_tracker_L1/variant.h index c5647caa8..b62b65161 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1/variant.h @@ -119,16 +119,14 @@ static const uint8_t SCL = PIN_WIRE_SCL; // ���������������������������������������� #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 // P0.26 -#define PIN_GPS_TX D7 +#define GPS_TX_PIN D6 // P0.26 - This is data from the MCU +#define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #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 +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN -#define GPS_RX_PIN PIN_GPS_TX -#define GPS_TX_PIN PIN_GPS_RX #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 7fb890303..98aeb8700 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -19,7 +19,7 @@ // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" -#include "graphics/niche/Inputs/TwoButton.h" +#include "graphics/niche/Inputs/TwoButtonExtended.h" void setupNicheGraphics() { @@ -54,7 +54,12 @@ void setupNicheGraphics() InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise +#if HAS_TRACKBALL + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead +#endif inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side @@ -75,16 +80,36 @@ void setupNicheGraphics() // Buttons // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); +#if HAS_TRACKBALL + // #0: Exit Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->exitShort(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->exitLong(); }); + + // #1: Joystick Center + buttons->setWiring(1, TB_PRESS); + buttons->setTiming(1, 75, 500); + buttons->setHandlerShortPress(1, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(1, [inkhud]() { inkhud->longpress(); }); + + // Joystick Directions + buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); + buttons->setJoystickDebounce(50); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->navUp(); }, [inkhud]() { inkhud->navDown(); }, + [inkhud]() { inkhud->navLeft(); }, [inkhud]() { inkhud->navRight(); }); +#else + // #0: User Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); +#endif // Begin handling button events buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 7f9eb0e2c..6b36a79c3 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -1,4 +1,13 @@ [env:seeed_wio_tracker_L1_eink] +custom_meshtastic_hw_model = 100 +custom_meshtastic_hw_model_slug = SEEED_WIO_TRACKER_L1_EINK +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +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 + board = seeed_wio_tracker_L1 extends = nrf52840_base ;board_level = extra @@ -24,7 +33,8 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = ${nrf52840_base.lib_deps} - https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master + https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] @@ -44,4 +54,4 @@ build_src_filter = lib_deps = ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space) ${nrf52840_base.lib_deps} -debug_tool = jlink \ No newline at end of file +debug_tool = jlink diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index f33d200b1..ae20f3c36 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -122,21 +122,21 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define ADC_MULTIPLIER 2.0 #define BATTERY_PIN PIN_VBAT // PIN_A7 #define AREF_VOLTAGE 3.6 +#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300 + // ���������������������������������������� // GPS L76KB // ���������������������������������������� #define GPS_L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 // P0.26 -#define PIN_GPS_TX D7 +#define GPS_TX_PIN D6 // P0.26 - This is data from the MCU +#define GPS_RX_PIN D7 // P0.27 - This is data from the GNSS #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 +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN -#define GPS_RX_PIN PIN_GPS_TX -#define GPS_TX_PIN PIN_GPS_RX #define PIN_GPS_STANDBY D0 // #define GPS_DEBUG diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 4c68b40e8..68be47622 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -1,5 +1,14 @@ ; Seeed Xiao BLE: https://wiki.seeedstudio.com/XIAO_BLE/ [env:seeed_xiao_nrf52840_kit] +custom_meshtastic_hw_model = 88 +custom_meshtastic_hw_model_slug = XIAO_NRF52_KIT +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = Seeed Xiao NRF52840 Kit +custom_meshtastic_images = seeed_xiao_nrf52_kit.svg +custom_meshtastic_tags = Seeed + extends = nrf52840_base board = xiao_ble_sense board_level = pr @@ -11,8 +20,6 @@ build_flags = ${nrf52840_base.build_flags} -DGPS_L76K board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h index a65500612..0844595da 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h @@ -147,12 +147,12 @@ static const uint8_t SCK = PIN_SPI_SCK; */ // GPS L76K #ifdef GPS_L76K -#define PIN_GPS_RX D6 -#define PIN_GPS_TX D7 +#define GPS_TX_PIN D6 // This is data from the MCU +#define GPS_RX_PIN D7 // This is data from the GNSS module #define HAS_GPS 1 #define GPS_THREAD_INTERVAL 50 -#define PIN_SERIAL1_RX PIN_GPS_TX -#define PIN_SERIAL1_TX PIN_GPS_RX +#define PIN_SERIAL1_TX GPS_TX_PIN +#define PIN_SERIAL1_RX GPS_RX_PIN #define PIN_GPS_STANDBY D0 #else #define PIN_SERIAL1_RX (-1) diff --git a/variants/nrf52840/t-echo-lite/platformio.ini b/variants/nrf52840/t-echo-lite/platformio.ini index 90e6487a7..c873dea37 100644 --- a/variants/nrf52840/t-echo-lite/platformio.ini +++ b/variants/nrf52840/t-echo-lite/platformio.ini @@ -1,5 +1,14 @@ ; Using original screen class [env:t-echo-lite] +custom_meshtastic_hw_model = 109 +custom_meshtastic_hw_model_slug = T_ECHO_LITE +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo Lite +custom_meshtastic_images = techo_lite.svg +custom_meshtastic_tags = LilyGo + extends = nrf52840_base board = t-echo board_check = true @@ -20,5 +29,6 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-lite> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/a05c11c02862624266b61599b0d6ba93e33c6f24.zip ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 051fb3099..a8fc027c8 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -1,5 +1,14 @@ ; Using original screen class [env:t-echo] +custom_meshtastic_hw_model = 7 +custom_meshtastic_hw_model_slug = T_ECHO +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Echo +custom_meshtastic_images = t-echo.svg +custom_meshtastic_tags = LilyGo + extends = nrf52840_base board = t-echo board_level = pr @@ -20,8 +29,10 @@ build_flags = ${nrf52840_base.build_flags} build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo> lib_deps = ${nrf52840_base.lib_deps} + # renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master https://github.com/meshtastic/GxEPD2/archive/55f618961db45a23eff0233546430f1e5a80f63a.zip - lewisxhe/PCF8563_Library@^1.0.1 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 ;upload_protocol = fs [env:t-echo-inkhud] @@ -41,4 +52,5 @@ build_src_filter = lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${nrf52840_base.lib_deps} - lewisxhe/PCF8563_Library@^1.0.1 \ No newline at end of file + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.3 diff --git a/variants/nrf52840/t-echo/variant.h b/variants/nrf52840/t-echo/variant.h index b2692e448..9244fc6c3 100644 --- a/variants/nrf52840/t-echo/variant.h +++ b/variants/nrf52840/t-echo/variant.h @@ -108,8 +108,6 @@ No longer populated on PCB #define TP_SER_IO (0 + 11) -#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC - /* External serial flash WP25R1635FZUIL0 */ @@ -181,9 +179,9 @@ External serial flash WP25R1635FZUIL0 #define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake // Seems to be missing on this new board -// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS -#define GPS_TX_PIN (32 + 8) // This is for bits going TOWARDS the CPU -#define GPS_RX_PIN (32 + 9) // This is for bits going TOWARDS the GPS +#define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 8) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 9) // This is for bits going TOWARDS the GPS #define GPS_THREAD_INTERVAL 50 @@ -191,7 +189,9 @@ External serial flash WP25R1635FZUIL0 #define PIN_SERIAL1_TX GPS_TX_PIN // PCF8563 RTC Module +#define PIN_RTC_INT (0 + 16) // Interrupt from the PCF8563 RTC #define PCF8563_RTC 0x51 +#define HAS_RTC 1 /* * SPI Interfaces @@ -203,8 +203,6 @@ External serial flash WP25R1635FZUIL0 #define PIN_SPI_MOSI (0 + 22) #define PIN_SPI_SCK (0 + 19) -#define PIN_PWR_EN (0 + 6) - // To debug via the segger JLINK console rather than the CDC-ACM serial device // #define USE_SEGGER @@ -221,8 +219,6 @@ External serial flash WP25R1635FZUIL0 // #define NO_EXT_GPIO 1 -#define HAS_RTC 1 - #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/tracker-t1000-e/platformio.ini b/variants/nrf52840/tracker-t1000-e/platformio.ini index 905d751fd..43ba7a8b4 100644 --- a/variants/nrf52840/tracker-t1000-e/platformio.ini +++ b/variants/nrf52840/tracker-t1000-e/platformio.ini @@ -1,7 +1,16 @@ [env:tracker-t1000-e] +custom_meshtastic_support_level = 1 +custom_meshtastic_images = tracker-t1000-e.svg +custom_meshtastic_tags = Seeed + extends = nrf52840_base board = tracker-t1000-e board_level = pr +custom_meshtastic_hw_model = 71 +custom_meshtastic_hw_model_slug = TRACKER_T1000_E +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_display_name = Seeed SenseCAP T1000-E +custom_meshtastic_actively_supported = true build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/tracker-t1000-e -Isrc/platform/nrf52/softdevice @@ -16,6 +25,7 @@ board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/tracker-t1000-e> lib_deps = ${nrf52840_base.lib_deps} + # TODO renovate https://github.com/meshtastic/QMA6100P_Arduino_Library/archive/14c900b8b2e4feaac5007a7e41e0c1b7f0841136.zip debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 403552ec0..5b6719e12 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -142,6 +142,8 @@ extern "C" { #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define OCV_ARRAY 4190, 4042, 3957, 3885, 3820, 3776, 3746, 3725, 3696, 3644, 3100 + // Buzzer #define BUZZER_EN_PIN (32 + 5) // P1.05, always high #define PIN_BUZZER (0 + 25) // P0.25, pwm output diff --git a/variants/nrf52840/wio-sdk-wm1110/platformio.ini b/variants/nrf52840/wio-sdk-wm1110/platformio.ini index 028129783..7c11ef6f6 100644 --- a/variants/nrf52840/wio-sdk-wm1110/platformio.ini +++ b/variants/nrf52840/wio-sdk-wm1110/platformio.ini @@ -4,11 +4,22 @@ extends = nrf52840_base board = wio-sdk-wm1110 extra_scripts = - ${env.extra_scripts} + ${nrf52840_base.extra_scripts} extra_scripts/disable_adafruit_usb.py # 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} -DUSBCON -DUSE_TINYUSB +build_unflags = + -Ofast + -Og + -ggdb3 + -ggdb2 + -g3 + -g2 + -g + -g1 + -g0 + -DUSBCON + -DUSE_TINYUSB build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/wio-sdk-wm1110 -Isrc/platform/nrf52/softdevice diff --git a/variants/nrf52840/wio-t1000-s/platformio.ini b/variants/nrf52840/wio-t1000-s/platformio.ini index c6b61fc8a..a6ea5102c 100644 --- a/variants/nrf52840/wio-t1000-s/platformio.ini +++ b/variants/nrf52840/wio-t1000-s/platformio.ini @@ -10,8 +10,6 @@ build_flags = ${nrf52840_base.build_flags} -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-t1000-s> -lib_deps = - ${nrf52840_base.lib_deps} debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) upload_protocol = jlink \ No newline at end of file diff --git a/variants/nrf52840/wio-tracker-wm1110/platformio.ini b/variants/nrf52840/wio-tracker-wm1110/platformio.ini index 73b7dedd4..515712062 100644 --- a/variants/nrf52840/wio-tracker-wm1110/platformio.ini +++ b/variants/nrf52840/wio-tracker-wm1110/platformio.ini @@ -1,5 +1,15 @@ ; The red tracker Dev Board with the WM1110 module [env:wio-tracker-wm1110] +custom_meshtastic_hw_model = 21 +custom_meshtastic_hw_model_slug = WIO_WM1110 +custom_meshtastic_architecture = nrf52840 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed Wio WM1110 Tracker +custom_meshtastic_images = wio-tracker-wm1110.svg +custom_meshtastic_tags = Seeed +custom_meshtastic_requires_dfu = true + extends = nrf52840_base board = wio-tracker-wm1110 build_flags = ${nrf52840_base.build_flags} @@ -9,7 +19,5 @@ build_flags = ${nrf52840_base.build_flags} -DWIO_WM1110 board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/wio-tracker-wm1110> -lib_deps = - ${nrf52840_base.lib_deps} ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink \ No newline at end of file +;upload_protocol = jlink diff --git a/variants/rp2040/challenger_2040_lora/platformio.ini b/variants/rp2040/challenger_2040_lora/platformio.ini index 4a709d650..06371d18e 100644 --- a/variants/rp2040/challenger_2040_lora/platformio.ini +++ b/variants/rp2040/challenger_2040_lora/platformio.ini @@ -10,7 +10,5 @@ build_flags = -I variants/rp2040/challenger_2040_lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/ec_catsniffer/platformio.ini b/variants/rp2040/ec_catsniffer/platformio.ini index b70eff6d7..08fa3fffc 100644 --- a/variants/rp2040/ec_catsniffer/platformio.ini +++ b/variants/rp2040/ec_catsniffer/platformio.ini @@ -9,7 +9,5 @@ build_flags = -I variants/rp2040/ec_catsniffer -D DEBUG_RP2040_PORT=Serial ; -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap diff --git a/variants/rp2040/feather_rp2040_rfm95/platformio.ini b/variants/rp2040/feather_rp2040_rfm95/platformio.ini index ef4118cb0..1da1218e4 100644 --- a/variants/rp2040/feather_rp2040_rfm95/platformio.ini +++ b/variants/rp2040/feather_rp2040_rfm95/platformio.ini @@ -1,6 +1,7 @@ [env:feather_rp2040_rfm95] extends = rp2040_base board = adafruit_feather +board_level = extra upload_protocol = picotool # add our variants files to the include and src paths build_flags = @@ -9,7 +10,5 @@ build_flags = -I variants/rp2040/feather_rp2040_rfm95 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/nibble_rp2040/platformio.ini b/variants/rp2040/nibble_rp2040/platformio.ini index 024a72206..5f4025cff 100644 --- a/variants/rp2040/nibble_rp2040/platformio.ini +++ b/variants/rp2040/nibble_rp2040/platformio.ini @@ -10,7 +10,5 @@ build_flags = -I variants/rp2040/nibble_rp2040 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rak11310/platformio.ini b/variants/rp2040/rak11310/platformio.ini index f3eaa176e..2c2b2a4bf 100644 --- a/variants/rp2040/rak11310/platformio.ini +++ b/variants/rp2040/rak11310/platformio.ini @@ -1,4 +1,14 @@ [env:rak11310] +custom_meshtastic_hw_model = 26 +custom_meshtastic_hw_model_slug = RAK11310 +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RAK WisBlock 11310 +custom_meshtastic_images = rak11310.svg +custom_meshtastic_tags = RAK +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rakwireless_rak11300 board_level = pr @@ -14,7 +24,10 @@ build_src_filter = ${rp2040_base.build_src_filter} +<../variants/rp2040/rak11310 lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} - melopero/Melopero RV3028@^1.1.0 + ${networking_extra.lib_deps} + # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 + melopero/Melopero RV3028@1.2.0 + # renovate: datasource=github-tags depName=RAK13800-W5100S packageName=RAKWireless/RAK13800-W5100S https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rp2040-lora/platformio.ini b/variants/rp2040/rp2040-lora/platformio.ini index d59e74f20..f1e0b9af6 100644 --- a/variants/rp2040/rp2040-lora/platformio.ini +++ b/variants/rp2040/rp2040-lora/platformio.ini @@ -1,4 +1,13 @@ [env:rp2040-lora] +custom_meshtastic_hw_model = 30 +custom_meshtastic_hw_model_slug = RP2040_LORA +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 2 +custom_meshtastic_display_name = RP2040 LoRa +custom_meshtastic_tags = Waveshare +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipico upload_protocol = picotool @@ -9,7 +18,5 @@ build_flags = -I variants/rp2040/rp2040-lora -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/arch/rp2xx0/rp2040.ini b/variants/rp2040/rp2040.ini similarity index 79% rename from arch/rp2xx0/rp2040.ini rename to variants/rp2040/rp2040.ini index 4f9421872..9abfcbe10 100644 --- a/arch/rp2xx0/rp2040.ini +++ b/variants/rp2040/rp2040.ini @@ -2,12 +2,12 @@ [rp2040_base] platform = # TODO renovate - https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 - ; For arduino-pico >= 4.4.3 + https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 + ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate - framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 + arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -17,6 +17,7 @@ build_flags = -Isrc/platform/rp2xx0/hardware_rosc/include -Isrc/platform/rp2xx0/pico_sleep/include -D__PLAT_RP2040__ + -D__FREERTOS=1 # -D _POSIX_THREADS build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - diff --git a/variants/rp2040/rpipico-slowclock/platformio.ini b/variants/rp2040/rpipico-slowclock/platformio.ini index 30928aead..d5f86b0ad 100644 --- a/variants/rp2040/rpipico-slowclock/platformio.ini +++ b/variants/rp2040/rpipico-slowclock/platformio.ini @@ -21,8 +21,6 @@ build_flags = -DHW_SPI1_DEVICE -g -DNO_USB -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags} -g -DNO_USB diff --git a/variants/rp2040/rpipico/platformio.ini b/variants/rp2040/rpipico/platformio.ini index a6171bbac..4ae134b28 100644 --- a/variants/rp2040/rpipico/platformio.ini +++ b/variants/rp2040/rpipico/platformio.ini @@ -1,9 +1,18 @@ [env:pico] +custom_meshtastic_hw_model = 47 +custom_meshtastic_hw_model_slug = RPI_PICO +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Raspberry Pi Pico +custom_meshtastic_images = pico.svg +custom_meshtastic_tags = RPi, DIY +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipico board_level = pr upload_protocol = picotool - # add our variants files to the include and src paths build_flags = ${rp2040_base.build_flags} @@ -11,7 +20,5 @@ build_flags = -I variants/rp2040/rpipico -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2040_base.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/rpipicow/platformio.ini b/variants/rp2040/rpipicow/platformio.ini index 60845ba39..99e02a1aa 100644 --- a/variants/rp2040/rpipicow/platformio.ini +++ b/variants/rp2040/rpipicow/platformio.ini @@ -1,4 +1,14 @@ [env:picow] +custom_meshtastic_hw_model = 47 +custom_meshtastic_hw_model_slug = RPI_PICO +custom_meshtastic_architecture = rp2040 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Raspberry Pi Pico W +custom_meshtastic_images = rpipicow.svg +custom_meshtastic_tags = RPi, DIY +custom_meshtastic_requires_dfu = true + extends = rp2040_base board = rpipicow board_level = pr @@ -16,5 +26,6 @@ build_src_filter = ${rp2040_base.build_src_filter} + lib_deps = ${rp2040_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} debug_build_flags = ${rp2040_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2040/senselora_rp2040/platformio.ini b/variants/rp2040/senselora_rp2040/platformio.ini index 3a574d0f9..e02fd587b 100644 --- a/variants/rp2040/senselora_rp2040/platformio.ini +++ b/variants/rp2040/senselora_rp2040/platformio.ini @@ -9,5 +9,3 @@ build_flags = ${rp2040_base.build_flags} -D SENSELORA_RP2040 -I variants/rp2040/senselora_rp2040 -D DEBUG_RP2040_PORT=Serial -lib_deps = - ${rp2040_base.lib_deps} \ No newline at end of file diff --git a/arch/rp2xx0/rp2350.ini b/variants/rp2350/rp2350.ini similarity index 78% rename from arch/rp2xx0/rp2350.ini rename to variants/rp2350/rp2350.ini index e8611a113..934875c6a 100644 --- a/arch/rp2xx0/rp2350.ini +++ b/variants/rp2350/rp2350.ini @@ -2,12 +2,12 @@ [rp2350_base] platform = # TODO renovate - https://github.com/maxgerhardt/platform-raspberrypi#76ecf3c7e9dd4503af0331154c4ca1cddc4b03e5 - ; For arduino-pico >= 4.4.3 + https://github.com/maxgerhardt/platform-raspberrypi#cc24cfef37ed22ca9f2a6aead28c2deb76c39f24 + ; For arduino-pico >= 5.4.4 extends = arduino_base platform_packages = # TODO renovate - framework-arduinopico@https://github.com/earlephilhower/arduino-pico#4.4.3 + arduino-pico@https://github.com/earlephilhower/arduino-pico/releases/download/5.4.4/rp2040-5.4.4.zip board_build.core = earlephilhower board_build.filesystem_size = 0.5m @@ -15,6 +15,7 @@ build_flags = ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align -Isrc/platform/rp2xx0 -D__PLAT_RP2350__ + -D__FREERTOS=1 build_src_filter = ${arduino_base.build_src_filter} - - - - - - - - - - - diff --git a/variants/rp2350/rpipico2/platformio.ini b/variants/rp2350/rpipico2/platformio.ini index ad7a4ce51..30dc15256 100644 --- a/variants/rp2350/rpipico2/platformio.ini +++ b/variants/rp2350/rpipico2/platformio.ini @@ -11,7 +11,5 @@ build_flags = -I variants/rp2350/rpipico2 -D DEBUG_RP2040_PORT=Serial -D HW_SPI1_DEVICE -lib_deps = - ${rp2350_base.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g debug_tool = cmsis-dap ; for e.g. Picotool diff --git a/variants/rp2350/rpipico2w/platformio.ini b/variants/rp2350/rpipico2w/platformio.ini index 5dbce533b..da408b67d 100644 --- a/variants/rp2350/rpipico2w/platformio.ini +++ b/variants/rp2350/rpipico2w/platformio.ini @@ -30,4 +30,5 @@ build_src_filter = ${rp2350_base.build_src_filter} + lib_deps = ${rp2350_base.lib_deps} ${networking_base.lib_deps} + ${networking_extra.lib_deps} debug_build_flags = ${rp2350_base.build_flags}, -g diff --git a/arch/stm32/stm32.ini b/variants/stm32/stm32.ini similarity index 92% rename from arch/stm32/stm32.ini rename to variants/stm32/stm32.ini index 7732533c9..bb0a4d3ce 100644 --- a/arch/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -2,13 +2,13 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.3.0 + platformio/ststm32@19.4.0 platform_packages = - # TODO renovate + # 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 extra_scripts = ${env.extra_scripts} - post:extra_scripts/extra_stm32.py + extra_scripts/stm32_extra.py build_type = release @@ -51,7 +51,6 @@ debug_tool = stlink lib_deps = ${env.lib_deps} ${radiolib_base.lib_deps} - # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index a9fcf51d6..311cade58 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -19,7 +19,3 @@ build_flags = -DGPS_SERIAL_PORT=Serial2 upload_port = stlink - -lib_deps = - ${stm32_base.lib_deps} - # Add your custom sensor here! \ No newline at end of file diff --git a/version.properties b/version.properties index 05d8a493f..0a028eff0 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 16 \ No newline at end of file +build = 18