diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile index 54b5cda0f..6114417c9 100644 --- a/.clusterfuzzlite/Dockerfile +++ b/.clusterfuzzlite/Dockerfile @@ -20,7 +20,7 @@ ENV PIP_ROOT_USER_ACTION=ignore RUN apt-get update && apt-get install --no-install-recommends -y \ cmake git zip libgpiod-dev libbluetooth-dev libi2c-dev \ libunistring-dev libmicrohttpd-dev libgnutls28-dev libgcrypt20-dev \ - libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev && \ + libusb-1.0-0-dev libssl-dev pkg-config libsqlite3-dev libsdl2-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir -U \ platformio==6.1.16 \ diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index c048b7ac2..e93796614 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -100,7 +100,7 @@ runs: id: version - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml index de114be1c..b3744493b 100644 --- a/.github/workflows/build_debian_src.yml +++ b/.github/workflows/build_debian_src.yml @@ -42,7 +42,7 @@ jobs: sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -64,7 +64,7 @@ jobs: PKG_VERSION: ${{ steps.version.outputs.deb }} - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src overwrite: true diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 23690766a..c4c7a54e0 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -111,7 +111,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY - name: Store binaries as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-firmware with: name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} @@ -127,7 +127,7 @@ jobs: release/device-*.bat - name: Store manifests as an artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml index 9cc0bac78..0a1744edb 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@v7 + - uses: actions/download-artifact@v8 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@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} overwrite: true @@ -127,7 +127,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-*-${{ needs.version.outputs.long }} merge-multiple: true @@ -146,7 +146,7 @@ jobs: run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip overwrite: true diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 6b48e8128..39da22ae0 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -177,7 +177,7 @@ jobs: ref: ${{github.event.pull_request.head.ref}} repository: ${{github.event.pull_request.head.repo.full_name}} - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: path: ./ pattern: firmware-${{matrix.arch}}-* @@ -187,7 +187,7 @@ jobs: run: ls -R - name: Repackage in single firmware zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -205,7 +205,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -224,7 +224,7 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -250,13 +250,13 @@ jobs: filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) fetch-depth: 0 - name: Download the current manifests - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: path: ./manifests-new/ pattern: manifest-* merge-multiple: true - name: Upload combined manifests for later commit and global stats crunching. - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 id: upload-manifest with: name: manifests-${{ github.sha }} @@ -324,14 +324,14 @@ jobs: body: ${{ steps.release_notes.outputs.notes }} - name: Download source deb - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -355,7 +355,7 @@ jobs: }' > firmware-${{ needs.version.outputs.long }}.json - name: Save Release manifest artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: manifest-${{ needs.version.outputs.long }} overwrite: true @@ -396,7 +396,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -413,7 +413,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -454,14 +454,14 @@ jobs: python-version: 3.x - name: Get firmware artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} merge-multiple: true path: ./publish - name: Get manifest artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: manifest-${{ needs.version.outputs.long }} path: ./publish diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml index bd3f6d4eb..ad8534984 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@v7 + - uses: actions/download-artifact@v8 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@v6 + uses: actions/upload-artifact@v7 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -176,7 +176,7 @@ jobs: ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -195,7 +195,7 @@ jobs: run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - name: Repackage in single elfs zip - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} overwrite: true @@ -235,14 +235,14 @@ jobs: Autogenerated by github action, developer should edit as required before publishing... - name: Download source deb - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src merge-multiple: true path: ./output/debian-src - name: Download `native-tft` pio deps - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} merge-multiple: true @@ -292,7 +292,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -309,7 +309,7 @@ jobs: - name: Zip firmware run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 with: name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} merge-multiple: true @@ -347,7 +347,7 @@ jobs: with: python-version: 3.x - - uses: actions/download-artifact@v7 + - uses: actions/download-artifact@v8 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 63f1fe8a0..395b721a5 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@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml index 82ffe66e9..eddf92cdc 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@v6 + uses: actions/upload-artifact@v7 with: name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} overwrite: true diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml index 9a463dbea..86e655809 100644 --- a/.github/workflows/package_ppa.yml +++ b/.github/workflows/package_ppa.yml @@ -46,7 +46,7 @@ jobs: sudo apt-get install -y dput - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} id: gpg @@ -60,7 +60,7 @@ jobs: id: version - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src merge-multiple: true diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 6306d777f..3556226ba 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -50,7 +50,7 @@ jobs: - name: Download test artifacts if: needs.native-tests.result != 'skipped' - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml index d93449d6d..95e5c2c3d 100644 --- a/.github/workflows/sec_sast_semgrep_cron.yml +++ b/.github/workflows/sec_sast_semgrep_cron.yml @@ -33,7 +33,7 @@ jobs: # step 3 - name: save report as pipeline artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: report.sarif overwrite: true diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index fc0702bd8..9255975a8 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.1 + uses: actions/stale@v10.2.0 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 b527c2fd9..e86146eb3 100644 --- a/.github/workflows/test_native.yml +++ b/.github/workflows/test_native.yml @@ -59,7 +59,7 @@ jobs: id: version - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} @@ -94,7 +94,7 @@ jobs: - name: Save test results if: always() # run this step even if previous step failed - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: platformio-test-report-${{ steps.version.outputs.long }} overwrite: true @@ -108,7 +108,7 @@ jobs: sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. - name: Save coverage information - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() # run this step even if previous step failed with: name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} @@ -137,7 +137,7 @@ jobs: id: version - name: Download test artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: platformio-test-report-${{ steps.version.outputs.long }} merge-multiple: true @@ -150,7 +150,7 @@ jobs: reporter: java-junit - name: Download coverage artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} path: code-coverage-report @@ -163,7 +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@v6 + uses: actions/upload-artifact@v7 with: name: code-coverage-report-${{ steps.version.outputs.long }} path: code-coverage-report diff --git a/.gitignore b/.gitignore index d6d97c6c4..43cee78db 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ __pycache__ *~ venv/ +.venv/ release/ .vscode/extensions.json /compile_commands.json diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 9d563a39a..90346778f 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,24 +4,24 @@ cli: plugins: sources: - id: trunk - ref: v1.7.4 + ref: v1.7.5 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.497 - - renovate@42.84.2 - - prettier@3.8.0 - - trufflehog@3.92.5 + - checkov@3.2.506 + - renovate@43.49.0 + - prettier@3.8.1 + - trufflehog@3.93.6 - yamllint@1.38.0 - - bandit@1.9.3 - - trivy@0.68.2 + - bandit@1.9.4 + - trivy@0.69.2 - taplo@0.10.0 - - ruff@0.14.13 - - isort@7.0.0 - - markdownlint@0.47.0 - - oxipng@10.0.0 + - ruff@0.15.4 + - isort@8.0.1 + - markdownlint@0.48.0 + - oxipng@10.1.0 - svgo@4.0.0 - - actionlint@1.7.10 + - actionlint@1.7.11 - flake8@7.3.0 - hadolint@2.14.0 - shfmt@3.6.0 diff --git a/Dockerfile b/Dockerfile index 91d3f7796..e00d81658 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ curl wget g++ zip git ca-certificates pkg-config \ libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ - libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev \ + libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -53,7 +53,7 @@ USER root RUN apt-get update && apt-get --no-install-recommends -y install \ libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \ liborcania2.3 libulfius2.7t64 libssl3t64 \ - libx11-6 libinput10 libxkbcommon-x11-0 \ + libx11-6 libinput10 libxkbcommon-x11-0 libsdl2-2.0-0 \ && apt-get clean && rm -rf /var/lib/apt/lists/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..12479b36d --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,26 @@ +# Lightweight container for running native PlatformIO tests on non-Linux hosts +FROM python:3.14-slim-trixie + +ENV DEBIAN_FRONTEND=noninteractive +ENV PIP_ROOT_USER_ACTION=ignore + +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install --no-install-recommends -y \ + g++ git ca-certificates pkg-config \ + libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \ + libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \ + libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir platformio==6.1.19 \ + && useradd --create-home --shell /usr/sbin/nologin meshtastic + +WORKDIR /firmware +RUN chown -R meshtastic:meshtastic /firmware + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD platformio --version || exit 1 + +USER meshtastic + +# Run tests by default; override with docker run args for specific filters +CMD ["platformio", "test", "-e", "coverage", "-v"] diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 64c281788..75c9aa594 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -11,7 +11,7 @@ RUN apk --no-cache add \ bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \ libgpiod-dev yaml-cpp-dev bluez-dev \ libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \ - libx11-dev libinput-dev libxkbcommon-dev sqlite-dev \ + libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \ && rm -rf /var/cache/apk/* \ && pip install --no-cache-dir -U platformio \ && mkdir /tmp/firmware @@ -42,7 +42,7 @@ USER root RUN apk --no-cache add \ shadow libstdc++ libbsd libgpiod yaml-cpp libusb \ - i2c-tools libuv libx11 libinput libxkbcommon \ + i2c-tools libuv libx11 libinput libxkbcommon sdl2 \ && rm -rf /var/cache/apk/* \ && mkdir -p /var/lib/meshtasticd \ && mkdir -p /etc/meshtasticd/config.d \ diff --git a/bin/check-all.sh b/bin/check-all.sh index 29d6b5532..9c7fc694d 100755 --- a/bin/check-all.sh +++ b/bin/check-all.sh @@ -23,4 +23,4 @@ for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done -pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high +pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=low --fail-on-defect=medium --fail-on-defect=high diff --git a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml index 066e36a10..80df34ca7 100644 --- a/bin/config.d/lora-hat-rak-6421-pi-hat.yaml +++ b/bin/config.d/lora-hat-rak-6421-pi-hat.yaml @@ -9,3 +9,5 @@ Lora: DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 + GPIO_DETECT_PA: 13 + TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] \ No newline at end of file diff --git a/bin/config.d/lora-lyra-ultra_1w.yaml b/bin/config.d/lora-lyra-ultra_1w.yaml new file mode 100644 index 000000000..0bdc05fef --- /dev/null +++ b/bin/config.d/lora-lyra-ultra_1w.yaml @@ -0,0 +1,16 @@ +# For use with Armbian luckfox-lyra-ultra-w +# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat +# 1 Watt Lyra Ultra hat +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + CS: 10 + IRQ: 5 + Busy: 11 + Reset: 9 + RXen: 14 + + spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-lyra-ultra_2w.yaml b/bin/config.d/lora-lyra-ultra_2w.yaml new file mode 100644 index 000000000..a1fe6d7f7 --- /dev/null +++ b/bin/config.d/lora-lyra-ultra_2w.yaml @@ -0,0 +1,17 @@ +# For use with Armbian luckfox-lyra-ultra-w +# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config +# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat +# 2 Watt Lyra Ultra hat +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + SX126X_MAX_POWER: 8 + CS: 10 + IRQ: 5 + Busy: 11 + Reset: 9 + RXen: 14 + + spidev: spidev0.0 #pins are (CS=10, CLK=8, MOSI=6, MISO=7) + spiSpeed: 2000000 diff --git a/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml new file mode 100644 index 000000000..8425fc385 --- /dev/null +++ b/bin/config.d/lora-lyra-ws-raspberry-pi-pico-hat.yaml @@ -0,0 +1,25 @@ +# For use with Armbian luckfox-lyra // luckfox-lyra-plus +# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config +# Waveshare LoRa HAT for Raspberry Pi Pico +# https://www.waveshare.com/wiki/Pico-LoRa-SX1262 +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + spidev: spidev0.0 + CS: # GPIO0_B5 + pin: 13 + gpiochip: 0 + line: 13 + IRQ: # GPIO1_C2 + pin: 50 + gpiochip: 1 + line: 18 + Busy: # GPIO0_B4 + pin: 12 + gpiochip: 0 + line: 12 + Reset: # GPIO0_A2 + pin: 2 + gpiochip: 0 + line: 2 diff --git a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml index 7726eccd1..8b32c5af2 100644 --- a/bin/config.d/lora-usb-umesh-1262-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1262-30dbm.yaml @@ -13,8 +13,7 @@ Lora: # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI - NUM_PA_POINTS: 22 - TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 + TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). diff --git a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml index c054a92f9..df772184c 100644 --- a/bin/config.d/lora-usb-umesh-1268-30dbm.yaml +++ b/bin/config.d/lora-usb-umesh-1268-30dbm.yaml @@ -13,8 +13,7 @@ Lora: # USB_Serialnum: 12345678 SX126X_MAX_POWER: 22 # Reduce output power to improve EMI - NUM_PA_POINTS: 22 - TX_GAIN_LORA: 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7 + TX_GAIN_LORA: [12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7] # Note: This module integrates an additional PA to achieve higher output power. # The 'power' parameter here does not represent the actual RF output. # TX_GAIN_LORA defines the gain offset applied at each SX1262 input power step (1–22 dBm). diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 6ad8962d1..3566ad506 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.19 diff --git a/bin/test-native-docker.sh b/bin/test-native-docker.sh new file mode 100755 index 000000000..b42c940c5 --- /dev/null +++ b/bin/test-native-docker.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Run native PlatformIO tests inside Docker (for macOS / non-Linux hosts). +# +# Usage: +# ./bin/test-native-docker.sh # run all native tests +# ./bin/test-native-docker.sh -f test_transmit_history # run specific test filter +# ./bin/test-native-docker.sh --rebuild # force rebuild the image +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +IMAGE_NAME="meshtastic-native-test" + +REBUILD=false +EXTRA_ARGS=() + +for arg in "$@"; do + if [[ "$arg" == "--rebuild" ]]; then + REBUILD=true + else + EXTRA_ARGS+=("$arg") + fi +done + +if $REBUILD || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + echo "Building test image (first run may take a few minutes)..." + docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/Dockerfile.test" "$ROOT_DIR" +fi + +# Disable BUILD_EPOCH to avoid full rebuilds between test runs (matches CI) +sed_cmd='s/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' + +# Default: run all tests. Pass extra args (e.g. -f test_transmit_history) through. +if [[ ${#EXTRA_ARGS[@]} -eq 0 ]]; then + CMD=("platformio" "test" "-e" "coverage" "-v") +else + CMD=("platformio" "test" "-e" "coverage" "-v" "${EXTRA_ARGS[@]}") +fi + +exec docker run --rm \ + -v "$ROOT_DIR:/src:ro" \ + "$IMAGE_NAME" \ + bash -c "rm -rf /tmp/fw-test && cp -a /src /tmp/fw-test && cd /tmp/fw-test && sed -i '${sed_cmd}' platformio.ini && ${CMD[*]}" diff --git a/boards/me25ls01-4y10td.json b/boards/me25ls01-4y10td.json index 9e1d63265..e0be46d67 100644 --- a/boards/me25ls01-4y10td.json +++ b/boards/me25ls01-4y10td.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Minesemi ME25LS01", diff --git a/boards/meshlink.json b/boards/meshlink.json index a608de88a..51190502e 100644 --- a/boards/meshlink.json +++ b/boards/meshlink.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "MeshLink", diff --git a/boards/mini-epaper-s3.json b/boards/mini-epaper-s3.json new file mode 100644 index 000000000..5140f88be --- /dev/null +++ b/boards/mini-epaper-s3.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "LilyGo Mini-Epaper-S3 (4 MB Flash, 2MB PSRAM)", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.lilygo.cc", + "vendor": "LilyGo" +} diff --git a/boards/minimesh_lite.json b/boards/minimesh_lite.json index 0b8f0b909..c94985531 100644 --- a/boards/minimesh_lite.json +++ b/boards/minimesh_lite.json @@ -31,7 +31,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Minimesh Lite", diff --git a/boards/ms24sf1.json b/boards/ms24sf1.json index 8356e3012..3f65a39c6 100644 --- a/boards/ms24sf1.json +++ b/boards/ms24sf1.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "MINEWSEMI_MS24SF1_SX1262", diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json index 7afce178b..1de7c0186 100644 --- a/boards/nano-g2-ultra.json +++ b/boards/nano-g2-ultra.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "BQ nRF52840", diff --git a/boards/promicro-nrf52840.json b/boards/promicro-nrf52840.json index 99ae3f01e..e7539e0cf 100644 --- a/boards/promicro-nrf52840.json +++ b/boards/promicro-nrf52840.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "ProMicro compatible nRF52840", diff --git a/boards/station-g2.json b/boards/station-g2.json index 871f067aa..f7ce50779 100755 --- a/boards/station-g2.json +++ b/boards/station-g2.json @@ -8,7 +8,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=0" ], diff --git a/boards/tracker-t1000-e.json b/boards/tracker-t1000-e.json index 9e8870041..15bae896f 100644 --- a/boards/tracker-t1000-e.json +++ b/boards/tracker-t1000-e.json @@ -33,7 +33,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed T1000-E", diff --git a/boards/wio-sdk-wm1110.json b/boards/wio-sdk-wm1110.json index f45b030d1..b0dc5326a 100644 --- a/boards/wio-sdk-wm1110.json +++ b/boards/wio-sdk-wm1110.json @@ -25,7 +25,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino", "freertos"], "name": "Seeed WIO WM1110", diff --git a/boards/wio-t1000-s.json b/boards/wio-t1000-s.json index 654a8f73d..3b61f3683 100644 --- a/boards/wio-t1000-s.json +++ b/boards/wio-t1000-s.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed WIO WM1110", diff --git a/boards/wio-tracker-wm1110.json b/boards/wio-tracker-wm1110.json index 37a9186ab..3137f68e7 100644 --- a/boards/wio-tracker-wm1110.json +++ b/boards/wio-tracker-wm1110.json @@ -32,7 +32,8 @@ "connectivity": ["bluetooth"], "debug": { "jlink_device": "nRF52840_xxAA", - "svd_path": "nrf52840.svd" + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" }, "frameworks": ["arduino"], "name": "Seeed WIO WM1110", diff --git a/debian/changelog b/debian/changelog index 38489b074..1e688ce80 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +meshtasticd (2.7.20.0) unstable; urgency=medium + + * Version 2.7.20 + + -- GitHub Actions Wed, 11 Feb 2026 12:19:54 +0000 + meshtasticd (2.7.19.0) unstable; urgency=medium * Version 2.7.19 diff --git a/debian/control b/debian/control index 46c932a80..8e5f17af9 100644 --- a/debian/control +++ b/debian/control @@ -26,7 +26,8 @@ Build-Depends: debhelper-compat (= 13), libx11-dev, libinput-dev, libxkbcommon-x11-dev, - libsqlite3-dev + libsqlite3-dev, + libsdl2-dev Standards-Version: 4.6.2 Homepage: https://github.com/meshtastic/firmware Rules-Requires-Root: no diff --git a/meshtasticd.spec.rpkg b/meshtasticd.spec.rpkg index fc14ede7f..a9eb552d7 100644 --- a/meshtasticd.spec.rpkg +++ b/meshtasticd.spec.rpkg @@ -49,6 +49,7 @@ BuildRequires: pkgconfig(libulfius) BuildRequires: pkgconfig(x11) BuildRequires: pkgconfig(libinput) BuildRequires: pkgconfig(xkbcommon-x11) +BuildRequires: pkgconfig(sdl2) # libbsd is needed on older Fedora/RHEL to provide 'strlcpy' %if 0%{?fedora} >= 39 || 0%{?rhel} >= 10 @@ -59,8 +60,14 @@ BuildRequires: pkgconfig(libbsd-overlay) Requires: systemd-udev +# Declare that this package provides the user/group it creates in %pre +# Required for Fedora 43+ which tracks users/groups as RPM dependencies +Provides: user(%{meshtasticd_user}) +Provides: group(%{meshtasticd_user}) +Provides: group(spi) + %description -Meshtastic daemon for controlling Meshtastic devices. Meshtastic is an off-grid +Meshtastic daemon. Meshtastic is an off-grid text communication platform that uses inexpensive LoRa radios. %prep @@ -151,6 +158,7 @@ fi %license LICENSE %doc README.md %{_bindir}/meshtasticd +%{_bindir}/meshtasticd-start.sh %dir %{_localstatedir}/lib/meshtasticd %{_udevrulesdir}/99-meshtasticd-udev.rules %dir %{_sysconfdir}/meshtasticd diff --git a/platformio.ini b/platformio.ini index f2324968f..eb40a2902 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,11 +50,13 @@ build_flags = -Wno-missing-field-initializers -DRADIOLIB_EXCLUDE_APRS=1 -DRADIOLIB_EXCLUDE_LORAWAN=1 -DMESHTASTIC_EXCLUDE_DROPZONE=1 + -DMESHTASTIC_EXCLUDE_REPLYBOT=1 -DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1 -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 + -DMESHTASTIC_EXCLUDE_STATUS=1 -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -DLED_BUILTIN=-1 #-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now @@ -66,7 +68,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/b34c6817c25d6faabb3a8a162b5d14fb75395433.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.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 @@ -115,12 +117,12 @@ lib_deps = [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - jgromes/RadioLib@7.5.0 + jgromes/RadioLib@7.6.0 [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/48e3a59ddfd746dfbde562a7f447cf9da4f369cf.zip + https://github.com/meshtastic/device-ui/archive/499f3a806d729e49ea5148a1633e9b64ae8c2d2e.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -136,7 +138,7 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit BME280 packageName=adafruit/library/Adafruit BME280 Library adafruit/Adafruit BME280 Library@2.3.0 # renovate: datasource=custom.pio depName=Adafruit DPS310 packageName=adafruit/library/Adafruit DPS310 - adafruit/Adafruit DPS310@1.1.5 + adafruit/Adafruit DPS310@1.1.6 # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library adafruit/Adafruit MCP9808 Library@2.0.2 # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library @@ -144,11 +146,11 @@ lib_deps = # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 adafruit/Adafruit INA219@1.2.3 # renovate: datasource=custom.pio depName=Adafruit MPU6050 packageName=adafruit/library/Adafruit MPU6050 - adafruit/Adafruit MPU6050@2.2.6 + adafruit/Adafruit MPU6050@2.2.9 # renovate: datasource=custom.pio depName=Adafruit LIS3DH packageName=adafruit/library/Adafruit LIS3DH adafruit/Adafruit LIS3DH@1.3.0 # renovate: datasource=custom.pio depName=Adafruit AHTX0 packageName=adafruit/library/Adafruit AHTX0 - adafruit/Adafruit AHTX0@2.0.5 + adafruit/Adafruit AHTX0@2.0.6 # renovate: datasource=custom.pio depName=Adafruit LSM6DS packageName=adafruit/library/Adafruit LSM6DS adafruit/Adafruit LSM6DS@4.7.4 # renovate: datasource=custom.pio depName=Adafruit TSL2591 packageName=adafruit/library/Adafruit TSL2591 Library @@ -156,7 +158,7 @@ lib_deps = # renovate: datasource=custom.pio depName=EmotiBit MLX90632 packageName=emotibit/library/EmotiBit MLX90632 emotibit/EmotiBit MLX90632@1.0.8 # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library - adafruit/Adafruit MLX90614 Library@2.1.5 + adafruit/Adafruit MLX90614 Library@2.1.6 # 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 @@ -178,12 +180,12 @@ lib_deps = # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 dfrobot/DFRobot_BMM150@1.0.0 # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 - adafruit/Adafruit TSL2561@1.1.2 + adafruit/Adafruit TSL2561@1.1.3 # 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] +; Common environmental sensor libraries (not included in native / portduino) +[environmental_extra_common] lib_deps = # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library adafruit/Adafruit BMP3XX Library@2.1.6 @@ -203,40 +205,29 @@ lib_deps = sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 closedcube/ClosedCube OPT3001@1.1.2 + # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master + https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip + # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core + sensirion/Sensirion Core@0.7.3 + # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x + sensirion/Sensirion I2C SCD4x@1.1.0 + # renovate: datasource=custom.pio depName=Sensirion I2C SFA3x packageName=sensirion/library/Sensirion I2C SFA3x + sensirion/Sensirion I2C SFA3x@1.0.0 + # renovate: datasource=custom.pio depName=Sensirion I2C SCD30 packageName=sensirion/library/Sensirion I2C SCD30 + sensirion/Sensirion I2C SCD30@1.0.0 + +; Environmental sensors with BSEC2 (Bosch proprietary IAQ) +[environmental_extra] +lib_deps = + ${environmental_extra_common.lib_deps} # renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2 boschsensortec/bsec2@1.10.2610 # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library boschsensortec/BME68x Sensor Library@1.3.40408 - # renovate: datasource=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.2 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 -; Same as environmental_extra but without BSEC (saves ~3.5KB DRAM for original ESP32 targets) + +; Environmental sensors without BSEC (saves ~3.5KB DRAM for original ESP32 targets) [environmental_extra_no_bsec] lib_deps = - # renovate: datasource=custom.pio depName=Adafruit BMP3XX packageName=adafruit/library/Adafruit BMP3XX Library - adafruit/Adafruit BMP3XX Library@2.1.6 - # renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X - adafruit/Adafruit MAX1704X@1.0.3 - # renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library - adafruit/Adafruit SHTC3 Library@1.0.2 - # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X - adafruit/Adafruit LPS2X@2.0.6 - # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library - adafruit/Adafruit SHT31 Library@2.2.2 - # renovate: datasource=custom.pio depName=Adafruit VEML7700 packageName=adafruit/library/Adafruit VEML7700 Library - adafruit/Adafruit VEML7700 Library@2.1.6 - # renovate: datasource=custom.pio depName=Adafruit SHT4x packageName=adafruit/library/Adafruit SHT4x Library - adafruit/Adafruit SHT4x Library@1.0.5 - # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library - sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 - # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 - closedcube/ClosedCube OPT3001@1.1.2 - # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master - https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip - # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core - sensirion/Sensirion Core@0.7.2 - # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x - sensirion/Sensirion I2C SCD4x@1.1.0 \ No newline at end of file + ${environmental_extra_common.lib_deps} + # renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680 + adafruit/Adafruit BME680 Library@^2.0.5 diff --git a/protobufs b/protobufs index bc63a57f9..a229208f2 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit bc63a57f9e5dba8a7c90ee0bd4a9840862d61f6d +Subproject commit a229208f29a59cf1d8cfa24cbb7567a08f2d1771 diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index 947b1e054..d52b10a53 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -1,19 +1,23 @@ +#ifndef AMBIENTLIGHTINGTHREAD_H +#define AMBIENTLIGHTINGTHREAD_H + #include "Observer.h" #include "configuration.h" +#include "detect/ScanI2C.h" +#include "sleep.h" #ifdef HAS_NCP5623 -#include -NCP5623 rgb; +#include + +#include #endif #ifdef HAS_LP5562 #include -LP5562 rgbw; #endif #ifdef HAS_NEOPIXEL -#include -Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); +#include #endif #ifdef UNPHONE @@ -21,10 +25,24 @@ Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); extern unPhone unphone; #endif -namespace concurrency -{ class AmbientLightingThread : public concurrency::OSThread { + friend class StatusLEDModule; // Let the LEDStatusModule trigger the ambient lighting for notifications and battery status. + friend class ExternalNotificationModule; // Let the ExternalNotificationModule trigger the ambient lighting for notifications. + + private: +#ifdef HAS_NCP5623 + NCP5623 rgb; +#endif + +#ifdef HAS_LP5562 + LP5562 rgbw; +#endif + +#ifdef HAS_NEOPIXEL + Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NEOPIXEL_COUNT, NEOPIXEL_DATA, NEOPIXEL_TYPE); +#endif + public: explicit AmbientLightingThread(ScanI2C::DeviceType type) : OSThread("AmbientLighting") { @@ -36,14 +54,15 @@ class AmbientLightingThread : public concurrency::OSThread moduleConfig.ambient_lighting.led_state = true; #endif #endif - // Uncomment to test module - // moduleConfig.ambient_lighting.led_state = true; - // moduleConfig.ambient_lighting.current = 10; +#if AMBIENT_LIGHTING_TEST + // define to enable test + moduleConfig.ambient_lighting.led_state = true; + moduleConfig.ambient_lighting.current = 10; // Default to a color based on our node number - // moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; - // moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; - // moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; - + moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16; + moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8; + moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF; +#endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) _type = type; if (_type == ScanI2C::DeviceType::NONE) { @@ -53,11 +72,6 @@ class AmbientLightingThread : public concurrency::OSThread } #endif #ifdef HAS_RGB_LED - if (!moduleConfig.ambient_lighting.led_state) { - LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); - disable(); - return; - } LOG_DEBUG("AmbientLighting init"); #ifdef HAS_NCP5623 if (_type == ScanI2C::NCP5623) { @@ -77,7 +91,13 @@ class AmbientLightingThread : public concurrency::OSThread pixels.clear(); // Set all pixel colors to 'off' pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + if (!moduleConfig.ambient_lighting.led_state) { + LOG_DEBUG("AmbientLighting Disable due to moduleConfig.ambient_lighting.led_state OFF"); + disable(); + return; + } + setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) } @@ -91,7 +111,8 @@ class AmbientLightingThread : public concurrency::OSThread #if defined(HAS_NCP5623) || defined(HAS_LP5562) if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); + setLighting(moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) } @@ -148,65 +169,53 @@ class AmbientLightingThread : public concurrency::OSThread return 0; } - void setLighting() + protected: + void setLighting(float current, uint8_t red, uint8_t green, uint8_t blue) { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(current); + rgb.setRed(red); + rgb.setGreen(green); + rgb.setBlue(blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(current); + rgbw.setRed(red); + rgbw.setGreen(green); + rgbw.setBlue(blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%f, red=%d, green=%d, blue=%d", current, red, green, blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue), - 0, NEOPIXEL_COUNT); + pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. -#ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); -#endif #endif pixels.show(); - // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - // moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, - // moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + // LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%f, red=%d, green=%d, blue=%d", + // current, red, green, blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - red); + analogWrite(RGBLED_GREEN, 255 - green); + analogWrite(RGBLED_BLUE, 255 - blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", red, green, blue); #elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, red); + analogWrite(RGBLED_GREEN, green); + analogWrite(RGBLED_BLUE, blue); + LOG_DEBUG("Init Ambient light RGB Common Cathode w/ red=%d, green=%d, blue=%d", red, green, blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + unphone.rgb(red, green, blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", red, green, blue); #endif } }; - -} // namespace concurrency +#endif // AMBIENTLIGHTINGTHREAD_H \ No newline at end of file diff --git a/src/AudioThread.h b/src/AudioThread.h index 23552c421..1129ee087 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -4,6 +4,7 @@ #include "configuration.h" #include "main.h" #include "sleep.h" +#include #ifdef HAS_I2S #include @@ -29,9 +30,9 @@ class AudioThread : public concurrency::OSThread io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif setCPUFast(true); - rtttlFile = new AudioFileSourcePROGMEM(data, len); - i2sRtttl = new AudioGeneratorRTTTL(); - i2sRtttl->begin(rtttlFile, audioOut); + rtttlFile = std::unique_ptr(new AudioFileSourcePROGMEM(data, len)); + i2sRtttl = std::unique_ptr(new AudioGeneratorRTTTL()); + i2sRtttl->begin(rtttlFile.get(), audioOut.get()); } // Also handles actually playing the RTTTL, needs to be called in loop @@ -47,14 +48,10 @@ class AudioThread : public concurrency::OSThread { if (i2sRtttl != nullptr) { i2sRtttl->stop(); - delete i2sRtttl; i2sRtttl = nullptr; } - if (rtttlFile != nullptr) { - delete rtttlFile; - rtttlFile = nullptr; - } + rtttlFile = nullptr; setCPUFast(false); #ifdef T_LORA_PAGER @@ -66,16 +63,14 @@ class AudioThread : public concurrency::OSThread { if (i2sRtttl != nullptr) { i2sRtttl->stop(); - delete i2sRtttl; i2sRtttl = nullptr; } #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, HIGH); #endif - ESP8266SAM *sam = new ESP8266SAM; - sam->Say(audioOut, text); - delete sam; + auto sam = std::unique_ptr(new ESP8266SAM); + sam->Say(audioOut.get(), text); setCPUFast(false); #ifdef T_LORA_PAGER io.digitalWrite(EXPANDS_AMP_EN, LOW); @@ -96,15 +91,15 @@ class AudioThread : public concurrency::OSThread private: void initOutput() { - audioOut = new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S); + audioOut = std::unique_ptr(new AudioOutputI2S(1, AudioOutputI2S::EXTERNAL_I2S)); audioOut->SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK); audioOut->SetGain(0.2); }; - AudioGeneratorRTTTL *i2sRtttl = nullptr; - AudioOutputI2S *audioOut = nullptr; + std::unique_ptr i2sRtttl = nullptr; + std::unique_ptr audioOut = nullptr; - AudioFileSourcePROGMEM *rtttlFile = nullptr; + std::unique_ptr rtttlFile = nullptr; }; #endif diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 680aec929..4ea4a95ac 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -89,22 +89,14 @@ class BluetoothStatus : public Status case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, LOW); -#else - digitalWrite(BLE_LED, HIGH); -#endif + digitalWrite(BLE_LED, LED_STATE_ON); #endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif break; } diff --git a/src/Led.cpp b/src/Led.cpp deleted file mode 100644 index 6406cd2f7..000000000 --- a/src/Led.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "Led.h" -#include "PowerMon.h" -#include "main.h" -#include "power.h" - -GpioVirtPin ledForceOn, ledBlink; - -#if defined(LED_PIN) -// Most boards have a GPIO for LED control -static GpioHwPin ledRawHwPin(LED_PIN); -#else -static GpioVirtPin ledRawHwPin; // Dummy pin for no hardware -#endif - -#if LED_STATE_ON == 0 -static GpioVirtPin ledHwPin; -static GpioNotTransformer ledInverter(&ledHwPin, &ledRawHwPin); -#else -static GpioPin &ledHwPin = ledRawHwPin; -#endif - -#if defined(HAS_PMU) -/** - * A GPIO controlled by the PMU - */ -class GpioPmuPin : public GpioPin -{ - public: - void set(bool value) - { - if (pmu_found && PMU) { - // blink the axp led - PMU->setChargingLedMode(value ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); - } - } -} ledPmuHwPin; - -// In some cases we need to drive a PMU LED and a normal LED -static GpioSplitter ledFinalPin(&ledHwPin, &ledPmuHwPin); -#else -static GpioPin &ledFinalPin = ledHwPin; -#endif - -#ifdef USE_POWERMON -/** - * We monitor changes to the LED drive output because we use that as a sanity test in our power monitor stuff. - */ -class MonitoredLedPin : public GpioPin -{ - public: - void set(bool value) - { - if (powerMon) { - if (value) - powerMon->setState(meshtastic_PowerMon_State_LED_On); - else - powerMon->clearState(meshtastic_PowerMon_State_LED_On); - } - ledFinalPin.set(value); - } -} monitoredLedPin; -#else -static GpioPin &monitoredLedPin = ledFinalPin; -#endif - -static GpioBinaryTransformer ledForcer(&ledForceOn, &ledBlink, &monitoredLedPin, GpioBinaryTransformer::Or); \ No newline at end of file diff --git a/src/Led.h b/src/Led.h deleted file mode 100644 index 68833e041..000000000 --- a/src/Led.h +++ /dev/null @@ -1,7 +0,0 @@ -#include "GpioLogic.h" -#include "configuration.h" - -/** - * ledForceOn and ledForceOff both override the normal ledBlinker behavior (which is controlled by main) - */ -extern GpioVirtPin ledForceOn, ledBlink; \ No newline at end of file diff --git a/src/Power.cpp b/src/Power.cpp index b211d760e..ea4fcf42a 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -459,6 +459,8 @@ class AnalogBatteryLevel : public HasBatteryLevel } // if it's not HIGH - check the battery #endif + // If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it. + return false; // technically speaking this should work for all(?) NRF52 boards // but needs testing across multiple devices. NRF52 USB would not even work if @@ -690,7 +692,9 @@ bool Power::setup() bool found = false; if (axpChipInit()) { found = true; - } else if (lipoInit()) { + } else if (cw2015Init()) { + found = true; + } else if (max17048Init()) { found = true; } else if (lipoChargerInit()) { found = true; @@ -700,11 +704,11 @@ bool Power::setup() found = true; } else if (analogInit()) { found = true; - } - + } else { #ifdef NRF_APM - found = true; + found = true; #endif + } #ifdef EXT_PWR_DETECT attachInterrupt( EXT_PWR_DETECT, @@ -842,8 +846,10 @@ void Power::readPowerStatus() if (batteryLevel) { hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse; +#ifndef NRF_APM usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse; isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse; +#endif if (hasBattery) { batteryVoltageMv = batteryLevel->getBattVoltage(); // If the AXP192 returns a valid battery percentage, use it @@ -1319,7 +1325,7 @@ bool Power::axpChipInit() /** * Wrapper class for an I2C MAX17048 Lipo battery sensor. */ -class LipoBatteryLevel : public HasBatteryLevel +class MAX17048BatteryLevel : public HasBatteryLevel { private: MAX17048Singleton *max17048 = nullptr; @@ -1367,18 +1373,18 @@ class LipoBatteryLevel : public HasBatteryLevel virtual bool isCharging() override { return max17048->isBatteryCharging(); } }; -LipoBatteryLevel lipoLevel; +MAX17048BatteryLevel max17048Level; /** * Init the Lipo battery level sensor */ -bool Power::lipoInit() +bool Power::max17048Init() { - bool result = lipoLevel.runOnce(); - LOG_DEBUG("Power::lipoInit lipo sensor is %s", result ? "ready" : "not ready yet"); + bool result = max17048Level.runOnce(); + LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet"); if (!result) return false; - batteryLevel = &lipoLevel; + batteryLevel = &max17048Level; return true; } @@ -1386,7 +1392,88 @@ bool Power::lipoInit() /** * The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel */ -bool Power::lipoInit() +bool Power::max17048Init() +{ + return false; +} +#endif + +#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015 + +class CW2015BatteryLevel : public AnalogBatteryLevel +{ + public: + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override + { + int data = -1; + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x04); + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { + data = Wire.read(); + } + } + return data; + } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override + { + uint16_t mv = 0; + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x02); + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) { + mv = Wire.read(); + mv <<= 8; + mv |= Wire.read(); + // Voltage is read in 305uV units, convert to mV + mv = mv * 305 / 1000; + } + } + return mv; + } +}; + +CW2015BatteryLevel cw2015Level; + +/** + * Init the CW2015 battery level sensor + */ +bool Power::cw2015Init() +{ + + Wire.beginTransmission(CW2015_ADDR); + uint8_t getInfo[] = {0x0a, 0x00}; + Wire.write(getInfo, 2); + Wire.endTransmission(); + delay(10); + Wire.beginTransmission(CW2015_ADDR); + Wire.write(0x00); + bool result = false; + if (Wire.endTransmission() == 0) { + if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) { + uint8_t data = Wire.read(); + LOG_DEBUG("CW2015 init read data: 0x%x", data); + if (data == 0x73) { + result = true; + batteryLevel = &cw2015Level; + } + } + } + return result; +} + +#else +/** + * The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::cw2015Init() { return false; } diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 9f8097b84..2153dbfd5 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -9,13 +9,13 @@ */ #include "PowerFSM.h" #include "Default.h" -#include "Led.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" #include "configuration.h" #include "graphics/Screen.h" #include "main.h" +#include "modules/StatusLEDModule.h" #include "sleep.h" #include "target_specific.h" @@ -103,7 +103,7 @@ static void lsIdle() uint32_t sleepTime = SLEEP_TIME; powerMon->setState(meshtastic_PowerMon_State_CPU_LightSleep); - ledBlink.set(false); // Never leave led on while in light sleep + statusLEDModule->setPowerLED(false); esp_sleep_source_t wakeCause2 = doLightSleep(sleepTime * 1000LL); powerMon->clearState(meshtastic_PowerMon_State_CPU_LightSleep); @@ -111,7 +111,7 @@ static void lsIdle() case ESP_SLEEP_WAKEUP_TIMER: // Normal case: timer expired, we should just go back to sleep ASAP - ledBlink.set(true); // briefly turn on led + statusLEDModule->setPowerLED(true); wakeCause2 = doLightSleep(100); // leave led on for 1ms secsSlept += sleepTime; @@ -146,7 +146,7 @@ static void lsIdle() } } else { // Time to stop sleeping! - ledBlink.set(false); + statusLEDModule->setPowerLED(false); LOG_INFO("Reached ls_secs, service loop()"); powerFSM.trigger(EVENT_WAKE_TIMER); } diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index e15d56912..672c8334c 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -227,34 +227,21 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_ isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); #endif if (isBleConnected) { - char *message; - size_t initialLen; - size_t len; - initialLen = strlen(format); - message = new char[initialLen + 1]; - len = vsnprintf(message, initialLen + 1, format, arg); - if (len > initialLen) { - delete[] message; - message = new char[len + 1]; - vsnprintf(message, len + 1, format, arg); - } auto thread = concurrency::OSThread::currentThread; meshtastic_LogRecord logRecord = meshtastic_LogRecord_init_zero; logRecord.level = getLogLevel(logLevel); - strcpy(logRecord.message, message); + vsprintf(logRecord.message, format, arg); if (thread) strcpy(logRecord.source, thread->ThreadName.c_str()); logRecord.time = getValidTime(RTCQuality::RTCQualityDevice, true); - uint8_t *buffer = new uint8_t[meshtastic_LogRecord_size]; - size_t size = pb_encode_to_bytes(buffer, meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); + auto buffer = std::unique_ptr(new uint8_t[meshtastic_LogRecord_size]); + size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord); #ifdef ARCH_ESP32 - nimbleBluetooth->sendLog(buffer, size); + nimbleBluetooth->sendLog(buffer.get(), size); #elif defined(ARCH_NRF52) - nrf52Bluetooth->sendLog(buffer, size); + nrf52Bluetooth->sendLog(buffer.get(), size); #endif - delete[] message; - delete[] buffer; } } #else @@ -292,8 +279,8 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) // append \n to format size_t len = strlen(format); - char *newFormat = new char[len + 2]; - strcpy(newFormat, format); + auto newFormat = std::unique_ptr(new char[len + 2]); + strcpy(newFormat.get(), format); newFormat[len] = '\n'; newFormat[len + 1] = '\0'; @@ -310,23 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) va_end(arg); } if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { - delete[] newFormat; return; } } if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; return; } else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { - delete[] newFormat; return; } else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { - delete[] newFormat; return; } #endif if (moduleConfig.serial.override_console_serial_port && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { - delete[] newFormat; return; } @@ -338,11 +320,19 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #endif va_list arg; + va_list arg_copy; + va_start(arg, format); - log_to_serial(logLevel, newFormat, arg); - log_to_syslog(logLevel, newFormat, arg); - log_to_ble(logLevel, newFormat, arg); + va_copy(arg_copy, arg); + log_to_serial(logLevel, newFormat.get(), arg_copy); + va_end(arg_copy); + + va_copy(arg_copy, arg); + log_to_syslog(logLevel, newFormat.get(), arg_copy); + va_end(arg_copy); + + log_to_ble(logLevel, newFormat.get(), arg); va_end(arg); #ifdef HAS_FREE_RTOS @@ -352,7 +342,6 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...) #endif } - delete[] newFormat; return; } diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index dd2acb599..e24aa3c57 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -35,6 +35,8 @@ void consoleInit() #if defined(SERIAL_HAS_ON_RECEIVE) // onReceive does only exist for HardwareSerial not for USB CDC serial Port.onReceive([sc]() { sc->rxInt(); }); +#else + (void)sc; #endif DEBUG_PORT.rpInit(); // Simply sets up semaphore } diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 0e4e31d9b..29aff32a5 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit void NotifiedWorkerThread::checkNotification() { - auto n = notification; - notification = 0; // clear notification + // Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification + // after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes + // in.) + auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe if (n) { onNotify(n); } diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index 7a150b0b0..166b9ea65 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -1,6 +1,7 @@ #pragma once #include "OSThread.h" +#include namespace concurrency { @@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread /** * The notification that was most recently used to wake the thread. Read from runOnce() */ - uint32_t notification = 0; + std::atomic notification{0}; public: NotifiedWorkerThread(const char *name) : OSThread(name) {} diff --git a/src/concurrency/Periodic.h b/src/concurrency/Periodic.h index db07145a6..8576be7ea 100644 --- a/src/concurrency/Periodic.h +++ b/src/concurrency/Periodic.h @@ -1,24 +1,29 @@ #pragma once +#include +#include + #include "concurrency/OSThread.h" namespace concurrency { /** - * @brief Periodically invoke a callback. This just provides C-style callback conventions - * rather than a virtual function - FIXME, remove? + * @brief Periodically invoke a callback. + * Supports both legacy function pointers and modern callables. */ class Periodic : public OSThread { - int32_t (*callback)(); - public: // callback returns the period for the next callback invocation (or 0 if we should no longer be called) - Periodic(const char *name, int32_t (*_callback)()) : OSThread(name), callback(_callback) {} + Periodic(const char *name, int32_t (*cb)()) : OSThread(name), callback(cb) {} + Periodic(const char *name, std::function cb) : OSThread(name), callback(std::move(cb)) {} protected: - int32_t runOnce() override { return callback(); } + int32_t runOnce() override { return callback ? callback() : 0; } + + private: + std::function callback; }; } // namespace concurrency diff --git a/src/configuration.h b/src/configuration.h index f11134345..d93147192 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -217,6 +217,7 @@ along with this program. If not, see . #define SHTC3_ADDR 0x70 #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D +#define SFA30_ADDR 0x5D #define SHT31_4x_ADDR 0x44 #define SHT31_4x_ADDR_ALT 0x45 #define PMSA003I_ADDR 0x12 @@ -233,6 +234,7 @@ along with this program. If not, see . #define NAU7802_ADDR 0x2A #define MAX30102_ADDR 0x57 #define SCD4X_ADDR 0x62 +#define CW2015_ADDR 0x62 #define MLX90614_ADDR_DEF 0x5A #define CGRADSENS_ADDR 0x66 #define LTR390UV_ADDR 0x53 @@ -241,6 +243,8 @@ along with this program. If not, see . #define BQ27220_ADDR 0x55 // same address as TDECK_KB #define BQ25896_ADDR 0x6B #define LTR553ALS_ADDR 0x23 +#define SEN5X_ADDR 0x69 +#define SCD30_ADDR 0x61 // ----------------------------------------------------------------------------- // ACCELEROMETER diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 4795d2abc..3389cce91 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -43,8 +43,8 @@ ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAQI() const { - ScanI2C::DeviceType types[] = {PMSA003I, SCD4X}; - return firstOfOrNONE(2, types); + ScanI2C::DeviceType types[] = {PMSA003I, SEN5X, SCD4X, SFA30}; + return firstOfOrNONE(4, types); } ScanI2C::FoundDevice ScanI2C::firstRGBLED() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 9a64bebac..1a81d2bb7 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -89,7 +89,11 @@ class ScanI2C BH1750, DA217, CHSC6X, - CST226SE + CST226SE, + SEN5X, + SFA30, + CW2015, + SCD30 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 4d992ed65..0465cb70c 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -1,4 +1,6 @@ #include "ScanI2CTwoWire.h" +#include "configuration.h" +#include "detect/ScanI2C.h" #if !MESHTASTIC_EXCLUDE_I2C @@ -8,6 +10,7 @@ #endif #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) #include "meshUtils.h" // vformat + #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -114,6 +117,64 @@ uint16_t ScanI2CTwoWire::getRegisterValue(const ScanI2CTwoWire::RegisterLocation return value; } +bool ScanI2CTwoWire::i2cCommandResponseLength(ScanI2C::DeviceAddress addr, uint16_t command, uint8_t expectedLength) const +{ + TwoWire *i2cBus = fetchI2CBus(addr); + i2cBus->beginTransmission(addr.address); + if (command > 0xFF) { + i2cBus->write((uint8_t)(command >> 8)); + } + i2cBus->write((uint8_t)(command & 0xFF)); + if (i2cBus->endTransmission() != 0) { + return false; + } + delay(20); + uint8_t received = i2cBus->requestFrom(addr.address, expectedLength); + bool match = (received == expectedLength); + while (i2cBus->available()) + i2cBus->read(); + return match; +} + +/// for SEN5X detection +// Note, this code needs to be called before setting the I2C bus speed +// for the screen at high speed. The speed needs to be at 100kHz, otherwise +// detection will not work +String readSEN5xProductName(TwoWire *i2cBus, uint8_t address) +{ + uint8_t cmd[] = {0xD0, 0x14}; + uint8_t response[48] = {0}; + + i2cBus->beginTransmission(address); + i2cBus->write(cmd, 2); + if (i2cBus->endTransmission() != 0) + return ""; + + delay(20); + if (i2cBus->requestFrom(address, (uint8_t)48) != 48) + return ""; + + for (int i = 0; i < 48 && i2cBus->available(); ++i) { + response[i] = i2cBus->read(); + } + + char productName[33] = {0}; + int j = 0; + for (int i = 0; i < 48 && j < 32; i += 3) { + if (response[i] >= 32 && response[i] <= 126) + productName[j++] = response[i]; + else + break; + + if (response[i + 1] >= 32 && response[i + 1] <= 126) + productName[j++] = response[i + 1]; + else + break; + } + + return String(productName); +} + #define SCAN_SIMPLE_CASE(ADDR, T, ...) \ case ADDR: \ logFoundDevice(__VA_ARGS__); \ @@ -392,8 +453,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { type = OPT3001; logFoundDevice("OPT3001", (uint8_t)addr.address); - } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 6) != - 0) { // unique SHT4x serial number (6 bytes inc. CRC) + } else if (i2cCommandResponseLength(addr, 0x89, 6)) { // SHT4x serial number (6 bytes inc. CRC) type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else { @@ -418,6 +478,19 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; case LPS22HB_ADDR_ALT: + // SFA30 detection: send 2-byte command 0xD060 (Get Device Marking) and check for 48-byte response + if (i2cCommandResponseLength(addr, 0xD060, 48)) { + type = SFA30; + logFoundDevice("SFA30", (uint8_t)addr.address); + break; + } + // Fallback: LPS22HB detection at alternate address using WHO_AM_I register (0x0F == 0xB1) + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); + if (registerValue == 0xB1) { + type = LPS22HB; + logFoundDevice("LPS22HB", (uint8_t)addr.address); + } + break; SCAN_SIMPLE_CASE(LPS22HB_ADDR, LPS22HB, "LPS22HB", (uint8_t)addr.address) SCAN_SIMPLE_CASE(QMC6310U_ADDR, QMC6310U, "QMC6310U", (uint8_t)addr.address) @@ -510,6 +583,7 @@ 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(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address); case CST328_ADDR: // Do we have the CST328 or the CST226SE registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1); @@ -543,7 +617,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) break; SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); + case SCD4X_ADDR: { + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x8), 1); + if (registerValue == 0x18) { + logFoundDevice("CW2015", (uint8_t)addr.address); + type = CW2015; + } else { + logFoundDevice("SCD4X", (uint8_t)addr.address); + type = SCD4X; + } + break; + } SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); @@ -570,8 +654,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; - case ICM20948_ADDR: // same as BMX160_ADDR + case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR case ICM20948_ADDR_ALT: // same as MPU6050_ADDR + // ICM20948 Register check registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); #ifdef HAS_ICM20948 type = ICM20948; @@ -582,14 +667,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = ICM20948; logFoundDevice("ICM20948", (uint8_t)addr.address); break; - } else if (addr.address == BMX160_ADDR) { - type = BMX160; - logFoundDevice("BMX160", (uint8_t)addr.address); - break; } else { - type = MPU6050; - logFoundDevice("MPU6050", (uint8_t)addr.address); - break; + String prod = ""; + prod = readSEN5xProductName(i2cBus, addr.address); + if (prod.startsWith("SEN55")) { + type = SEN5X; + logFoundDevice("Sensirion SEN55", addr.address); + break; + } else if (prod.startsWith("SEN54")) { + type = SEN5X; + logFoundDevice("Sensirion SEN54", addr.address); + break; + } else if (prod.startsWith("SEN50")) { + type = SEN5X; + logFoundDevice("Sensirion SEN50", addr.address); + break; + } + if (addr.address == BMX160_ADDR) { + type = BMX160; + logFoundDevice("BMX160", (uint8_t)addr.address); + break; + } else { + type = MPU6050; + logFoundDevice("MPU6050", (uint8_t)addr.address); + break; + } } break; diff --git a/src/detect/ScanI2CTwoWire.h b/src/detect/ScanI2CTwoWire.h index c5b791920..841a8b946 100644 --- a/src/detect/ScanI2CTwoWire.h +++ b/src/detect/ScanI2CTwoWire.h @@ -55,6 +55,8 @@ class ScanI2CTwoWire : public ScanI2C uint16_t getRegisterValue(const RegisterLocation &, ResponseWidth, bool) const; + bool i2cCommandResponseLength(DeviceAddress addr, uint16_t command, uint8_t expectedLength) const; + DeviceType probeOLED(ScanI2C::DeviceAddress) const; static void logFoundDevice(const char *device, uint8_t address); diff --git a/src/detect/reClockI2C.cpp b/src/detect/reClockI2C.cpp new file mode 100644 index 000000000..60cd3c808 --- /dev/null +++ b/src/detect/reClockI2C.cpp @@ -0,0 +1,31 @@ +#include "reClockI2C.h" +#include "ScanI2CTwoWire.h" + +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force) +{ + + uint32_t currentClock = 0; + + /* See https://github.com/arduino/Arduino/issues/11457 + Currently, only ESP32 can getClock() + While all cores can setClock() + https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 + https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 + https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 + For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) + we need to reclock I2C and set it back to the previous desired speed. + Only for cases where we can know OR predefine the speed, we can do this. + */ + +// TODO add getClock function or return a predefined clock speed per variant? +#ifdef CAN_RECLOCK_I2C + currentClock = i2cBus->getClock(); +#endif + + if ((currentClock != desiredClock) || force) { + LOG_DEBUG("Changing I2C clock to %u", desiredClock); + i2cBus->setClock(desiredClock); + } + + return currentClock; +} diff --git a/src/detect/reClockI2C.h b/src/detect/reClockI2C.h index 689e88d6f..9c53efc4f 100644 --- a/src/detect/reClockI2C.h +++ b/src/detect/reClockI2C.h @@ -1,41 +1,11 @@ -#ifdef CAN_RECLOCK_I2C + +#ifndef RECLOCK_I2C_ +#define RECLOCK_I2C_ + #include "ScanI2CTwoWire.h" +#include +#include -uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus) -{ +uint32_t reClockI2C(uint32_t desiredClock, TwoWire *i2cBus, bool force); - uint32_t currentClock; - - /* See https://github.com/arduino/Arduino/issues/11457 - Currently, only ESP32 can getClock() - While all cores can setClock() - https://github.com/sandeepmistry/arduino-nRF5/blob/master/libraries/Wire/Wire.h#L50 - https://github.com/earlephilhower/arduino-pico/blob/master/libraries/Wire/src/Wire.h#L60 - https://github.com/stm32duino/Arduino_Core_STM32/blob/main/libraries/Wire/src/Wire.h#L103 - For cases when I2C speed is different to the ones defined by sensors (see defines in sensor classes) - we need to reclock I2C and set it back to the previous desired speed. - Only for cases where we can know OR predefine the speed, we can do this. - */ - -#ifdef ARCH_ESP32 - currentClock = i2cBus->getClock(); -#elif defined(ARCH_NRF52) - // TODO add getClock function or return a predefined clock speed per variant? - return 0; -#elif defined(ARCH_RP2040) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#elif defined(ARCH_STM32WL) - // TODO add getClock function or return a predefined clock speed per variant - return 0; -#else - return 0; -#endif - - if (currentClock != desiredClock) { - LOG_DEBUG("Changing I2C clock to %u", desiredClock); - i2cBus->setClock(desiredClock); - } - return currentClock; -} #endif diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 13e5c32d1..2beaeb127 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -52,7 +52,7 @@ SerialUART *GPS::_serial_gps = &GPS_SERIAL_PORT; HardwareSerial *GPS::_serial_gps = nullptr; #endif -GPS *gps = nullptr; +std::unique_ptr gps = nullptr; static GPSUpdateScheduling scheduling; @@ -127,7 +127,7 @@ static int32_t gpsSwitch() return 1000; } -static concurrency::Periodic *gpsPeriodic; +static std::unique_ptr gpsPeriodic; #endif static void UBXChecksum(uint8_t *message, size_t length) @@ -1485,7 +1485,7 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector 2048) bufferSize = 2048; - char *response = new char[bufferSize](); // Dynamically allocate based on baud rate + auto response = std::unique_ptr(new char[bufferSize]); // Dynamically allocate based on baud rate uint16_t responseLen = 0; unsigned long start = millis(); while (millis() - start < timeout) { @@ -1501,19 +1501,18 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) { // check if we can see our chips for (const auto &chipInfo : responseMap) { - if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { + if (strstr(response.get(), chipInfo.detectionString.c_str()) != nullptr) { #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response.get()); #endif LOG_INFO("%s detected", chipInfo.chipName.c_str()); - delete[] response; // Cleanup before return return chipInfo.driver; } } } if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { #ifdef GPS_DEBUG - LOG_DEBUG(response); + LOG_DEBUG(response.get()); #endif // Reset the response buffer for the next potential message responseLen = 0; @@ -1522,13 +1521,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector GPS::createGps() { int8_t _rx_gpio = config.position.rx_gpio; int8_t _tx_gpio = config.position.tx_gpio; @@ -1553,7 +1551,7 @@ GPS *GPS::createGps() if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all return nullptr; - GPS *new_gps = new GPS; + auto new_gps = std::unique_ptr(new GPS()); new_gps->rx_gpio = _rx_gpio; new_gps->tx_gpio = _tx_gpio; @@ -1581,7 +1579,7 @@ GPS *GPS::createGps() #ifdef PIN_GPS_SWITCH // toggle GPS via external GPIO switch pinMode(PIN_GPS_SWITCH, INPUT); - gpsPeriodic = new concurrency::Periodic("GPSSwitch", gpsSwitch); + gpsPeriodic = std::unique_ptr(new concurrency::Periodic("GPSSwitch", gpsSwitch)); #endif // Currently disabled per issue #525 (TinyGPS++ crash bug) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index fcbf361d5..8d63ce82f 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -2,6 +2,8 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_GPS +#include + #include "GPSStatus.h" #include "GpioLogic.h" #include "Observer.h" @@ -118,7 +120,7 @@ class GPS : private concurrency::OSThread // Creates an instance of the GPS class. // Returns the new instance or null if the GPS is not present. - static GPS *createGps(); + static std::unique_ptr createGps(); // Wake the GPS hardware - ready for an update void up(); @@ -256,5 +258,5 @@ class GPS : private concurrency::OSThread uint8_t fixeddelayCtr = 0; }; -extern GPS *gps; +extern std::unique_ptr gps; #endif // Exclude GPS diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 3bca6f6ec..e67bef53e 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -72,11 +72,13 @@ RTCSetResult readFromRTC() #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { + SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { + SensorPCF85063 rtc; + #endif uint32_t now = millis(); - SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); @@ -240,10 +242,12 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #elif defined(PCF8563_RTC) || defined(PCF85063_RTC) #if defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { + SensorPCF8563 rtc; #elif defined(PCF85063_RTC) if (rtc_found.address == PCF85063_RTC) { + SensorPCF85063 rtc; + #endif - SensorRtcHelper rtc; #if WIRE_INTERFACES_COUNT == 2 rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); @@ -308,7 +312,7 @@ const char *RtcName(RTCQuality quality) * @param t The time to potentially set the RTC to. * @return True if the RTC was set to the provided time, false otherwise. */ -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) +RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t) { /* Convert to unix time The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 diff --git a/src/gps/RTC.h b/src/gps/RTC.h index cf6db0239..16ecd8245 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -41,7 +41,7 @@ extern uint32_t lastSetFromPhoneNtpOrGps; /// If we haven't yet set our RTC this boot, set it from a GPS derived time RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); -RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t); +RTCSetResult perhapsSetRTC(RTCQuality q, const struct tm &t); /// Return a string name for the quality const char *RtcName(RTCQuality quality); diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 1678da793..c05864349 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -259,6 +259,17 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(3); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } +#elif defined(MINI_EPAPER_S3) + spi1 = new SPIClass(HSPI); + spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS); + + // Create GxEPD2 objects + auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + + // Init GxEPD2 + adafruitDisplay->init(); + adafruitDisplay->setRotation(1); #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) // Detect display model, before starting SPI diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index f5418b069..14adeda12 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -93,7 +93,8 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \ + defined(MINI_EPAPER_S3) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/EInkDynamicDisplay.cpp b/src/graphics/EInkDynamicDisplay.cpp index 8e4adf87e..892a4a885 100644 --- a/src/graphics/EInkDynamicDisplay.cpp +++ b/src/graphics/EInkDynamicDisplay.cpp @@ -10,7 +10,7 @@ EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDI { // If tracking ghost pixels, grab memory #ifdef EINK_LIMIT_GHOSTING_PX - dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros + dirtyPixels = std::unique_ptr(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros #endif } @@ -19,7 +19,7 @@ EInkDynamicDisplay::~EInkDynamicDisplay() { // If we were tracking ghost pixels, free the memory #ifdef EINK_LIMIT_GHOSTING_PX - delete[] dirtyPixels; + dirtyPixels = nullptr; #endif } @@ -454,7 +454,7 @@ void EInkDynamicDisplay::checkExcessiveGhosting() void EInkDynamicDisplay::resetGhostPixelTracking() { // Copy the current frame into dirtyPixels[] from the display buffer - memcpy(dirtyPixels, EInkDisplay::buffer, EInkDisplay::displayBufferSize); + memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize); } #endif // EINK_LIMIT_GHOSTING_PX diff --git a/src/graphics/EInkDynamicDisplay.h b/src/graphics/EInkDynamicDisplay.h index d5e29e3f0..b03061873 100644 --- a/src/graphics/EInkDynamicDisplay.h +++ b/src/graphics/EInkDynamicDisplay.h @@ -1,6 +1,7 @@ #pragma once #include "configuration.h" +#include #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) @@ -116,11 +117,11 @@ class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWo // Optional - track ghosting, pixel by pixel // May 2024: no longer used by any display. Kept for possible future use. #ifdef EINK_LIMIT_GHOSTING_PX - void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh - void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit - void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. - uint8_t *dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) - uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use + void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh + void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit + void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display. + std::unique_ptr dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem) + uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use #endif // Conditional - async full refresh - only with modified meshtastic/GxEPD2 diff --git a/src/graphics/NeoPixel.h b/src/graphics/NeoPixel.h deleted file mode 100644 index dde74366e..000000000 --- a/src/graphics/NeoPixel.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifdef HAS_NEOPIXEL -#include -extern Adafruit_NeoPixel pixels; -#endif \ No newline at end of file diff --git a/src/graphics/NomadStarLED.h b/src/graphics/NomadStarLED.h index 0633a577e..6633db0c8 100644 --- a/src/graphics/NomadStarLED.h +++ b/src/graphics/NomadStarLED.h @@ -1,4 +1,6 @@ #ifdef HAS_LP5562 +#include + #include extern LP5562 rgbw; diff --git a/src/graphics/RAKled.h b/src/graphics/RAKled.h deleted file mode 100644 index 659ea9b72..000000000 --- a/src/graphics/RAKled.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifdef HAS_NCP5623 -#include -extern NCP5623 rgb; - -#endif \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 111a47f7c..c76019fd3 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -875,6 +875,10 @@ int32_t Screen::runOnce() break; case Cmd::STOP_ALERT_FRAME: NotificationRenderer::pauseBanner = false; + // Return from one-off alert mode back to regular frames. + if (!showingNormalScreen && NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } break; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 8f06fcf9f..b86f3e32c 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -221,7 +221,6 @@ 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, minute, second; graphics::decomposeTime(rtc_sec, hour, minute, second); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); @@ -428,39 +427,33 @@ const int *getTextPositions(OLEDDisplay *display) // ************************* void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) { - bool drawConnectionState = false; - if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || - service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET || - service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) { - drawConnectionState = true; - } + if (!isAPIConnected(service->api_state)) + return; - if (drawConnectionState) { - 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); + 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); - for (int yy = 0; yy < connection_icon_height; ++yy) { - const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; - for (int xx = 0; xx < connection_icon_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); - } + for (int yy = 0; yy < connection_icon_height; ++yy) { + const uint8_t *rowPtr = connection_icon + yy * bytesPerRow; + for (int xx = 0; xx < connection_icon_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(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, - connection_icon); } + + } else { + display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, + connection_icon); } } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index a8ecdfada..35e767056 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -63,4 +63,18 @@ bool isAllowedPunctuation(char c); std::string sanitizeString(const std::string &input); +static inline bool isAPIConnected(uint8_t state) +{ + static constexpr bool connectedStates[] = { + /* STATE_NONE */ false, + /* STATE_BLE */ true, + /* STATE_WIFI */ true, + /* STATE_SERIAL */ true, + /* STATE_PACKET */ true, + /* STATE_HTTP */ true, + /* STATE_ETH */ true, + }; + return state < sizeof(connectedStates) ? connectedStates[state] : false; +} + } // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 12fac4f34..1c2eb72d4 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1209,8 +1209,8 @@ void TFTDisplay::display(bool fromBlank) bool somethingChanged = false; // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step - colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); - colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + colorTftMesh = __builtin_bswap16(TFT_MESH); + colorTftBlack = __builtin_bswap16(TFT_BLACK); y = 0; while (y < displayHeight) { diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp index 0a1c23341..02450efa3 100644 --- a/src/graphics/TimeFormatters.cpp +++ b/src/graphics/TimeFormatters.cpp @@ -110,14 +110,14 @@ void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, ui uint32_t secs = (uptimeMillis % 60000) / 1000; if (days) { - snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours); + snprintf(uptimeStr, maxLength, "%s%ud %uh", prefix, days, hours); } else if (hours) { - snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins); + snprintf(uptimeStr, maxLength, "%s%uh %um", prefix, hours, mins); } else if (!includeSecs) { - snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins); + snprintf(uptimeStr, maxLength, "%s%um", prefix, mins); } else if (mins) { - snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs); + snprintf(uptimeStr, maxLength, "%s%um %us", prefix, mins, secs); } else { - snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs); + snprintf(uptimeStr, maxLength, "%s%us", prefix, secs); } -} +} \ No newline at end of file diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index a24f5b15c..52f0195b3 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -429,6 +429,10 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool c = c - 'a' + 'A'; } keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + // Show the common "/" pairing next to "?" like on a real keyboard + if (key.type == VK_CHAR && key.character == '?') { + keyText = "?/"; + } } int textWidth = display->getStringWidth(keyText.c_str()); @@ -518,9 +522,13 @@ char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) char c = key.character; - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); + // Long-press: letters become uppercase; for "?" provide "/" like a typical keyboard + if (isLongPress) { + if (c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } else if (c == '?') { + c = '/'; + } } return c; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 2dca38d66..2069c71ec 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -663,7 +663,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line <= 5)) { // Only show uptime if the screen can show it char uptimeStr[32] = ""; - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); textWidth = display->getStringWidth(uptimeStr); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], uptimeStr); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 6d29e9f7f..f57c39512 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // namespace -menuHandler::screenMenus menuHandler::menuQueue = menu_none; +menuHandler::screenMenus menuHandler::menuQueue = MenuNone; uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; @@ -66,7 +66,7 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 }; + enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; @@ -74,14 +74,14 @@ void menuHandler::loraMenu() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action - } else if (selected == device_role_picker) { - menuHandler::menuQueue = menuHandler::device_role_picker; - } else if (selected == radio_preset_picker) { - menuHandler::menuQueue = menuHandler::radio_preset_picker; - } else if (selected == frequency_slot) { - menuHandler::menuQueue = menuHandler::frequency_slot; - } else if (selected == lora_picker) { - menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == DeviceRolePicker) { + menuHandler::menuQueue = menuHandler::DeviceRolePicker; + } else if (selected == RadioPresetPicker) { + menuHandler::menuQueue = menuHandler::RadioPresetPicker; + } else if (selected == FrequencySlot) { + menuHandler::menuQueue = menuHandler::FrequencySlot; + } else if (selected == LoraPicker) { + menuHandler::menuQueue = menuHandler::LoraPicker; } }; screen->showOverlayBanner(bannerOptions); @@ -102,7 +102,7 @@ void menuHandler::OnboardMessage() bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { - menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker; screen->runNow(); }; screen->showOverlayBanner(bannerOptions); @@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } -void menuHandler::DeviceRolePicker() +void menuHandler::deviceRolePicker() { static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; enum optionsNumbers { @@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker() bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } else if (selected == devicerole_client) { @@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } @@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker() screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() +void menuHandler::radioPresetPicker() { static const RadioPresetOption presetOptions[] = { {"Back", OptionsAction::Back}, @@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker() auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } @@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker() screen->showOverlayBanner(bannerOptions); } -void menuHandler::TwelveHourPicker() +void menuHandler::twelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; @@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); } else if (selected == twelve) { config.display.use_12h_clock = true; @@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::functionshowOverlayBanner(confirmBanner); } -void menuHandler::ClockFacePicker() +void menuHandler::clockFacePicker() { static const ClockFaceOption clockFaceOptions[] = { {"Back", OptionsAction::Back}, @@ -404,7 +404,7 @@ void menuHandler::ClockFacePicker() auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -456,7 +456,7 @@ void menuHandler::TZPicker() auto bannerOptions = createStaticBannerOptions( "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -503,13 +503,13 @@ void menuHandler::clockMenu() bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Clock) { - menuHandler::menuQueue = menuHandler::clock_face_picker; + menuHandler::menuQueue = menuHandler::ClockFacePicker; screen->runNow(); } else if (selected == Time) { - menuHandler::menuQueue = menuHandler::twelve_hour_picker; + menuHandler::menuQueue = menuHandler::TwelveHourPicker; screen->runNow(); } else if (selected == Timezone) { - menuHandler::menuQueue = menuHandler::TZ_picker; + menuHandler::menuQueue = menuHandler::TzPicker; screen->runNow(); } }; @@ -539,7 +539,7 @@ void menuHandler::messageResponseMenu() // 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); + const auto &chan = channels.getByIndex(chIndex); optionsArray[options] = chan.settings.module_settings.is_muted ? "Unmute Channel" : "Mute Channel"; optionsEnumArray[options++] = MuteChannel; @@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu() 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; + menuHandler::menuQueue = menuHandler::MessageViewModeMenu; screen->runNow(); // Reply submenu } else if (selected == ReplyMenu) { - menuHandler::menuQueue = menuHandler::reply_menu; + menuHandler::menuQueue = menuHandler::ReplyMenu; screen->runNow(); } else if (selected == MuteChannel) { @@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu() } } else if (selected == DeleteMenu) { - menuHandler::menuQueue = menuHandler::delete_messages_menu; + menuHandler::menuQueue = menuHandler::DeleteMessagesMenu; screen->runNow(); #ifdef HAS_I2S @@ -649,7 +649,7 @@ void menuHandler::replyMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -831,7 +831,7 @@ void menuHandler::messageViewModeMenu() // Gather unique peers auto dms = messageStore.getDirectMessages(); std::vector uniquePeers; - for (auto &m : dms) { + for (const 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); @@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu() bannerOptions.bannerCallback = [=](int selected) -> void { LOG_DEBUG("messageViewModeMenu: selected=%d", selected); if (selected == -1) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); } else if (selected == -2) { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); @@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker; screen->runNow(); } else if (selected == ScreenOptions) { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } else if (selected == PowerMenu) { - menuHandler::menuQueue = menuHandler::power_menu; + menuHandler::menuQueue = menuHandler::PowerMenu; screen->runNow(); } else if (selected == Test) { - menuHandler::menuQueue = menuHandler::test_menu; + menuHandler::menuQueue = menuHandler::TestMenu; screen->runNow(); } else if (selected == Bluetooth) { - menuQueue = bluetooth_toggle_menu; + menuQueue = BluetoothToggleMenu; screen->runNow(); #if HAS_WIFI && !defined(ARCH_PORTDUINO) } else if (selected == WiFiToggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); #endif } else if (selected == Back && !test_enabled) { @@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu() evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { - menuHandler::menuQueue = menuHandler::remove_favorite; + menuHandler::menuQueue = menuHandler::RemoveFavorite; screen->runNow(); } else if (selected == TraceRoute) { if (traceRouteModule) { @@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu() auto action = static_cast(option.value); switch (action) { case PositionAction::GpsToggle: - menuQueue = gps_toggle_menu; + menuQueue = GpsToggleMenu; screen->runNow(); break; case PositionAction::GpsFormat: - menuQueue = gps_format_menu; + menuQueue = GpsFormatMenu; screen->runNow(); break; case PositionAction::CompassMenu: - menuQueue = compass_point_north_menu; + menuQueue = CompassPointNorthMenu; screen->runNow(); break; case PositionAction::CompassCalibrate: @@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu() } break; case PositionAction::GPSSmartPosition: - menuQueue = gps_smart_position_menu; + menuQueue = GpsSmartPositionMenu; screen->runNow(); break; case PositionAction::GPSUpdateInterval: - menuQueue = gps_update_interval_menu; + menuQueue = GpsUpdateIntervalMenu; screen->runNow(); break; case PositionAction::GPSPositionBroadcast: - menuQueue = gps_position_broadcast_menu; + menuQueue = GpsPositionBroadcastMenu; screen->runNow(); break; } @@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NodePicker) { - menuQueue = NodePicker_menu; + menuQueue = NodePickerMenu; screen->runNow(); } else if (selected == Reset) { - menuQueue = reset_node_db_menu; + menuQueue = ResetNodeDbMenu; screen->runNow(); } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; + menuHandler::menuQueue = menuHandler::NodeNameLengthMenu; screen->runNow(); } }; @@ -1330,12 +1330,12 @@ void menuHandler::NodePicker() menuHandler::pickedNodeNum = nodenum; // Keep UI favorite context in sync (used elsewhere for some node-based actions) graphics::UIRenderer::currentFavoriteNodeNum = nodenum; - menuQueue = Manage_Node_menu; + menuQueue = ManageNodeMenu; screen->runNow(); }); } -void menuHandler::ManageNodeMenu() +void menuHandler::manageNodeMenu() { // If we don't have a node selected yet, go fast exit auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); @@ -1391,13 +1391,13 @@ void menuHandler::ManageNodeMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } if (selected == Favorite) { - auto n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); + const auto *n = nodeDB->getMeshNode(menuHandler::pickedNodeNum); if (!n) { return; } @@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu() auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } @@ -1498,6 +1498,7 @@ void menuHandler::nodeNameLengthMenu() config.display.use_long_node_name = option.value; saveUIConfig(); + service->reloadConfig(SEGMENT_CONFIG); LOG_INFO("Setting names to %s", option.value ? "long" : "short"); }); @@ -1528,7 +1529,7 @@ void menuHandler::resetNodeDBMenu() nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 0) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); } }; @@ -1550,7 +1551,7 @@ void menuHandler::compassNorthMenu() auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1595,7 +1596,7 @@ void menuHandler::GPSToggleMenu() auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1660,7 +1661,7 @@ void menuHandler::GPSFormatMenu() auto onSelection = [](const GPSFormatOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1715,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_smart_enabled = true; @@ -1744,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu() bannerOptions.optionsCount = 16; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.gps_update_interval = 8; @@ -1832,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu() bannerOptions.optionsCount = 17; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_secs = 60; @@ -1915,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu() #endif -void menuHandler::BluetoothToggleMenu() +void menuHandler::bluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; @@ -2043,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) auto bannerOptions = createStaticBannerOptions( "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); return; } @@ -2138,7 +2139,7 @@ void menuHandler::rebootMenu() messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2160,7 +2161,7 @@ void menuHandler::shutdownMenu() InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2221,14 +2222,14 @@ void menuHandler::testMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NumberPicker) { - menuQueue = number_test; + menuQueue = NumberTest; screen->runNow(); } else if (selected == ShowChirpy) { screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2252,7 +2253,7 @@ void menuHandler::wifiBaseMenu() bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Wifi_toggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); } }; @@ -2291,19 +2292,18 @@ void menuHandler::wifiToggleMenu() void menuHandler::screenOptionsMenu() { // Check if brightness is supported - bool hasSupportBrightness = false; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) - hasSupportBrightness = true; -#endif - #if defined(T_DECK) // TDeck Doesn't seem to support brightness at all, at least not reliably - hasSupportBrightness = false; + bool hasSupportBrightness = false; +#elif defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) + bool hasSupportBrightness = true; +#else + bool hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; - static const char *optionsArray[5] = {"Back"}; - static int optionsEnumArray[5] = {Back}; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles }; + static const char *optionsArray[6] = {"Back"}; + static int optionsEnumArray[6] = {Back}; int options = 1; // Only show brightness for B&W displays @@ -2325,6 +2325,9 @@ void menuHandler::screenOptionsMenu() optionsArray[options] = "Display Units"; optionsEnumArray[options++] = DisplayUnits; + optionsArray[options] = "Message Bubbles"; + optionsEnumArray[options++] = MessageBubbles; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Display Options"; bannerOptions.optionsArrayPtr = optionsArray; @@ -2332,10 +2335,10 @@ void menuHandler::screenOptionsMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Brightness) { - menuHandler::menuQueue = menuHandler::brightness_picker; + menuHandler::menuQueue = menuHandler::BrightnessPicker; screen->runNow(); } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::tftcolormenupicker; + menuHandler::menuQueue = menuHandler::TftColorMenuPicker; screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; @@ -2343,8 +2346,11 @@ void menuHandler::screenOptionsMenu() } else if (selected == DisplayUnits) { menuHandler::menuQueue = menuHandler::DisplayUnits; screen->runNow(); + } else if (selected == MessageBubbles) { + menuHandler::menuQueue = menuHandler::MessageBubblesMenu; + screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2380,16 +2386,16 @@ void menuHandler::powerMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Reboot) { - menuHandler::menuQueue = menuHandler::reboot_menu; + menuHandler::menuQueue = menuHandler::RebootMenu; screen->runNow(); } else if (selected == Shutdown) { - menuHandler::menuQueue = menuHandler::shutdown_menu; + menuHandler::menuQueue = menuHandler::ShutdownMenu; screen->runNow(); } else if (selected == MUI) { - menuHandler::menuQueue = menuHandler::mui_picker; + menuHandler::menuQueue = menuHandler::MuiPicker; screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2427,7 +2433,7 @@ void menuHandler::keyVerificationFinalPrompt() } } -void menuHandler::FrameToggles_menu() +void menuHandler::frameTogglesMenu() { enum optionsNumbers { Finish, @@ -2437,7 +2443,7 @@ void menuHandler::FrameToggles_menu() nodelist_hopsignal, nodelist_distance, nodelist_bearings, - gps, + gps_position, lora, clock, show_favorites, @@ -2475,7 +2481,7 @@ void menuHandler::FrameToggles_menu() #endif optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; - optionsEnumArray[options++] = gps; + optionsEnumArray[options++] = gps_position; #endif optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; @@ -2538,7 +2544,7 @@ void menuHandler::FrameToggles_menu() screen->toggleFrameVisibility("nodelist_bearings"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); - } else if (selected == gps) { + } else if (selected == gps_position) { screen->toggleFrameVisibility("gps"); menuHandler::menuQueue = menuHandler::FrameToggles; screen->runNow(); @@ -2571,7 +2577,7 @@ void menuHandler::FrameToggles_menu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::DisplayUnits_menu() +void menuHandler::displayUnitsMenu() { enum optionsNumbers { Back, MetricUnits, ImperialUnits }; @@ -2592,7 +2598,34 @@ void menuHandler::DisplayUnits_menu() config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; service->reloadConfig(SEGMENT_CONFIG); } else { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageBubblesMenu() +{ + enum optionsNumbers { Back, ShowBubbles, HideBubbles }; + + static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Bubbles"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == ShowBubbles) { + config.display.enable_message_bubbles = true; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles enabled"); + } else if (selected == HideBubbles) { + config.display.enable_message_bubbles = false; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles disabled"); + } else { + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } }; @@ -2601,153 +2634,156 @@ void menuHandler::DisplayUnits_menu() void menuHandler::handleMenuSwitch(OLEDDisplay *display) { - if (menuQueue != menu_none) + if (menuQueue != MenuNone) test_count = 0; switch (menuQueue) { - case menu_none: + case MenuNone: break; - case lora_Menu: + case LoraMenu: loraMenu(); break; - case lora_picker: + case LoraPicker: LoraRegionPicker(); break; - case device_role_picker: - DeviceRolePicker(); + case DeviceRolePicker: + deviceRolePicker(); break; - case radio_preset_picker: - RadioPresetPicker(); + case RadioPresetPicker: + radioPresetPicker(); break; - case frequency_slot: + case FrequencySlot: FrequencySlotPicker(); break; - case no_timeout_lora_picker: + case NoTimeoutLoraPicker: LoraRegionPicker(0); break; - case TZ_picker: + case TzPicker: TZPicker(); break; - case twelve_hour_picker: - TwelveHourPicker(); + case TwelveHourPicker: + twelveHourPicker(); break; - case clock_face_picker: - ClockFacePicker(); + case ClockFacePicker: + clockFacePicker(); break; - case clock_menu: + case ClockMenu: clockMenu(); break; - case system_base_menu: + case SystemBaseMenu: systemBaseMenu(); break; - case position_base_menu: + case PositionBaseMenu: positionBaseMenu(); break; - case node_base_menu: + case NodeBaseMenu: nodeListMenu(); break; #if !MESHTASTIC_EXCLUDE_GPS - case gps_toggle_menu: + case GpsToggleMenu: GPSToggleMenu(); break; - case gps_format_menu: + case GpsFormatMenu: GPSFormatMenu(); break; - case gps_smart_position_menu: + case GpsSmartPositionMenu: GPSSmartPositionMenu(); break; - case gps_update_interval_menu: + case GpsUpdateIntervalMenu: GPSUpdateIntervalMenu(); break; - case gps_position_broadcast_menu: + case GpsPositionBroadcastMenu: GPSPositionBroadcastMenu(); break; #endif - case compass_point_north_menu: + case CompassPointNorthMenu: compassNorthMenu(); break; - case reset_node_db_menu: + case ResetNodeDbMenu: resetNodeDBMenu(); break; - case buzzermodemenupicker: + case BuzzerModeMenuPicker: BuzzerModeMenu(); break; - case mui_picker: + case MuiPicker: switchToMUIMenu(); break; - case tftcolormenupicker: + case TftColorMenuPicker: TFTColorPickerMenu(display); break; - case brightness_picker: + case BrightnessPicker: BrightnessPickerMenu(); break; - case node_name_length_menu: + case NodeNameLengthMenu: nodeNameLengthMenu(); break; - case reboot_menu: + case RebootMenu: rebootMenu(); break; - case shutdown_menu: + case ShutdownMenu: shutdownMenu(); break; - case NodePicker_menu: + case NodePickerMenu: NodePicker(); break; - case Manage_Node_menu: - ManageNodeMenu(); + case ManageNodeMenu: + manageNodeMenu(); break; - case remove_favorite: + case RemoveFavorite: removeFavoriteMenu(); break; - case trace_route_menu: + case TraceRouteMenu: traceRouteMenu(); break; - case test_menu: + case TestMenu: testMenu(); break; - case number_test: + case NumberTest: numberTest(); break; - case wifi_toggle_menu: + case WifiToggleMenu: wifiToggleMenu(); break; - case key_verification_init: + case KeyVerificationInit: keyVerificationInitMenu(); break; - case key_verification_final_prompt: + case KeyVerificationFinalPrompt: keyVerificationFinalPrompt(); break; - case bluetooth_toggle_menu: - BluetoothToggleMenu(); + case BluetoothToggleMenu: + bluetoothToggleMenu(); break; - case screen_options_menu: + case ScreenOptionsMenu: screenOptionsMenu(); break; - case power_menu: + case PowerMenu: powerMenu(); break; case FrameToggles: - FrameToggles_menu(); + frameTogglesMenu(); break; case DisplayUnits: - DisplayUnits_menu(); + displayUnitsMenu(); break; - case throttle_message: + case ThrottleMessage: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; - case message_response_menu: + case MessageResponseMenu: messageResponseMenu(); break; - case reply_menu: + case ReplyMenu: replyMenu(); break; - case delete_messages_menu: + case DeleteMessagesMenu: deleteMessagesMenu(); break; - case message_viewmode_menu: + case MessageViewModeMenu: messageViewModeMenu(); break; + case MessageBubblesMenu: + messageBubblesMenu(); + break; } - menuQueue = menu_none; + menuQueue = MenuNone; } void menuHandler::saveUIConfig() diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1b964678b..4a0360412 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -8,53 +8,54 @@ class menuHandler { public: enum screenMenus { - menu_none, - lora_Menu, - lora_picker, - device_role_picker, - radio_preset_picker, - frequency_slot, - no_timeout_lora_picker, - TZ_picker, - twelve_hour_picker, - 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, - mui_picker, - tftcolormenupicker, - brightness_picker, - reboot_menu, - shutdown_menu, - NodePicker_menu, - Manage_Node_menu, - remove_favorite, - test_menu, - number_test, - wifi_toggle_menu, - bluetooth_toggle_menu, - screen_options_menu, - power_menu, - system_base_menu, - key_verification_init, - key_verification_final_prompt, - trace_route_menu, - throttle_message, - message_response_menu, - message_viewmode_menu, - reply_menu, - delete_messages_menu, - node_name_length_menu, + MenuNone, + LoraMenu, + LoraPicker, + DeviceRolePicker, + RadioPresetPicker, + FrequencySlot, + NoTimeoutLoraPicker, + TzPicker, + TwelveHourPicker, + ClockFacePicker, + ClockMenu, + PositionBaseMenu, + NodeBaseMenu, + GpsToggleMenu, + GpsFormatMenu, + GpsSmartPositionMenu, + GpsUpdateIntervalMenu, + GpsPositionBroadcastMenu, + CompassPointNorthMenu, + ResetNodeDbMenu, + BuzzerModeMenuPicker, + MuiPicker, + TftColorMenuPicker, + BrightnessPicker, + RebootMenu, + ShutdownMenu, + NodePickerMenu, + ManageNodeMenu, + RemoveFavorite, + TestMenu, + NumberTest, + WifiToggleMenu, + BluetoothToggleMenu, + ScreenOptionsMenu, + PowerMenu, + SystemBaseMenu, + KeyVerificationInit, + KeyVerificationFinalPrompt, + TraceRouteMenu, + ThrottleMessage, + MessageResponseMenu, + MessageViewModeMenu, + ReplyMenu, + DeleteMessagesMenu, + NodeNameLengthMenu, FrameToggles, - DisplayUnits + DisplayUnits, + MessageBubblesMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -62,15 +63,15 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); - static void DeviceRolePicker(); - static void RadioPresetPicker(); + static void deviceRolePicker(); + static void radioPresetPicker(); static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); static void TZPicker(); - static void TwelveHourPicker(); - static void ClockFacePicker(); + static void twelveHourPicker(); + static void clockFacePicker(); static void messageResponseMenu(); static void messageViewModeMenu(); static void replyMenu(); @@ -95,7 +96,7 @@ class menuHandler static void rebootMenu(); static void shutdownMenu(); static void NodePicker(); - static void ManageNodeMenu(); + static void manageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -106,15 +107,16 @@ class menuHandler static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); - static void FrameToggles_menu(); - static void DisplayUnits_menu(); + static void frameTogglesMenu(); + static void displayUnitsMenu(); + static void messageBubblesMenu(); static void textMessageMenu(); private: static void saveUIConfig(); static void keyVerificationInitMenu(); static void keyVerificationFinalPrompt(); - static void BluetoothToggleMenu(); + static void bluetoothToggleMenu(); }; /* Generic Menu Options designations */ diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 193164439..79d8b1ccd 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 constexpr int BUBBLE_MIN_W = 24; constexpr int BUBBLE_TEXT_INDENT = 2; + // Check if bubbles are enabled + const bool showBubbles = config.display.enable_message_bubbles; + const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN; + // Derived widths - const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2); + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0); const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; // Title string depending on mode @@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } } - // Draw bubbles - for (size_t bi = 0; bi < blocks.size(); ++bi) { - const auto &b = blocks[bi]; - if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) - continue; + // Draw bubbles (only if enabled) + if (showBubbles) { + for (size_t bi = 0; bi < blocks.size(); ++bi) { + const auto &b = blocks[bi]; + if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) + continue; - int visualTop = lineTop[b.start]; + int visualTop = lineTop[b.start]; - int topY; - if (isHeader[b.start]) { - // Header start - constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 - topY = visualTop - BUBBLE_PAD_TOP_HEADER; - } else { - // Body start - bool thisLineHasEmote = false; - for (int e = 0; e < numEmotes; ++e) { - if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { - thisLineHasEmote = true; - break; + int topY; + if (isHeader[b.start]) { + // Header start + constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 + topY = visualTop - BUBBLE_PAD_TOP_HEADER; + } else { + // Body start + bool thisLineHasEmote = false; + for (int e = 0; e < numEmotes; ++e) { + if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { + thisLineHasEmote = true; + break; + } } + if (thisLineHasEmote) { + constexpr int EMOTE_PADDING_ABOVE = 4; + visualTop -= EMOTE_PADDING_ABOVE; + } + topY = visualTop - BUBBLE_PAD_Y; } - if (thisLineHasEmote) { - constexpr int EMOTE_PADDING_ABOVE = 4; - visualTop -= EMOTE_PADDING_ABOVE; + int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); + int bottomY = visualBottom + BUBBLE_PAD_Y; + + if (bi + 1 < blocks.size()) { + int nextHeaderIndex = (int)blocks[bi + 1].start; + int nextTop = lineTop[nextHeaderIndex]; + int maxBottom = nextTop - 1 - bubbleGapY; + if (bottomY > maxBottom) + bottomY = maxBottom; } - topY = visualTop - BUBBLE_PAD_Y; - } - int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); - int bottomY = visualBottom + BUBBLE_PAD_Y; - if (bi + 1 < blocks.size()) { - int nextHeaderIndex = (int)blocks[bi + 1].start; - int nextTop = lineTop[nextHeaderIndex]; - int maxBottom = nextTop - 1 - bubbleGapY; - if (bottomY > maxBottom) - bottomY = maxBottom; - } + if (bottomY <= topY + 2) + continue; - if (bottomY <= topY + 2) - continue; + if (bottomY < contentTop || topY > contentBottom - 1) + continue; - if (bottomY < contentTop || topY > contentBottom - 1) - continue; + int maxLineW = 0; - int maxLineW = 0; - - for (size_t i = b.start; i <= b.end; ++i) { - int w = 0; - if (isHeader[i]) { - w = display->getStringWidth(cachedLines[i].c_str()); - if (b.mine) - w += 12; // room for ACK/NACK/relay mark - } else { - w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + for (size_t i = b.start; i <= b.end; ++i) { + int w = 0; + if (isHeader[i]) { + w = display->getStringWidth(cachedLines[i].c_str()); + if (b.mine) + w += 12; // room for ACK/NACK/relay mark + } else { + w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + } + if (w > maxLineW) + maxLineW = w; } - if (w > maxLineW) - maxLineW = w; - } - - int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2)); - int bubbleH = (bottomY - topY) + 1; - int bubbleX = 0; - if (b.mine) { - bubbleX = rightEdge - bubbleW; - } else { - bubbleX = x; - } - if (bubbleX < x) - bubbleX = x; - if (bubbleX + bubbleW > rightEdge) - bubbleW = std::max(1, rightEdge - bubbleX); - - if (bubbleW > 1 && bubbleH > 1) { - int x1 = bubbleX + bubbleW - 1; - int y1 = topY + bubbleH - 1; + int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2)); + int bubbleH = (bottomY - topY) + 1; + int bubbleX = 0; if (b.mine) { - // Send Message (Right side) - display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH); - // Top Right Corner - display->drawRect(x1, topY, 2, 1); - display->drawRect(x1, topY, 1, 2); - // Bottom Right Corner - display->drawRect(x1 - 1, bottomY - 2, 2, 1); - display->drawRect(x1, bottomY - 3, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(x1 - bubbleW, topY - 1, 1, 1); - display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1); - display->setColor(WHITE); + bubbleX = rightEdge - bubbleW; } else { - // Received Message (Left Side) - display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH); - // Top Left Corner - display->drawRect(bubbleX + 1, topY + 1, 2, 1); - display->drawRect(bubbleX + 1, topY + 1, 1, 2); - // Bottom Left Corner - display->drawRect(bubbleX + 1, bottomY - 1, 2, 1); - display->drawRect(bubbleX + 1, bottomY - 2, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(bubbleX + bubbleW, topY, 1, 1); - display->drawRect(bubbleX + bubbleW, bottomY, 1, 1); - display->setColor(WHITE); + bubbleX = x; + } + if (bubbleX < x) + bubbleX = x; + if (bubbleX + bubbleW > rightEdge) + bubbleW = std::max(1, rightEdge - bubbleX); + + // Draw rounded rectangle bubble + if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) { + const int r = BUBBLE_RADIUS; + const int bx = bubbleX; + const int by = topY; + const int bw = bubbleW; + const int bh = bubbleH; + + // Draw the 4 corner arcs using drawCircleQuads + display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left + display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right + display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left + display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right + + // Draw the 4 edges between corners + display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge + display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge + display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge + display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge + } else if (bubbleW > 1 && bubbleH > 1) { + // Fallback to simple rectangle for very small bubbles + display->drawRect(bubbleX, topY, bubbleW, bubbleH); } } - } + } // end if (showBubbles) // Render visible lines int lineY = yOffset; @@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int headerX; if (isMine[i]) { // push header left to avoid overlap with scrollbar - headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT; + headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0); if (headerX < LEFT_MARGIN) headerX = LEFT_MARGIN; } else { - headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT; + headerX = x + textIndent; } display->drawString(headerX, lineY, cachedLines[i].c_str()); @@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (isMine[i]) { // Calculate actual rendered width including emotes int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); - int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT; + int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0); if (rightX < LEFT_MARGIN) rightX = LEFT_MARGIN; drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); } else { - drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes, - numEmotes); + drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes); } } } diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 9d6780130..b36a5057c 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -171,7 +171,7 @@ unsigned long getModeCycleIntervalMs() int calculateMaxScroll(int totalEntries, int visibleRows) { - return std::max(0, (totalEntries - 1) / (visibleRows * 2)); + return max(0, (totalEntries - 1) / (visibleRows * 2)); } void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) @@ -187,13 +187,12 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, if (totalEntries <= visibleNodeRows * columns) return; - int scrollbarX = display->getWidth() - 2; int scrollbarHeight = display->getHeight() - scrollStartY - 10; - int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); - int perPage = visibleNodeRows * columns; - int maxScroll = std::max(0, (totalEntries - 1) / perPage); - int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + int thumbHeight = max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / + max(1, max(0, (totalEntries - 1) / (visibleNodeRows * columns))); + int scrollbarX = display->getWidth() - 2; for (int i = 0; i < thumbHeight; i++) { display->setPixel(scrollbarX, thumbY + i); } @@ -556,13 +555,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int maxScroll = 0; if (perPage > 0) { - maxScroll = std::max(0, (totalEntries - 1) / perPage); + maxScroll = max(0, (totalEntries - 1) / perPage); } if (scrollIndex > maxScroll) scrollIndex = maxScroll; int startIndex = scrollIndex * visibleNodeRows * totalColumns; - int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + int endIndex = min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; @@ -580,7 +579,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (extras) extras(display, node, xPos, yPos, columnWidth, heading, lat, lon); - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; rowCount++; @@ -613,13 +612,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (millis() - popupTime < POPUP_DURATION_MS) { popupTotal = totalEntries; - int perPage = visibleNodeRows * totalColumns; - popupStart = startIndex + 1; - popupEnd = std::min(startIndex + perPage, totalEntries); + popupEnd = min(startIndex + perPage, totalEntries); popupPage = (scrollIndex + 1); - popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage); + popupMaxPage = max(1, (totalEntries + perPage - 1) / perPage); char buf[32]; snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 7ce9d5afe..25a70f16d 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -2,6 +2,7 @@ #if HAS_SCREEN #include "CompassRenderer.h" #include "GPSStatus.h" +#include "MeshService.h" #include "NodeDB.h" #include "NodeListRenderer.h" #include "UIRenderer.h" @@ -287,7 +288,8 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes // ********************** // * Favorite Node Info * // ********************** -void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) +// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library +void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { if (favoritedNodes.empty()) return; @@ -313,7 +315,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st // === Create the shortName and title string === const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; char titlestr[32] = {0}; - snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); // === Draw battery/time/mail header (common across screens) === graphics::drawCommonHeader(display, x, y, titlestr); @@ -342,34 +344,162 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st } // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Sig: 97% [2hops]" - // If only one: show only that one char signalHopsStr[32] = ""; bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + int bars = 0; - // Always use "Sig" for the label - const char *signalLabel = " Sig"; + // Helper to get SNR limit based on modem preset + auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float { + 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; + } + }; + + // Calculate signal grade using modem preset and SNR only + float snrLimit = getSnrLimit(config.lora.modem_preset); + float snr = node->snr; + + // Determine signal quality label and bars using SNR-only grading + const char *qualityLabel = nullptr; + + if (snr > snrLimit + 10) { + qualityLabel = "Good"; + bars = 4; + } else if (snr > snrLimit + 6) { + qualityLabel = "Good"; + bars = 3; + } else if (snr > snrLimit + 2) { + qualityLabel = "Good"; + bars = 2; + } else if (snr > snrLimit - 4) { + qualityLabel = "Fair"; + bars = 1; + } else { + qualityLabel = "Bad"; + bars = 1; + } + + // Add extra spacing on the left if we have an API connection to account for the common footer icons + const char *leftSideSpacing = + graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " "; // --- Build the Signal/Hops line --- - // If SNR looks reasonable, show signal - if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + // Only show signal if we have valid SNR + if (snr > -100 && snr != 0) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel); haveSignal = true; } - // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { size_t len = strlen(signalHopsStr); - // Decide between "1 Hop" and "N Hops" if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, - (node->hops_away == 1 ? "Hop" : "Hops")); + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]"); } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]"); } } - if (signalHopsStr[0] && line < 5) { - display->drawString(x, getTextPositions(display)[line++], signalHopsStr); + + if (signalHopsStr[0]) { + int yPos = getTextPositions(display)[line++]; + int curX = x; + + // Split combined string into signal text and hop suffix + char sigPart[20] = ""; + const char *hopPart = nullptr; + + char *bracket = strchr(signalHopsStr, '['); + if (bracket) { + size_t n = (size_t)(bracket - signalHopsStr); + if (n >= sizeof(sigPart)) + n = sizeof(sigPart) - 1; + memcpy(sigPart, signalHopsStr, n); + sigPart[n] = '\0'; + + // Trim trailing spaces + while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') { + sigPart[strlen(sigPart) - 1] = '\0'; + } + + hopPart = bracket; // "[n Hop(s)]" + } else { + strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1); + sigPart[sizeof(sigPart) - 1] = '\0'; + } + + // Draw signal quality text + display->drawString(curX, yPos, sigPart); + curX += display->getStringWidth(sigPart) + 4; + + // Draw signal bars (skip on UltraLow, text only) + if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) { + const int kMaxBars = 4; + if (bars < 1) + bars = 1; + if (bars > kMaxBars) + bars = kMaxBars; + + int barX = curX; + + const bool hi = (currentResolution == ScreenResolution::High); + int barWidth = hi ? 2 : 1; + int barGap = hi ? 2 : 1; + int maxBarHeight = FONT_HEIGHT_SMALL - 7; + if (!hi) + maxBarHeight -= 1; + int barY = yPos + (FONT_HEIGHT_SMALL - maxBarHeight) / 2; + + for (int bi = 0; bi < kMaxBars; bi++) { + int barHeight = maxBarHeight * (bi + 1) / kMaxBars; + if (barHeight < 2) + barHeight = 2; + + int bx = barX + bi * (barWidth + barGap); + int by = barY + maxBarHeight - barHeight; + + if (bi < bars) { + display->fillRect(bx, by, barWidth, barHeight); + } else { + int baseY = barY + maxBarHeight - 1; + display->drawHorizontalLine(bx, baseY, barWidth); + } + } + + curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2; + } + + // Draw hops AFTER the bars as: [ number + hop icon ] + if (hopPart && node->hops_away > 0) { + + // open bracket + display->drawString(curX, yPos, "["); + curX += display->getStringWidth("[") + 1; + + // hop count + char hopCount[6]; + snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away); + display->drawString(curX, yPos, hopCount); + curX += display->getStringWidth(hopCount) + 2; + + // hop icon + const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2; + display->drawXbm(curX, iconY, hop_width, hop_height, hop); + curX += hop_width + 1; + + // closing bracket + display->drawString(curX, yPos, "]"); + } } // === 3. Heard (last seen, skip if node never seen) === @@ -377,8 +507,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st uint32_t seconds = sinceLastSeen(node); if (seconds != 0 && seconds != UINT32_MAX) { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + // Format as "Heard:Xm ago", "Heard:Xh ago", or "Heard:Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard:?" : "%sHeard:%d%c ago"), leftSideSpacing, (days ? days : hours ? hours : minutes), @@ -386,16 +516,18 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st : hours ? 'h' : 'm')); } - if (seenStr[0] && line < 5) { + if (seenStr[0]) { display->drawString(x, getTextPositions(display)[line++], seenStr); } #if !defined(M5STACK_UNITC6L) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { - getUptimeStr(node->device_metrics.uptime_seconds * 1000, " Up", uptimeStr, sizeof(uptimeStr)); + char upPrefix[12]; // enough for leftSideSpacing + "Up:" + snprintf(upPrefix, sizeof(upPrefix), "%sUp:", leftSideSpacing); + getUptimeStr(node->device_metrics.uptime_seconds * 1000, upPrefix, uptimeStr, sizeof(uptimeStr)); } - if (uptimeStr[0] && line < 5) { + if (uptimeStr[0]) { display->drawString(x, getTextPositions(display)[line++], uptimeStr); } @@ -422,16 +554,16 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + snprintf(distStr, sizeof(distStr), "%sDistance:%dft", leftSideSpacing, feet); haveDistance = true; } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); + snprintf(distStr, sizeof(distStr), "%sDistance:¼mi", leftSideSpacing); haveDistance = true; } } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + snprintf(distStr, sizeof(distStr), "%sDistance:%dmi", leftSideSpacing, roundedMiles); haveDistance = true; } } @@ -439,26 +571,74 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + snprintf(distStr, sizeof(distStr), "%sDistance:%dm", leftSideSpacing, meters); haveDistance = true; } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: 1km"); + snprintf(distStr, sizeof(distStr), "%sDistance:1km", leftSideSpacing); haveDistance = true; } } else { int km = (int)(distanceKm + 0.5); if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + snprintf(distStr, sizeof(distStr), "%sDistance:%dkm", leftSideSpacing, km); haveDistance = true; } } } } - // Only display if we actually have a value! - if (haveDistance && distStr[0] && line < 5) { + if (haveDistance && distStr[0]) { display->drawString(x, getTextPositions(display)[line++], distStr); } + // === 6. Battery after Distance line, otherwise next available line === + char batLine[32] = ""; + bool haveBatLine = false; + + if (node->has_device_metrics) { + bool hasPct = node->device_metrics.has_battery_level; + bool hasVolt = node->device_metrics.has_voltage && node->device_metrics.voltage > 0.001f; + + int pct = 0; + float volt = 0.0f; + + if (hasPct) { + pct = (int)node->device_metrics.battery_level; + } + + if (hasVolt) { + volt = node->device_metrics.voltage; + } + + if (hasPct && pct > 0 && pct <= 100) { + // Normal battery percentage + if (hasVolt) { + snprintf(batLine, sizeof(batLine), "%sBat:%d%% (%.2fV)", leftSideSpacing, pct, volt); + } else { + snprintf(batLine, sizeof(batLine), "%sBat:%d%%", leftSideSpacing, pct); + } + haveBatLine = true; + } else if (hasPct && pct > 100) { + // Plugged in + if (hasVolt) { + snprintf(batLine, sizeof(batLine), "%sPlugged In (%.2fV)", leftSideSpacing, volt); + } else { + snprintf(batLine, sizeof(batLine), "%sPlugged In", leftSideSpacing); + } + haveBatLine = true; + } else if (!hasPct && hasVolt) { + // Voltage only + snprintf(batLine, sizeof(batLine), "%sBat:%.2fV", leftSideSpacing, volt); + haveBatLine = true; + } + } + + const int maxTextLines = (currentResolution == ScreenResolution::High) ? 6 : 5; + + // Only draw battery if it fits within the allowed lines + if (haveBatLine && line <= maxTextLines) { + display->drawString(x, getTextPositions(display)[line++], batLine); + } + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { bool showCompass = false; @@ -593,7 +773,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } char uptimeStr[32] = ""; if (currentResolution != ScreenResolution::UltraLow) { - getUptimeStr(millis(), "Up", uptimeStr, sizeof(uptimeStr)); + getUptimeStr(millis(), "Up: ", uptimeStr, sizeof(uptimeStr)); } display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); @@ -984,7 +1164,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU config.display.heading_bold = false; const char *displayLine = ""; // Initialize to empty string by default - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { @@ -1029,10 +1208,10 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU char uptimeStr[32]; #if defined(USE_EINK) // E-Ink: skip seconds, show only days/hours/mins - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), false); + getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), false); #else // Non E-Ink: include seconds where useful - getUptimeStr(delta, "Last", uptimeStr, sizeof(uptimeStr), true); + getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true); #endif display->drawString(0, getTextPositions(display)[line++], uptimeStr); @@ -1210,6 +1389,7 @@ static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; +// cppcheck-suppress constParameterPointer; signature must match OverlayCallback typedef from OLEDDisplayUi library void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { int currentFrame = state->currentFrame; diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 6e37b68f2..8f0d07881 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -49,7 +49,7 @@ class UIRenderer // Navigation bar overlay static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/graphics/images.h b/src/graphics/images.h index ef9ffef78..66fcbc79c 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -83,6 +83,12 @@ static const unsigned char mail[] PROGMEM = { 0b11111111, 0b00 // Bottom line }; +// Hop icon (9x10) +#define hop_width 9 +#define hop_height 10 +const uint8_t hop[] PROGMEM = {0x05, 0x00, 0x07, 0x00, 0x05, 0x00, 0x38, 0x00, 0x28, 0x00, + 0x38, 0x00, 0xC0, 0x01, 0x40, 0x01, 0xC0, 0x01, 0x40, 0x00}; + // 📬 Mail / Message const uint8_t icon_mail[] PROGMEM = { 0b11111111, // ████████ top border diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp index 6d9b709b1..ad92e28ea 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.cpp @@ -42,7 +42,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused) { // Contingency only // - pin wasn't set - if (pin != (uint8_t)-1) { + if (pin != static_cast(-1)) { off(); pinMode(pin, INPUT); // High impedance - unnecessary? } else @@ -55,7 +55,7 @@ int LatchingBacklight::beforeDeepSleep(void *unused) // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::peek() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); digitalWrite(pin, logicActive); // On on = true; latched = false; @@ -67,7 +67,7 @@ void LatchingBacklight::peek() // The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling void LatchingBacklight::latch() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); // Blink if moving from peek to latch // Indicates to user that the transition has taken place @@ -89,7 +89,7 @@ void LatchingBacklight::latch() // Suitable for ending both peek and latch void LatchingBacklight::off() { - assert(pin != (uint8_t)-1); + assert(pin != static_cast(-1)); digitalWrite(pin, !logicActive); // Off on = false; latched = false; diff --git a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h index 0097cae4c..87862ea1b 100644 --- a/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h +++ b/src/graphics/niche/Drivers/Backlight/LatchingBacklight.h @@ -40,7 +40,7 @@ class LatchingBacklight CallbackObserver deepSleepObserver = CallbackObserver(this, &LatchingBacklight::beforeDeepSleep); - uint8_t pin = (uint8_t)-1; + uint8_t pin = static_cast(-1); bool logicActive = HIGH; // Is light active HIGH or active LOW bool on = false; // Is light on (either peek or latched) diff --git a/src/graphics/niche/InkHUD/Applet.cpp b/src/graphics/niche/InkHUD/Applet.cpp index ccdd76f97..0a9cd3add 100644 --- a/src/graphics/niche/InkHUD/Applet.cpp +++ b/src/graphics/niche/InkHUD/Applet.cpp @@ -1,3 +1,5 @@ +#include "graphics/niche/InkHUD/Tile.h" +#include #ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Applet.h" @@ -32,7 +34,7 @@ void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { // Only render pixels if they fall within user's cropped region if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) - assignedTile->handleAppletPixel(x, y, (Color)color); + assignedTile->handleAppletPixel(x, y, static_cast(color)); } // Link our applet to a tile @@ -142,6 +144,21 @@ void InkHUD::Applet::resetDrawingSpace() setFont(fontSmall); } +// Sets one or more inputs to enabled/disabled for this applet and if they should be sent to it +void InkHUD::Applet::setInputsSubscribed(uint8_t input, bool captured) +{ + if (captured) + subscribedInputs |= input; + else + subscribedInputs &= ~input; +} + +// Checks if a specific input is enabled for this applet and should be sent to it +bool InkHUD::Applet::isInputSubscribed(InputMask input) +{ + return (subscribedInputs & input) == input; +} + // Tell InkHUD::Renderer that we want to render now // Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc // When an applet decides it has heard something important, and wants to redraw, it calls this method @@ -310,7 +327,7 @@ void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalA } // Print text, specifying the position of any edge / corner of the textbox -void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) +void InkHUD::Applet::printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha, VerticalAlignment va) { printAt(x, y, text.c_str(), ha, va); } @@ -332,7 +349,7 @@ InkHUD::AppletFont InkHUD::Applet::getFont() // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts -std::string InkHUD::Applet::parse(std::string text) +std::string InkHUD::Applet::parse(const std::string &text) { return getFont().decodeUTF8(text); } @@ -359,10 +376,10 @@ std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) } // Determine if all characters of a string are printable using the current font -bool InkHUD::Applet::isPrintable(std::string text) +bool InkHUD::Applet::isPrintable(const std::string &text) { // Scan for SUB (0x1A), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled - for (char &c : text) { + for (const char &c : text) { if (c == '\x1A') return false; } @@ -385,7 +402,7 @@ uint16_t InkHUD::Applet::getTextWidth(const char *text) // Gets rendered width of a string // Wrapper for getTextBounds -uint16_t InkHUD::Applet::getTextWidth(std::string text) +uint16_t InkHUD::Applet::getTextWidth(const std::string &text) { return getTextWidth(text.c_str()); } @@ -433,7 +450,7 @@ std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible -void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) +void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text) { // Place the AdafruitGFX cursor to suit our "top" coord setCursor(left, top + getFont().heightAboveCursor()); @@ -490,15 +507,15 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std // Todo: rewrite making use of AdafruitGFX native text wrapping char cstr[] = {0, 0}; - int16_t l, t; - uint16_t w, h; + int16_t bx, by; + uint16_t bw, bh; for (uint16_t c = 0; c < word.length(); c++) { // Shove next char into a c string cstr[0] = word[c]; - getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); + getTextBounds(cstr, getCursorX(), getCursorY(), &bx, &by, &bw, &bh); // Manual newline, if next character will spill beyond screen edge - if ((l + w) > left + width) + if ((bx + bw) > left + width) setCursor(left, getCursorY() + getFont().lineHeight()); // Print next character @@ -517,7 +534,7 @@ void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? -uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) +uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text) { // Cache the current crop region int16_t cL = cropLeft; @@ -647,7 +664,7 @@ uint16_t InkHUD::Applet::getActiveNodeCount() // For each node in db for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Check if heard recently, and not our own node if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) @@ -700,7 +717,7 @@ std::string InkHUD::Applet::localizeDistance(uint32_t meters) } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly -void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) +void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, uint8_t thicknessY) { // How many times to draw along x axis int16_t xStart; @@ -768,7 +785,7 @@ bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) │ │ └───────────────────────────────┘ */ -void InkHUD::Applet::drawHeader(std::string text) +void InkHUD::Applet::drawHeader(const std::string &text) { // Y position for divider // - between header text and messages @@ -785,6 +802,16 @@ void InkHUD::Applet::drawHeader(std::string text) drawPixel(x, 0, BLACK); drawPixel(x, headerDivY, BLACK); // Dotted 50% } + + // Dither near battery + if (settings->optionalFeatures.batteryIcon) { + constexpr uint16_t ditherSizePx = 4; + Tile *batteryTile = ((Applet *)inkhud->getSystemApplet("BatteryIcon"))->getTile(); + const uint16_t batteryTileLeft = batteryTile->getLeft(); + const uint16_t batteryTileTop = batteryTile->getTop(); + const uint16_t batteryTileHeight = batteryTile->getHeight(); + hatchRegion(batteryTileLeft - ditherSizePx, batteryTileTop, ditherSizePx, batteryTileHeight, 2, WHITE); + } } // Get the height of the standard applet header diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 69d35a234..84fd86465 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -89,6 +89,9 @@ class Applet : public GFX virtual void onForeground() {} virtual void onBackground() {} virtual void onShutdown() {} + + // Input Events + virtual void onButtonShortPress() {} virtual void onButtonLongPress() {} virtual void onExitShort() {} @@ -100,6 +103,18 @@ class Applet : public GFX virtual void onFreeText(char c) {} virtual void onFreeTextDone() {} virtual void onFreeTextCancel() {} + // List of inputs which can be subscribed to + enum InputMask { // | No Joystick | With Joystick | + BUTTON_SHORT = 1, // | Button Click | Joystick Center Click | + BUTTON_LONG = 2, // | Button Hold | Joystick Center Hold | + EXIT_SHORT = 4, // | no-op | Back Button Click | + EXIT_LONG = 8, // | no-op | Back Button Hold | + NAV_UP = 16, // | no-op | Joystick Up | + NAV_DOWN = 32, // | no-op | Joystick Down | + NAV_LEFT = 64, // | no-op | Joystick Left | + NAV_RIGHT = 128 // | no-op | Joystick Right | + }; + bool isInputSubscribed(InputMask input); // Check if input should be handled by applet, this should not be overloaded. virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification @@ -121,20 +136,28 @@ class Applet : public GFX void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region void resetCrop(); // Removes setCrop() + // User Input Handling + + uint8_t subscribedInputs = 0b00000000; // Maybe uint16_t for futureproofing? other devices may need more inputs + void setInputsSubscribed(uint8_t input, + bool captured); // Set if an input should be handled by applet or not, this should not be + // overloaded. Can take multiple inputs at once if you OR/`|` them together + // Text void setFont(AppletFont f); AppletFont getFont(); - uint16_t getTextWidth(std::string text); + uint16_t getTextWidth(const std::string &text); uint16_t getTextWidth(const char *text); - uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped + uint32_t getWrappedTextHeight(int16_t left, uint16_t width, const std::string &text); // Result of printWrapped void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); - void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold - void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping + void printAt(int16_t x, int16_t y, const std::string &text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP); + void printThick(int16_t xCenter, int16_t yCenter, const std::string &text, uint8_t thicknessX, + uint8_t thicknessY); // Faux bold + void printWrapped(int16_t left, int16_t top, uint16_t width, const std::string &text); // Per-word line wrapping void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines - void drawHeader(std::string text); // Draw the standard applet header + void drawHeader(const std::string &text); // Draw the standard applet header // Meshtastic Logo @@ -150,9 +173,9 @@ class Applet : public GFX std::string getTimeString(); // Current time, human readable uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric - std::string parse(std::string text); // Handle text which might contain special chars + std::string parse(const std::string &text); // Handle text which might contain special chars std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars - bool isPrintable(std::string); // Check for characters which the font can't print + bool isPrintable(const std::string &text); // Check for characters which the font can't print // Convenient references diff --git a/src/graphics/niche/InkHUD/AppletFont.cpp b/src/graphics/niche/InkHUD/AppletFont.cpp index 93a621ee8..188671a0e 100644 --- a/src/graphics/niche/InkHUD/AppletFont.cpp +++ b/src/graphics/niche/InkHUD/AppletFont.cpp @@ -39,11 +39,11 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding // Caution: signed and unsigned types int8_t glyphAscender = 0 - gfxFont->glyph[i].yOffset; if (glyphAscender > 0) - this->ascenderHeight = max(this->ascenderHeight, (uint8_t)glyphAscender); + this->ascenderHeight = max(this->ascenderHeight, static_cast(glyphAscender)); int8_t glyphDescender = gfxFont->glyph[i].height + gfxFont->glyph[i].yOffset; if (glyphDescender > 0) - this->descenderHeight = max(this->descenderHeight, (uint8_t)glyphDescender); + this->descenderHeight = max(this->descenderHeight, static_cast(glyphDescender)); } // Apply any manual padding to grow or shrink the line size @@ -52,7 +52,7 @@ InkHUD::AppletFont::AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding descenderHeight += paddingBottom; // Find how far the cursor advances when we "print" a space character - spaceCharWidth = gfxFont->glyph[(uint8_t)' ' - gfxFont->first].xAdvance; + spaceCharWidth = gfxFont->glyph[static_cast(' ') - gfxFont->first].xAdvance; } /* @@ -98,7 +98,7 @@ uint8_t InkHUD::AppletFont::widthBetweenWords() // Convert a unicode char from set of UTF-8 bytes to UTF-32 // Used by AppletFont::applyEncoding, which remaps unicode chars for extended ASCII fonts, based on their UTF-32 value -uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) +uint32_t InkHUD::AppletFont::toUtf32(const std::string &utf8) { uint32_t utf32 = 0; @@ -132,7 +132,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8) // Process a string, collating UTF-8 bytes, and sending them off for re-encoding to extended ASCII // Not all InkHUD text is passed through here, only text which could potentially contain non-ASCII chars -std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) +std::string InkHUD::AppletFont::decodeUTF8(const std::string &encoded) { // Final processed output std::string decoded; @@ -141,7 +141,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) std::string utf8Char; uint8_t utf8CharSize = 0; - for (char &c : encoded) { + for (const char &c : encoded) { // If first byte if (utf8Char.empty()) { @@ -178,7 +178,7 @@ std::string InkHUD::AppletFont::decodeUTF8(std::string encoded) // Re-encode a single UTF-8 character to extended ASCII // Target encoding depends on the font -char InkHUD::AppletFont::applyEncoding(std::string utf8) +char InkHUD::AppletFont::applyEncoding(const std::string &utf8) { // ##################################################### Syntactic Sugar ##################################################### #define REMAP(in, out) \ diff --git a/src/graphics/niche/InkHUD/AppletFont.h b/src/graphics/niche/InkHUD/AppletFont.h index 02ba13c31..8374c7f61 100644 --- a/src/graphics/niche/InkHUD/AppletFont.h +++ b/src/graphics/niche/InkHUD/AppletFont.h @@ -30,20 +30,21 @@ class AppletFont }; AppletFont(); - AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0); + explicit AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, + int8_t paddingBottom = 0); uint8_t lineHeight(); uint8_t heightAboveCursor(); uint8_t heightBelowCursor(); uint8_t widthBetweenWords(); // Width of the space character - std::string decodeUTF8(std::string encoded); + std::string decodeUTF8(const std::string &encoded); - const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font + const GFXfont *gfxFont = nullptr; // Default value: in-built AdafruitGFX font private: - uint32_t toUtf32(std::string utf8); - char applyEncoding(std::string utf8); + uint32_t toUtf32(const std::string &utf8); + char applyEncoding(const std::string &utf8); uint8_t height = 8; // Default value: in-built AdafruitGFX font uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font diff --git a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp index 9794c3efb..a063c08b5 100644 --- a/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.cpp @@ -81,25 +81,25 @@ ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacke uint8_t InkHUD::NodeListApplet::maxCards() { // Cache result. Shouldn't change during execution - static uint8_t cards = 0; + static uint8_t maxCardCount = 0; - if (!cards) { + if (!maxCardCount) { const uint16_t height = Tile::maxDisplayDimension(); // Use a loop instead of arithmetic, because it's easier for my brain to follow // Add cards one by one, until the latest card extends below screen uint16_t y = cardH; // First card: no margin above - cards = 1; + maxCardCount = 1; while (y < height) { y += cardMarginH; y += cardH; - cards++; + maxCardCount++; } } - return cards; + return maxCardCount; } // Draw, using info which derived applet placed into NodeListApplet::cards for us @@ -137,12 +137,12 @@ void InkHUD::NodeListApplet::onRender(bool full) // Gather info // ======================================== - NodeNum &nodeNum = card->nodeNum; + const NodeNum &nodeNum = card->nodeNum; SignalStrength &signal = card->signal; std::string longName; // handled below std::string shortName; // handled below std::string distance; // handled below; - uint8_t &hopsAway = card->hopsAway; + const uint8_t &hopsAway = card->hopsAway; meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); diff --git a/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp new file mode 100644 index 000000000..79133719a --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.cpp @@ -0,0 +1,79 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD +#include "./UserAppletInputExample.h" + +using namespace NicheGraphics; + +void InkHUD::UserAppletInputExampleApplet::onActivate() +{ + setGrabbed(false); +} + +void InkHUD::UserAppletInputExampleApplet::onRender(bool full) +{ + drawHeader("Input Example"); + uint16_t headerHeight = getHeaderHeight(); + + std::string buttonName; + if (settings->joystick.enabled) + buttonName = "joystick center button"; + else + buttonName = "user button"; + + std::string additional = " | Control is grabbed, long press " + buttonName + " to release controls"; + if (!isGrabbed) + additional = " | Control is released, long press " + buttonName + " to grab controls"; + + printWrapped(0, headerHeight, width(), "Last button: " + lastInput + additional); +} + +void InkHUD::UserAppletInputExampleApplet::setGrabbed(bool grabbed) +{ + isGrabbed = grabbed; + setInputsSubscribed(BUTTON_SHORT | EXIT_SHORT | EXIT_LONG | NAV_UP | NAV_DOWN | NAV_LEFT | NAV_RIGHT, + grabbed); // Enables/disables grabbing all inputs + setInputsSubscribed(BUTTON_LONG, true); // Always grab this input +} + +void InkHUD::UserAppletInputExampleApplet::onButtonShortPress() +{ + lastInput = "BUTTON_SHORT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onButtonLongPress() +{ + lastInput = "BUTTON_LONG"; + setGrabbed(!isGrabbed); + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onExitShort() +{ + lastInput = "EXIT_SHORT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onExitLong() +{ + lastInput = "EXIT_LONG"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavUp() +{ + lastInput = "NAV_UP"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavDown() +{ + lastInput = "NAV_DOWN"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavLeft() +{ + lastInput = "NAV_LEFT"; + requestUpdate(); +} +void InkHUD::UserAppletInputExampleApplet::onNavRight() +{ + lastInput = "NAV_RIGHT"; + requestUpdate(); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h new file mode 100644 index 000000000..a99dec00c --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h @@ -0,0 +1,36 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applet.h" + +namespace NicheGraphics::InkHUD +{ + +class UserAppletInputExampleApplet : public Applet +{ + public: + void onActivate() override; + + void onRender(bool full) 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; + + private: + std::string lastInput = "None"; + bool isGrabbed = false; + + void setGrabbed(bool grabbed); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp index 0cc6f50ed..c0850b742 100644 --- a/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/BatteryIcon/BatteryIconApplet.cpp @@ -29,10 +29,10 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta // If we get a different type of status, something has gone weird elsewhere assert(status->getStatusType() == STATUS_TYPE_POWER); - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status; + const meshtastic::PowerStatus *pwrStatus = (const meshtastic::PowerStatus *)status; // Get the new state of charge %, and round to the nearest 10% - uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10; + uint8_t newSocRounded = ((pwrStatus->getBatteryChargePercent() + 5) / 10) * 10; // If rounded value has changed, trigger a display update // It's okay to requestUpdate before we store the new value, as the update won't run until next loop() @@ -48,37 +48,27 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta void InkHUD::BatteryIconApplet::onRender(bool full) { - // Fill entire tile - // - size of icon controlled by size of tile - int16_t l = 0; - int16_t t = 0; - uint16_t w = width(); - int16_t h = height(); - - // Clear the region beneath the tile + // Clear the region beneath the tile, including the border // Most applets are drawing onto an empty frame buffer and don't need to do this // We do need to do this with the battery though, as it is an "overlay" - fillRect(l, t, w, h, WHITE); - - // Vertical centerline - const int16_t m = t + (h / 2); + fillRect(0, 0, width(), height(), WHITE); // ===================== // Draw battery outline // ===================== // Positive terminal "bump" - const int16_t &bumpL = l; - const uint16_t bumpH = h / 2; - const int16_t bumpT = m - (bumpH / 2); constexpr uint16_t bumpW = 2; + const int16_t &bumpL = 1; + const uint16_t bumpH = (height() - 2) / 2; + const int16_t bumpT = (1 + ((height() - 2) / 2)) - (bumpH / 2); fillRect(bumpL, bumpT, bumpW, bumpH, BLACK); // Main body of battery - const int16_t bodyL = bumpL + bumpW; - const int16_t &bodyT = t; - const int16_t &bodyH = h; - const int16_t bodyW = w - bumpW; + const int16_t bodyL = 1 + bumpW; + const int16_t &bodyT = 1; + const int16_t &bodyH = height() - 2; // Handle top/bottom padding + const int16_t bodyW = (width() - 1) - bumpW; // Handle 1px left pad drawRect(bodyL, bodyT, bodyW, bodyH, BLACK); // Erase join between bump and body @@ -89,12 +79,13 @@ void InkHUD::BatteryIconApplet::onRender(bool full) // =================== constexpr int16_t slicePad = 2; - const int16_t sliceL = bodyL + slicePad; + int16_t sliceL = bodyL + slicePad; const int16_t sliceT = bodyT + slicePad; const uint16_t sliceH = bodyH - (slicePad * 2); uint16_t sliceW = bodyW - (slicePad * 2); - sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceW = (sliceW * socRounded) / 100; // Apply percentage + sliceL += ((bodyW - (slicePad * 2)) - sliceW); // Shift slice to the battery's negative terminal, correcting drain direction hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK); drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 6a141f73e..520a6de97 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -1176,7 +1176,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back(MenuItem("Back", previousPage)); for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { - meshtastic_Channel &ch = channels.getByIndex(i); + const meshtastic_Channel &ch = channels.getByIndex(i); if (!ch.has_settings) continue; @@ -1252,7 +1252,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page) case NODE_CONFIG_CHANNEL_PRECISION: { previousPage = MenuPage::NODE_CONFIG_CHANNEL_DETAIL; items.push_back(MenuItem("Back", previousPage)); - meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); + const meshtastic_Channel &ch = channels.getByIndex(selectedChannelIndex); if (!ch.settings.has_module_settings || ch.settings.module_settings.position_precision == 0) { items.push_back(MenuItem("Position is Off", MenuPage::NODE_CONFIG_CHANNEL_DETAIL)); break; @@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress() if (!settings->joystick.enabled) { if (!cursorShown) { cursorShown = true; + // Select the first item that isn't a header cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } } else { do { cursor = (cursor + 1) % items.size(); @@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp() if (!cursorShown) { cursorShown = true; - cursor = 0; + // Select the last item that isn't a header + cursor = items.size() - 1; + while (items.at(cursor).isHeader) { + if (cursor == 0) { + cursorShown = false; + break; + } + cursor--; + } } else { do { if (cursor == 0) @@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown() if (!cursorShown) { cursorShown = true; + // Select the first item that isn't a header cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } } else { do { cursor = (cursor + 1) % items.size(); @@ -1759,7 +1783,7 @@ void InkHUD::MenuApplet::populateRecipientPage() for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { // Get the channel, and check if it's enabled - meshtastic_Channel &channel = channels.getByIndex(i); + const meshtastic_Channel &channel = channels.getByIndex(i); if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) continue; @@ -1829,7 +1853,7 @@ void InkHUD::MenuApplet::populateRecipientPage() items.push_back(MenuItem("Exit", MenuPage::EXIT)); } -void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text) +void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, const std::string &text) { setFont(fontSmall); uint16_t wrapMaxH = 0; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 7b092153b..b5c1c86e4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -55,7 +55,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, - std::string text); // Draw input field for free text + const std::string &text); // Draw input field for free text uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, uint16_t *height = nullptr); // Info panel at top of root menu diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 19cef4fbd..6c8069c8b 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -228,17 +228,17 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) { // Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently - bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; + bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; // Pick source of message - MessageStore::Message *message = - isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; + const MessageStore::Message *message = + msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Find info about the sender meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender); // Leading tag (channel vs. DM) - text += isBroadcast ? "From:" : "DM: "; + text += msgIsBroadcast ? "From:" : "DM: "; // Sender id if (node && node->has_user) @@ -252,7 +252,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila text.clear(); // Leading tag (channel vs. DM) - text += isBroadcast ? "Msg from " : "DM from "; + text += msgIsBroadcast ? "Msg from " : "DM from "; // Sender id if (node && node->has_user) diff --git a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp index a09ff55d5..54515b296 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Pairing/PairingApplet.cpp @@ -55,12 +55,12 @@ int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *sta // We'll mimic that behavior, just to keep in line with the other Statuses, // even though I'm not sure what the original reason for jumping through these extra hoops was. assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH); - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status; + const auto *btStatus = static_cast(status); // When pairing begins - if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { + if (btStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) { // Store the passkey for rendering - passkey = bluetoothStatus->getPasskey(); + passkey = btStatus->getPasskey(); // Show pairing screen bringToForeground(); diff --git a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp new file mode 100644 index 000000000..520070d72 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.cpp @@ -0,0 +1,111 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./FavoritesMapApplet.h" +#include "NodeDB.h" + +using namespace NicheGraphics; + +bool InkHUD::FavoritesMapApplet::shouldDrawNode(meshtastic_NodeInfoLite *node) +{ + // Keep our own node available as map anchor/center; all others must be favorited. + return node && (node->num == nodeDB->getNodeNum() || node->is_favorite); +} + +void InkHUD::FavoritesMapApplet::onRender(bool full) +{ + // Custom empty state text for favorites-only map. + if (!enoughMarkers()) { + printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Favorite node position", CENTER, MIDDLE); + printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE); + return; + } + + // Draw the usual map applet first. + MapApplet::onRender(full); + + // Draw our latest "node of interest" as a special marker. + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom); + if (node && node->is_favorite && nodeDB->hasValidPosition(node) && enoughMarkers()) + drawLabeledMarker(node); +} + +// Determine if we need to redraw the map, when we receive a new position packet. +ProcessMessage InkHUD::FavoritesMapApplet::handleReceived(const meshtastic_MeshPacket &mp) +{ + // If applet is not active, we shouldn't be handling any data. + if (!isActive()) + return ProcessMessage::CONTINUE; + + // Try decode a position from the packet. + bool hasPosition = false; + float lat; + float lng; + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) { + meshtastic_Position position = meshtastic_Position_init_default; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) { + if (position.has_latitude_i && position.has_longitude_i // Actually has position + && (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island" + { + hasPosition = true; + lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format + lng = position.longitude_i * 1e-7; + } + } + } + + // Skip if we didn't get a valid position. + if (!hasPosition) + return ProcessMessage::CONTINUE; + + const int8_t hopsAway = getHopsAway(mp); + const bool hasHopsAway = hopsAway >= 0; + + // Determine if the position packet would change anything on-screen. + bool somethingChanged = false; + + // If our own position. + if (isFromUs(&mp)) { + // Ignore tiny local movement to reduce update spam. + if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) { + somethingChanged = true; + ourLastLat = lat; + ourLastLng = lng; + } + } else { + // For non-local packets, this applet only reacts to favorited nodes. + const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); + if (!sender || !sender->is_favorite) + return ProcessMessage::CONTINUE; + + // Check if this position is from someone different than our previous position packet. + if (mp.from != lastFrom) { + somethingChanged = true; + lastFrom = mp.from; + lastLat = lat; + lastLng = lng; + lastHopsAway = hopsAway; + } + + // Same sender: check if position changed. + else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) { + somethingChanged = true; + lastLat = lat; + lastLng = lng; + } + + // Same sender, same position: check if hops changed. + else if (hasHopsAway && (hopsAway != lastHopsAway)) { + somethingChanged = true; + lastHopsAway = hopsAway; + } + } + + if (somethingChanged) { + requestAutoshow(); + requestUpdate(); + } + + return ProcessMessage::CONTINUE; +} + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h new file mode 100644 index 000000000..da5fb0dc3 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h @@ -0,0 +1,44 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +Plots position of favorited nodes from DB, with North facing up. +Scaled to fit the most distant node. +Size of marker represents hops away. +The favorite node which most recently sent a position will be labeled. + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h" + +#include "SinglePortModule.h" + +namespace NicheGraphics::InkHUD +{ + +class FavoritesMapApplet : public MapApplet, public SinglePortModule +{ + public: + FavoritesMapApplet() : SinglePortModule("FavoritesMapApplet", meshtastic_PortNum_POSITION_APP) {} + void onRender(bool full) override; + + protected: + bool shouldDrawNode(meshtastic_NodeInfoLite *node) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + NodeNum lastFrom = 0; // Sender of most recent favorited (non-local) position packet + float lastLat = 0.0; + float lastLng = 0.0; + float lastHopsAway = 0; + + float ourLastLat = 0.0; // Info about most recent local position + float ourLastLng = 0.0; +}; + +} // namespace NicheGraphics::InkHUD + +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp index 5a659c606..a7fd094e6 100644 --- a/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.cpp @@ -69,9 +69,10 @@ void InkHUD::HeardApplet::populateFromNodeDB() } // Sort the collection by age - std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { - return (top->last_heard > bottom->last_heard); - }); + std::sort(ordered.begin(), ordered.end(), + [](const meshtastic_NodeInfoLite *top, const meshtastic_NodeInfoLite *bottom) -> bool { + return (top->last_heard > bottom->last_heard); + }); // Keep the most recent entries only // Just enough to fill the screen diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index f16721357..01bdc2224 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -69,7 +69,7 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full) while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { // Grab data for message - MessageStore::Message &m = store->messages.at(i); + const MessageStore::Message &m = store->messages.at(i); bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index e6c16d350..577a773bb 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -59,10 +59,16 @@ void InkHUD::Events::onButtonShort() if (consumer) { consumer->onButtonShortPress(); } 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(); + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_SHORT)) + userConsumer->onButtonShortPress(); + else { + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } } @@ -84,8 +90,14 @@ void InkHUD::Events::onButtonLong() // If no system applet is handling input, default behavior instead is to open the menu if (consumer) consumer->onButtonLongPress(); - else - inkhud->openMenu(); + else { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::BUTTON_LONG)) + userConsumer->onButtonLongPress(); + else + inkhud->openMenu(); + } } void InkHUD::Events::onExitShort() @@ -110,8 +122,14 @@ void InkHUD::Events::onExitShort() // 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(); + else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) + userConsumer->onExitShort(); + else + inkhud->nextTile(); + } } } @@ -133,6 +151,13 @@ void InkHUD::Events::onExitLong() if (consumer) consumer->onExitLong(); + else { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG)) + userConsumer->onExitLong(); + // Nothing uses exit long yet + } } } @@ -157,6 +182,12 @@ void InkHUD::Events::onNavUp() if (consumer) consumer->onNavUp(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP)) + userConsumer->onNavUp(); + } } } @@ -181,6 +212,12 @@ void InkHUD::Events::onNavDown() if (consumer) consumer->onNavDown(); + else if (!dismissedExt) { + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN)) + userConsumer->onNavDown(); + } } } @@ -206,8 +243,14 @@ void InkHUD::Events::onNavLeft() // 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(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT)) + userConsumer->onNavLeft(); + else + inkhud->prevApplet(); + } } } @@ -233,8 +276,14 @@ void InkHUD::Events::onNavRight() // 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(); + else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + Applet *userConsumer = inkhud->getActiveApplet(); + + if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT)) + userConsumer->onNavRight(); + else + inkhud->nextApplet(); + } } } diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 5fab67639..edffda6b7 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -210,6 +210,12 @@ void InkHUD::InkHUD::prevApplet() windowManager->prevApplet(); } +// Returns the currently active applet +InkHUD::Applet *InkHUD::InkHUD::getActiveApplet() +{ + return windowManager->getActiveApplet(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index ae029137e..0e25b0900 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -74,6 +74,7 @@ class InkHUD void nextApplet(); void prevApplet(); + NicheGraphics::InkHUD::Applet *getActiveApplet(); void openMenu(); void openAlignStick(); void openKeyboard(); diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp index 94e0aa661..44a1ef633 100644 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ b/src/graphics/niche/InkHUD/MessageStore.cpp @@ -12,7 +12,7 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::MessageStore::MessageStore(std::string label) +InkHUD::MessageStore::MessageStore(const std::string &label) { filename = ""; filename += "/NicheGraphics"; @@ -50,12 +50,13 @@ void InkHUD::MessageStore::saveToFlash() // For each message for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { Message &m = messages.at(i); - f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes - f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes - f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte - f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length - f.write('\0'); // Append null term - LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); + f.write(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes + f.write(reinterpret_cast(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes + f.write(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte + f.write(reinterpret_cast(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text + f.write('\0'); // Append null term + LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast(i), min(MAX_MESSAGE_SIZE, m.text.size()), + m.text.c_str()); } // Release firmware's SPI lock, because SafeFile::close needs it @@ -111,17 +112,17 @@ void InkHUD::MessageStore::loadFromFlash() // First byte: how many messages are in the flash store uint8_t flashMessageCount = 0; - f.readBytes((char *)&flashMessageCount, 1); - LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount); + f.readBytes(reinterpret_cast(&flashMessageCount), 1); + LOG_DEBUG("Messages available: %u", static_cast(flashMessageCount)); // For each message for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { Message m; // Read meta data (fixed width) - f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); - f.readBytes((char *)&m.sender, sizeof(m.sender)); - f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); + f.readBytes(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); + f.readBytes(reinterpret_cast(&m.sender), sizeof(m.sender)); + f.readBytes(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Read characters until we find a null term char c; @@ -136,7 +137,8 @@ void InkHUD::MessageStore::loadFromFlash() // Store in RAM messages.push_back(m); - LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str()); + LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast(i), m.timestamp, m.sender, + m.text.c_str()); } f.close(); diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h index 745c3b2eb..55fb9b8cc 100644 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ b/src/graphics/niche/InkHUD/MessageStore.h @@ -31,7 +31,7 @@ class MessageStore }; MessageStore() = delete; - explicit MessageStore(std::string label); // Label determines filename in flash + explicit MessageStore(const std::string &label); // Label determines filename in flash void saveToFlash(); void loadFromFlash(); diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index b985f9f77..67ad5098f 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -8,5 +8,5 @@ build_flags = -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = - # TODO renovate - https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX + # renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root + https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX diff --git a/src/graphics/niche/InkHUD/Renderer.cpp b/src/graphics/niche/InkHUD/Renderer.cpp index 89a83c932..a73e209ff 100644 --- a/src/graphics/niche/InkHUD/Renderer.cpp +++ b/src/graphics/niche/InkHUD/Renderer.cpp @@ -269,42 +269,42 @@ void InkHUD::Renderer::clearTile(Tile *t) // Rotate the tile dimensions int16_t left = 0; int16_t top = 0; - uint16_t width = 0; - uint16_t height = 0; + uint16_t tileW = 0; + uint16_t tileH = 0; switch (settings->rotation) { case 0: left = t->getLeft(); top = t->getTop(); - width = t->getWidth(); - height = t->getHeight(); + tileW = t->getWidth(); + tileH = t->getHeight(); break; case 1: left = driver->width - (t->getTop() + t->getHeight()); top = t->getLeft(); - width = t->getHeight(); - height = t->getWidth(); + tileW = t->getHeight(); + tileH = t->getWidth(); break; case 2: left = driver->width - (t->getLeft() + t->getWidth()); top = driver->height - (t->getTop() + t->getHeight()); - width = t->getWidth(); - height = t->getHeight(); + tileW = t->getWidth(); + tileH = t->getHeight(); break; case 3: left = t->getTop(); top = driver->height - (t->getLeft() + t->getWidth()); - width = t->getHeight(); - height = t->getWidth(); + tileW = t->getHeight(); + tileH = t->getWidth(); break; } // Calculate the bounds to clear uint16_t xStart = (left < 0) ? 0 : left; uint16_t yStart = (top < 0) ? 0 : top; - if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0) + if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0) return; // the box is completely off the screen - uint16_t xEnd = left + width; - uint16_t yEnd = top + height; + uint16_t xEnd = left + tileW; + uint16_t yEnd = top + tileH; if (xEnd > driver->width) xEnd = driver->width; if (yEnd > driver->height) diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index 9c18fbd48..fce3c9770 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -273,6 +273,12 @@ void InkHUD::WindowManager::prevApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// Returns active applet +NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet() +{ + return userTiles.at(settings->userTiles.focused)->getAssignedApplet(); +} + // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { @@ -396,7 +402,7 @@ void InkHUD::WindowManager::autoshow() { // Don't perform autoshow if a system applet has exclusive use of the display right now // Note: lockRequests prevents autoshow attempting to hide menuApplet - for (SystemApplet *sa : inkhud->systemApplets) { + for (const SystemApplet *sa : inkhud->systemApplets) { if (sa->lockRendering || sa->lockRequests) return; } @@ -510,10 +516,10 @@ void InkHUD::WindowManager::placeSystemTiles() const uint16_t batteryIconWidth = batteryIconHeight * 1.8; inkhud->getSystemApplet("BatteryIcon") ->getTile() - ->setRegion(inkhud->width() - batteryIconWidth, // x - 2, // y - batteryIconWidth, // width - batteryIconHeight); // height + ->setRegion(inkhud->width() - batteryIconWidth - 1, // x + 1, // y + batteryIconWidth + 1, // width + batteryIconHeight + 2); // height // Note: the tiles of placeholder and menu applets are manipulated specially // - menuApplet borrows user tiles diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 948ef6131..a11688cf5 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -29,6 +29,7 @@ class WindowManager void nextTile(); void prevTile(); + Applet *getActiveApplet(); void openMenu(); void openAlignStick(); void openKeyboard(); diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp index 50998930d..182b7e1f8 100644 --- a/src/graphics/niche/Utils/CannedMessageStore.cpp +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -146,7 +146,7 @@ void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) std::string merged; if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 merged.reserve(201); - for (std::string &s : messages) { + for (const std::string &s : messages) { merged += s; merged += '|'; } diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 0d835a3a9..3e4aa4bcd 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -276,7 +276,8 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); - if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && + // Require press started after boot holdoff to avoid phantom shutdown from floating pins + if (millis() > 30000 && buttonPressStartTime > 30000 && _longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime && leadUpPlayed) { evt.inputEvent = _longLongPress; this->notifyObservers(&evt); diff --git a/src/input/HackadayCommunicatorKeyboard.cpp b/src/input/HackadayCommunicatorKeyboard.cpp index c6a9e0ae8..b096c74d2 100644 --- a/src/input/HackadayCommunicatorKeyboard.cpp +++ b/src/input/HackadayCommunicatorKeyboard.cpp @@ -106,8 +106,8 @@ static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{}, {}}; 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) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { reset(); } @@ -147,7 +147,6 @@ void HackadayCommunicatorKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { @@ -187,8 +186,8 @@ void HackadayCommunicatorKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/HackadayCommunicatorKeyboard.h b/src/input/HackadayCommunicatorKeyboard.h index 8316bed72..cbba5c12f 100644 --- a/src/input/HackadayCommunicatorKeyboard.h +++ b/src/input/HackadayCommunicatorKeyboard.h @@ -18,8 +18,8 @@ class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase 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; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index c0a56233f..acf79f149 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -100,13 +100,28 @@ void InputBroker::processInputEventQueue() int InputBroker::handleInputEvent(const InputEvent *event) { - powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release +#if HAS_SCREEN + bool screenWasOff = false; + if (screen) { + screenWasOff = !screen->isScreenOn(); + } +#endif + powerFSM.trigger(EVENT_INPUT); if (event && event->inputEvent != INPUT_BROKER_NONE && externalNotificationModule && moduleConfig.external_notification.enabled && externalNotificationModule->nagging()) { externalNotificationModule->stopNow(); + // If this turns off a notification, don't further process the event + return 0; } +#if HAS_SCREEN + if (screen && screenWasOff) { + // If the screen was off, it is in the process of turning on, and we just drop the event + return 0; + } +#endif + this->notifyObservers(event); return 0; } diff --git a/src/input/MPR121Keyboard.cpp b/src/input/MPR121Keyboard.cpp index 9bca6801d..ec37cfbaa 100644 --- a/src/input/MPR121Keyboard.cpp +++ b/src/input/MPR121Keyboard.cpp @@ -91,7 +91,7 @@ MPR121Keyboard::MPR121Keyboard() : m_wire(nullptr), m_addr(0), readCallback(null { // LOG_DEBUG("MPR121 @ %02x", m_addr); state = Init; - last_key = -1; + last_key = UINT8_MAX; last_tap = 0L; char_idx = 0; queue = ""; @@ -359,8 +359,8 @@ void MPR121Keyboard::released() return; } // would clear longpress callback... later. - if (last_key < 0 || last_key > _NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; + if (last_key >= _NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/MPR121Keyboard.h b/src/input/MPR121Keyboard.h index 6349750ce..ec3f56e87 100644 --- a/src/input/MPR121Keyboard.h +++ b/src/input/MPR121Keyboard.h @@ -14,7 +14,7 @@ class MPR121Keyboard MPR121States state; - int8_t last_key; + uint8_t last_key; uint32_t last_tap; uint8_t char_idx; diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index bd8338acf..822885a9f 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -43,8 +43,8 @@ static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = { }; TCA8418Keyboard::TCA8418Keyboard() - : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(-1), next_key(-1), last_tap(0L), char_idx(0), tap_interval(0), - should_backspace(false) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), last_key(UINT8_MAX), next_key(UINT8_MAX), last_tap(0L), char_idx(0), + tap_interval(0), should_backspace(false) { } @@ -63,7 +63,6 @@ void TCA8418Keyboard::pressed(uint8_t key) if (state == Init || state == Busy) { return; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -72,7 +71,7 @@ void TCA8418Keyboard::pressed(uint8_t key) } // Compute key index based on dynamic row/column - next_key = row * _TCA8418_COLS + col; + next_key = (uint8_t)(row * _TCA8418_COLS + col); // LOG_DEBUG("TCA8418: Key %u -> Next Key %u", key, next_key); @@ -106,8 +105,8 @@ void TCA8418Keyboard::released() return; } - if (last_key < 0 || last_key > _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { // reset to idle if last_key out of bounds + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index b76916643..0e8821260 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -14,8 +14,8 @@ class TCA8418Keyboard : public TCA8418KeyboardBase void pressed(uint8_t key) override; void released(void) override; - int8_t last_key; - int8_t next_key; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/TDeckProKeyboard.cpp b/src/input/TDeckProKeyboard.cpp index eeafe4949..b83f0c6ae 100644 --- a/src/input/TDeckProKeyboard.cpp +++ b/src/input/TDeckProKeyboard.cpp @@ -62,8 +62,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = { }; TDeckProKeyboard::TDeckProKeyboard() - : 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) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { } @@ -101,7 +101,6 @@ void TDeckProKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -142,8 +141,8 @@ void TDeckProKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TDeckProKeyboard.h b/src/input/TDeckProKeyboard.h index 617f3f20b..3ef97fc3d 100644 --- a/src/input/TDeckProKeyboard.h +++ b/src/input/TDeckProKeyboard.h @@ -19,8 +19,8 @@ class TDeckProKeyboard : public TCA8418KeyboardBase 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; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp index 9a4fd8679..4efa5d6e2 100644 --- a/src/input/TLoraPagerKeyboard.cpp +++ b/src/input/TLoraPagerKeyboard.cpp @@ -65,8 +65,8 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, {' ', 0x00, Key::BL_TOGGLE}}; TLoraPagerKeyboard::TLoraPagerKeyboard() - : 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) + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX), + next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0) { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); @@ -129,7 +129,6 @@ void TLoraPagerKeyboard::pressed(uint8_t key) modifierFlag = 0; } - uint8_t next_key = 0; int row = (key - 1) / 10; int col = (key - 1) % 10; @@ -170,8 +169,8 @@ void TLoraPagerKeyboard::released() return; } - if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { - last_key = -1; + if (last_key >= _TCA8418_NUM_KEYS) { + last_key = UINT8_MAX; state = Idle; return; } diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index f04d2ce6a..06a4c7b63 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -21,8 +21,8 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase 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; + uint8_t last_key; + uint8_t next_key; uint32_t last_tap; uint8_t char_idx; int32_t tap_interval; diff --git a/src/main.cpp b/src/main.cpp index d9773dfb9..8a46b3f5b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,13 +7,14 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "PowerMon.h" +#include "RadioLibInterface.h" #include "ReliableRouter.h" +#include "TransmitHistory.h" #include "airtime.h" #include "buzz.h" #include "power/PowerHAL.h" #include "FSCommon.h" -#include "Led.h" #include "RTC.h" #include "SPILock.h" #include "Throttle.h" @@ -29,7 +30,6 @@ #include #endif #include "detect/einkScan.h" -#include "graphics/RAKled.h" #include "graphics/Screen.h" #include "main.h" #include "mesh/generated/meshtastic/config.pb.h" @@ -242,33 +242,12 @@ const char *getDeviceName() return name; } -// TODO remove from main.cpp -static int32_t ledBlinker() -{ - // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if - // config.device.led_heartbeat_disabled is changed - if (config.device.led_heartbeat_disabled) - return 1000; - - static bool ledOn; - ledOn ^= 1; - - ledBlink.set(ledOn); - - // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); -} - uint32_t timeLastPowered = 0; -static Periodic *ledPeriodic; static OSThread *powerFSMthread; -static OSThread *ambientLightingThread; +OSThread *ambientLightingThread; -RadioInterface *rIf = NULL; -#ifdef ARCH_PORTDUINO RadioLibHal *RadioLibHAL = NULL; -#endif /** * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. @@ -299,21 +278,16 @@ void earlyInitVariant() {} // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe() { - -#ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); -#endif - while (powerHAL_isPowerLevelSafe() == false) { -#ifdef LED_PIN +#ifdef LED_POWER // 3x: blink for 300 ms, pause for 300 ms for (int i = 0; i < 3; i++) { - digitalWrite(LED_PIN, LED_STATE_ON); + digitalWrite(LED_POWER, LED_STATE_ON); delay(300); - digitalWrite(LED_PIN, LED_STATE_OFF); + digitalWrite(LED_POWER, LED_STATE_OFF); delay(300); } #endif @@ -337,6 +311,11 @@ void setup() // initialize power HAL layer as early as possible powerHAL_init(); +#ifdef LED_POWER + pinMode(LED_POWER, OUTPUT); + digitalWrite(LED_POWER, LED_STATE_ON); +#endif + // prevent booting if device is in power failure mode // boot sequence will follow when battery level raises to safe mode waitUntilPowerLevelSafe(); @@ -349,11 +328,6 @@ void setup() digitalWrite(PIN_POWER_EN, HIGH); #endif -#ifdef LED_POWER - pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, LED_STATE_ON); -#endif - #ifdef LED_NOTIFICATION pinMode(LED_NOTIFICATION, OUTPUT); digitalWrite(LED_NOTIFICATION, HIGH ^ LED_STATE_ON); @@ -366,17 +340,14 @@ void setup() #ifdef BLE_LED pinMode(BLE_LED, OUTPUT); -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif concurrency::hasBeenSetup = true; - +#if HAS_SCREEN meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; +#endif OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; #ifdef USE_SEGGER @@ -418,8 +389,8 @@ void setup() #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR - // use PSRAM for malloc calls > 256 bytes - heap_caps_malloc_extmem_enable(256); + // use PSRAM for malloc calls > 2048 bytes + heap_caps_malloc_extmem_enable(2048); #endif #endif @@ -488,14 +459,6 @@ void setup() OSThread::setup(); - // TODO make this ifdef based on defined pins and move from main.cpp -#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - // The ThinkNodes have their own blink logic - // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); -#else - ledPeriodic = new Periodic("Blink", ledBlinker); -#endif - fsInit(); #if !MESHTASTIC_EXCLUDE_I2C @@ -601,6 +564,7 @@ void setup() } #endif +#if HAS_SCREEN auto screenInfo = i2cScanner->firstScreen(); screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; @@ -618,6 +582,7 @@ void setup() screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } } +#endif #define UPDATE_FROM_SCANNER(FIND_FN) #if defined(USE_VIRTUAL_KEYBOARD) @@ -712,21 +677,12 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MLX90614, meshtastic_TelemetrySensorType_MLX90614); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::ICM20948, meshtastic_TelemetrySensorType_ICM20948); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::MAX30102, meshtastic_TelemetrySensorType_MAX30102); - scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); - #endif #ifdef HAS_SDCARD setupSDCard(); #endif - // LED init - -#ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LED_STATE_ON); // turn on for now -#endif - // Hello printInfo(); #ifdef BUILD_EPOCH @@ -748,6 +704,9 @@ void setup() // We do this as early as possible because this loads preferences from flash // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; + + // Initialize transmit history to persist broadcast throttle timers across reboots + TransmitHistory::getInstance()->loadFromDisk(); #if HAS_TFT if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { tftSetup(); @@ -764,17 +723,17 @@ void setup() else playStartMelody(); - // fixed screen override? - if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) - screen_model = config.display.oled; - +#if HAS_SCREEN + // fixed screen override? #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 screen_geometry = GEOMETRY_128_128; -#endif - -#if defined(USE_SH1107_128_64) +#elif defined(USE_SH1107_128_64) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // keep dimension of 128x64 +#else + if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) + screen_model = config.display.oled; +#endif #endif #if !MESHTASTIC_EXCLUDE_I2C @@ -834,7 +793,7 @@ void setup() SPI.begin(); #endif #else -// ESP32 + // ESP32 #if defined(HW_SPI1_DEVICE) SPI1.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); LOG_DEBUG("SPI1.begin(SCK=%d, MISO=%d, MOSI=%d, NSS=%d)", LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); @@ -929,6 +888,13 @@ void setup() service = new MeshService(); service->init(); + // Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) +#ifndef HAS_PHYSICAL_KEYBOARD + osk_found = true; +#endif +#endif + // Now that the mesh service is created, create any modules setupModules(); @@ -960,12 +926,6 @@ void setup() setupNicheGraphics(); #endif -#ifdef LED_PIN - // Turn LED off after boot, if heartbeat by config - if (config.device.led_heartbeat_disabled) - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); -#endif - // Do this after service.init (because that clears error_code) #ifdef HAS_PMU if (!pmu_found) @@ -991,7 +951,7 @@ void setup() #endif #endif - initLoRa(); + auto rIf = initLoRa(); lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation) @@ -1019,12 +979,6 @@ void setup() #endif #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. webServerThread = new WebServerThread(); @@ -1046,12 +1000,12 @@ void setup() if (!rIf) RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_NO_RADIO); else { - router->addInterface(rIf); - // Log bit rate to debug output LOG_DEBUG("LoRA bitrate = %f bytes / sec", (float(meshtastic_Constants_DATA_PAYLOAD_LEN) / (float(rIf->getPacketTime(meshtastic_Constants_DATA_PAYLOAD_LEN)))) * 1000); + + router->addInterface(std::move(rIf)); } // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values @@ -1168,6 +1122,21 @@ void loop() #endif power->powerCommandsCheck(); + if (RadioLibInterface::instance != nullptr) { + static uint32_t lastRadioMissedIrqPoll; + if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) { + lastRadioMissedIrqPoll = millis(); + RadioLibInterface::instance->pollMissedIrqs(); + } + + // Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain + static uint32_t lastAgcReset; + if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) { + lastAgcReset = millis(); + RadioLibInterface::instance->resetAGC(); + } + } + #ifdef DEBUG_STACK static uint32_t lastPrint = 0; if (!Throttle::isWithinTimespanMs(lastPrint, 10 * 1000L)) { @@ -1187,10 +1156,7 @@ void loop() } if (portduino_status.LoRa_in_error && rebootAtMsec == 0) { LOG_ERROR("LoRa in error detected, attempting to recover"); - if (rIf != nullptr) { - delete rIf; - rIf = nullptr; - } + router->addInterface(nullptr); if (portduino_config.lora_spi_dev == "ch341") { if (ch341Hal != nullptr) { delete ch341Hal; @@ -1206,8 +1172,9 @@ void loop() exit(EXIT_FAILURE); } } - if (initLoRa()) { - router->addInterface(rIf); + auto rIf = initLoRa(); + if (rIf) { + router->addInterface(std::move(rIf)); portduino_status.LoRa_in_error = false; } else { LOG_WARN("Reconfigure failed, rebooting"); diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index 0f4d64113..72216a63c 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -1,6 +1,7 @@ #include "CryptoEngine.h" // #include "NodeDB.h" #include "architecture.h" +#include #if !(MESHTASTIC_EXCLUDE_PKI) #include "NodeDB.h" @@ -169,10 +170,9 @@ void CryptoEngine::hash(uint8_t *bytes, size_t numBytes) void CryptoEngine::aesSetKey(const uint8_t *key_bytes, size_t key_len) { - delete aes; aes = nullptr; if (key_len != 0) { - aes = new AESSmall256(); + aes = std::unique_ptr(new AESSmall256()); aes->setKey(key_bytes, key_len); } } @@ -231,12 +231,11 @@ void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes // Generic implementation of AES-CTR encryption. void CryptoEngine::encryptAESCtr(CryptoKey _key, uint8_t *_nonce, size_t numBytes, uint8_t *bytes) { - delete ctr; - ctr = nullptr; + std::unique_ptr ctr; if (_key.length == 16) - ctr = new CTR(); + ctr = std::unique_ptr(new CTR()); else - ctr = new CTR(); + ctr = std::unique_ptr(new CTR()); ctr->setKey(_key.bytes, _key.length); static uint8_t scratch[MAX_BLOCKSIZE]; memcpy(scratch, bytes, numBytes); diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h index 7689006ab..19d572355 100644 --- a/src/mesh/CryptoEngine.h +++ b/src/mesh/CryptoEngine.h @@ -5,6 +5,7 @@ #include "configuration.h" #include "mesh-pb-constants.h" #include +#include extern concurrency::Lock *cryptLock; @@ -48,7 +49,7 @@ class CryptoEngine virtual void aesSetKey(const uint8_t *key, size_t key_len); virtual void aesEncrypt(uint8_t *in, uint8_t *out); - AESSmall256 *aes = NULL; + std::unique_ptr aes = nullptr; #endif @@ -77,7 +78,6 @@ class CryptoEngine /** Our per packet nonce */ uint8_t nonce[16] = {0}; CryptoKey key = {}; - CTRCommon *ctr = NULL; #if !(MESHTASTIC_EXCLUDE_PKI) uint8_t shared_key[32] = {0}; uint8_t private_key[32] = {0}; diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 7c73b56cd..4fec06da4 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -263,6 +263,7 @@ template void LR11x0Interface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } @@ -298,6 +299,38 @@ template bool LR11x0Interface::isActivelyReceiving() RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } +#ifdef LR11X0_AGC_RESET +template void LR11x0Interface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)"); + + // 1. Warm sleep — powers down the analog frontend, resetting AGC state + lora.sleep(true, 0); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true); + + // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) + // calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation) + uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true); + + // 4. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); + + // 5. Re-apply RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} +#endif + template bool LR11x0Interface::sleep() { // \todo Display actual typename of the adapter, not just `LR11x0` diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 840184bbf..1a6b92520 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -27,6 +27,10 @@ template class LR11x0Interface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } +#ifdef LR11X0_AGC_RESET + void resetAGC() override; +#endif + protected: /** * Specific module instance diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 63f401d18..9d579d4f1 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -106,6 +106,18 @@ class MeshModule /* We allow modules to ignore a request without sending an error if they have a specific reason for it. */ bool ignoreRequest = false; + /** + * Check if the current request is a multi-hop broadcast. Modules should call this in allocReply() + * and return NULL to prevent reply storms from broadcast requests that have already been relayed. + */ + bool isMultiHopBroadcastRequest() + { + if (currentRequest && isBroadcast(currentRequest->to) && currentRequest->hop_limit < currentRequest->hop_start) { + return true; + } + return false; + } + /** If a bound channel name is set, we will only accept received packets that come in on that channel. * A special exception (FIXME, not sure if this is a good idea) - packets that arrive on the local interface * are allowed on any channel (this lets the local user do anything). diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 0ba60f1e2..360253041 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -143,9 +143,8 @@ uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_ digitalWrite(rst, HIGH); delay(10); - uint32_t ID = 0; - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); - ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice + readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); + uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice return ID; } @@ -322,9 +321,9 @@ NodeDB::NodeDB() // Uncomment below to always enable UDP broadcasts // config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; - // If we are setup to broadcast on the default channel, ensure that the telemetry intervals are coerced to the minimum value - // of 30 minutes or more - if (channels.isDefaultChannel(channels.getPrimaryIndex())) { + // If we are setup to broadcast on any default channel slot (with default frequency slot semantics), + // ensure that the telemetry intervals are coerced to the minimum value of 30 minutes or more. + if (channels.hasDefaultChannel()) { LOG_DEBUG("Coerce telemetry to min of 30 minutes on defaults"); moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs); @@ -574,6 +573,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif +#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200) + config.display.enable_message_bubbles = true; +#endif + #ifdef USERPREFS_CONFIG_DEVICE_ROLE // Restrict ROUTER*, LOST AND FOUND roles for security reasons if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, @@ -810,26 +813,28 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_external_notification = true; -#if defined(PIN_BUZZER) +#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) moduleConfig.external_notification.enabled = true; +#endif +#if defined(PIN_BUZZER) moduleConfig.external_notification.output_buzzer = PIN_BUZZER; moduleConfig.external_notification.use_pwm = true; moduleConfig.external_notification.alert_message_buzzer = true; - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif #if defined(PIN_VIBRATION) - moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output_vibra = PIN_VIBRATION; moduleConfig.external_notification.alert_message_vibra = true; moduleConfig.external_notification.output_ms = 500; - moduleConfig.external_notification.nag_timeout = 2; #endif #if defined(LED_NOTIFICATION) - moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.output = LED_NOTIFICATION; moduleConfig.external_notification.active = LED_STATE_ON; moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; +#endif +#if defined(PIN_VIBRATION) + moduleConfig.external_notification.nag_timeout = 2; +#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif @@ -1404,6 +1409,15 @@ void NodeDB::loadFromDisk() if (portduino_config.has_configDisplayMode) { config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode; } + if (portduino_config.has_statusMessage) { + moduleConfig.has_statusmessage = true; + strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(), + sizeof(moduleConfig.statusmessage.node_status)); + moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0'; + } + if (portduino_config.enable_UDP) { + config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST; + } #endif } @@ -1544,6 +1558,7 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) moduleConfig.has_ambient_lighting = true; moduleConfig.has_audio = true; moduleConfig.has_paxcounter = true; + moduleConfig.has_statusmessage = true; success &= saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig); @@ -1758,7 +1773,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) info->has_device_metrics = false; info->has_position = false; info->user.public_key.size = 0; - info->user.public_key.bytes[0] = 0; + memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes)); } else { /* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with * public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM! @@ -2213,8 +2228,8 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, } else if (location == meshtastic_AdminMessage_BackupLocation_SD) { // TODO: After more mainline SD card support } - return success; #endif + return success; } /// Record an error that should be reported via analytics diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index b4af707ae..845a936d4 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -90,9 +90,9 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd bool seenRecently = (found != NULL); // If found -> the packet was seen recently // Check for hop_limit upgrade scenario - if (seenRecently && wasUpgraded && found->hop_limit < p->hop_limit) { - LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, found->hop_limit, - p->hop_limit); + if (seenRecently && wasUpgraded && getHighestHopLimit(*found) < p->hop_limit) { + LOG_DEBUG("Packet History - Hop limit upgrade: packet 0x%08x from hop_limit=%d to hop_limit=%d", p->id, + getHighestHopLimit(*found), p->hop_limit); *wasUpgraded = true; } else if (wasUpgraded) { *wasUpgraded = false; // Initialize to false if not an upgrade @@ -439,7 +439,7 @@ void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, cons } // Getters and setters for hop limit fields packed in hop_limit -inline uint8_t PacketHistory::getHighestHopLimit(PacketRecord &r) +inline uint8_t PacketHistory::getHighestHopLimit(const PacketRecord &r) { return r.hop_limit & HOP_LIMIT_HIGHEST_MASK; } @@ -449,7 +449,7 @@ inline void PacketHistory::setHighestHopLimit(PacketRecord &r, uint8_t hopLimit) r.hop_limit = (r.hop_limit & ~HOP_LIMIT_HIGHEST_MASK) | (hopLimit & HOP_LIMIT_HIGHEST_MASK); } -inline uint8_t PacketHistory::getOurTxHopLimit(PacketRecord &r) +inline uint8_t PacketHistory::getOurTxHopLimit(const PacketRecord &r) { return (r.hop_limit & HOP_LIMIT_OUR_TX_MASK) >> HOP_LIMIT_OUR_TX_SHIFT; } diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index 5fbad2dc9..9b6a93280 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -43,9 +43,9 @@ class PacketHistory * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); - uint8_t getHighestHopLimit(PacketRecord &r); + uint8_t getHighestHopLimit(const PacketRecord &r); void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit); - uint8_t getOurTxHopLimit(PacketRecord &r); + uint8_t getOurTxHopLimit(const PacketRecord &r); void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit); PacketHistory(const PacketHistory &); // non construction-copyable diff --git a/src/mesh/ProtobufModule.h b/src/mesh/ProtobufModule.h index 725477eae..27e653efe 100644 --- a/src/mesh/ProtobufModule.h +++ b/src/mesh/ProtobufModule.h @@ -82,7 +82,6 @@ template class ProtobufModule : protected SinglePortModule // it would be better to update even if the message was destined to others. auto &p = mp.decoded; - LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, p.payload.size); T scratch; T *decoded = NULL; @@ -90,6 +89,8 @@ template class ProtobufModule : protected SinglePortModule memset(&scratch, 0, sizeof(scratch)); if (pb_decode_from_bytes(p.payload.bytes, p.payload.size, fields, &scratch)) { decoded = &scratch; + LOG_INFO("Received %s from=0x%0x, id=0x%x, portnum=%d, payloadlen=%d", name, mp.from, mp.id, p.portnum, + p.payload.size); } else { LOG_ERROR("Error decoding proto module!"); // if we can't decode it, nobody can process it! diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 0c12401ca..b3aa72f7a 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -301,6 +301,7 @@ void RF95Interface::startReceive() // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); } bool RF95Interface::isChannelActive() diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index bbd766329..e8202d9b0 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -227,23 +227,19 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1]; // Global LoRa radio type LoRaRadioType radioType = NO_RADIO; -extern RadioInterface *rIf; extern RadioLibHal *RadioLibHAL; #if defined(HW_SPI1_DEVICE) && defined(ARCH_ESP32) extern SPIClass SPI1; #endif -bool initLoRa() +std::unique_ptr initLoRa() { - if (rIf != nullptr) { - delete rIf; - rIf = nullptr; - } + std::unique_ptr rIf = nullptr; #if ARCH_PORTDUINO - SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); + SPISettings loraSpiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0); #else - SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); + SPISettings loraSpiSettings(4000000, MSBFIRST, SPI_MODE0); #endif #ifdef ARCH_PORTDUINO @@ -252,26 +248,26 @@ bool initLoRa() RADIOLIB_PIN_TYPE busy) { switch (portduino_config.lora_module) { case use_rf95: - return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new RF95Interface(hal, cs, irq, rst, busy)); case use_sx1262: - return (RadioInterface *)new SX1262Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1262Interface(hal, cs, irq, rst, busy)); case use_sx1268: - return (RadioInterface *)new SX1268Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1268Interface(hal, cs, irq, rst, busy)); case use_sx1280: - return (RadioInterface *)new SX1280Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new SX1280Interface(hal, cs, irq, rst, busy)); case use_lr1110: - return (RadioInterface *)new LR1110Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1110Interface(hal, cs, irq, rst, busy)); case use_lr1120: - return (RadioInterface *)new LR1120Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1120Interface(hal, cs, irq, rst, busy)); case use_lr1121: - return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LR1121Interface(hal, cs, irq, rst, busy)); case use_llcc68: - return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy); + return std::unique_ptr(new LLCC68Interface(hal, cs, irq, rst, busy)); case use_simradio: - return (RadioInterface *)new SimRadio; + return std::unique_ptr(new SimRadio); default: assert(0); // shouldn't happen - return (RadioInterface *)nullptr; + return std::unique_ptr(nullptr); } }; @@ -284,7 +280,7 @@ bool initLoRa() delete RadioLibHAL; RadioLibHAL = nullptr; } - RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + RadioLibHAL = new LockingArduinoHal(SPI, loraSpiSettings); } rIf = loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin, @@ -292,27 +288,28 @@ bool initLoRa() if (!rIf->init()) { LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str()); - delete rIf; - rIf = NULL; + rIf = nullptr; exit(EXIT_FAILURE); } else { LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str()); } #elif defined(HW_SPI1_DEVICE) - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings); + LockingArduinoHal *loraHal = new LockingArduinoHal(SPI1, loraSpiSettings); + RadioLibHAL = loraHal; #else // HW_SPI1_DEVICE - LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI, spiSettings); + LockingArduinoHal *loraHal = new LockingArduinoHal(SPI, loraSpiSettings); + RadioLibHAL = loraHal; #endif // radio init MUST BE AFTER service.init, so we have our radio config settings (from nodedb init) #if defined(USE_STM32WLx) if (!rIf) { - rIf = new STM32WLE5JCInterface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr( + new STM32WLE5JCInterface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No STM32WL radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("STM32WL init success"); radioType = STM32WLx_RADIO; @@ -322,11 +319,10 @@ bool initLoRa() #if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1); + rIf = std::unique_ptr(new RF95Interface(loraHal, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1)); if (!rIf->init()) { LOG_WARN("No RF95 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("RF95 init success"); radioType = RF95_RADIO; @@ -336,17 +332,17 @@ bool initLoRa() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && !defined(TCXO_OPTIONAL) && RADIOLIB_EXCLUDE_SX126X != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); #ifdef SX126X_DIO3_TCXO_VOLTAGE sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); #endif if (!sxIf->init()) { LOG_WARN("No SX1262 radio"); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success"); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1262_RADIO; } } @@ -355,26 +351,25 @@ bool initLoRa() #if defined(USE_SX1262) && !defined(ARCH_PORTDUINO) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage - auto *sxIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1262 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1262_RADIO; } } if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // If specified TCXO voltage fails, attempt to use DIO3 as a reference instead - rIf = new SX1262Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new SX1262Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1262 radio with XTAL, Vref 0.0V"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1262 init success, XTAL, Vref 0.0V"); radioType = SX1262_RADIO; @@ -386,25 +381,24 @@ bool initLoRa() #if defined(SX126X_DIO3_TCXO_VOLTAGE) && defined(TCXO_OPTIONAL) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { // try using the specified TCXO voltage - auto *sxIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + auto sxIf = + std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); sxIf->setTCXOVoltage(SX126X_DIO3_TCXO_VOLTAGE); if (!sxIf->init()) { LOG_WARN("No SX1268 radio with TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - delete sxIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1268 init success, TCXO, Vref %fV", SX126X_DIO3_TCXO_VOLTAGE); - rIf = sxIf; + rIf = std::move(sxIf); radioType = SX1268_RADIO; } } #endif if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new SX1268Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new SX1268Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1268 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1268 init success"); radioType = SX1268_RADIO; @@ -414,11 +408,10 @@ bool initLoRa() #if defined(USE_LLCC68) if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LLCC68Interface(RadioLibHAL, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY); + rIf = std::unique_ptr(new LLCC68Interface(loraHal, SX126X_CS, SX126X_DIO1, SX126X_RESET, SX126X_BUSY)); if (!rIf->init()) { LOG_WARN("No LLCC68 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LLCC68 init success"); radioType = LLCC68_RADIO; @@ -428,11 +421,11 @@ bool initLoRa() #if defined(USE_LR1110) && RADIOLIB_EXCLUDE_LR11X0 != 1 if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) { - rIf = new LR1110Interface(RadioLibHAL, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN); + rIf = std::unique_ptr( + new LR1110Interface(loraHal, LR1110_SPI_NSS_PIN, LR1110_IRQ_PIN, LR1110_NRESET_PIN, LR1110_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1110 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1110 init success"); radioType = LR1110_RADIO; @@ -442,11 +435,11 @@ bool initLoRa() #if defined(USE_LR1120) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { - rIf = new LR1120Interface(RadioLibHAL, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN); + rIf = std::unique_ptr( + new LR1120Interface(loraHal, LR1120_SPI_NSS_PIN, LR1120_IRQ_PIN, LR1120_NRESET_PIN, LR1120_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1120 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1120 init success"); radioType = LR1120_RADIO; @@ -456,11 +449,11 @@ bool initLoRa() #if defined(USE_LR1121) && RADIOLIB_EXCLUDE_LR11X0 != 1 if (!rIf) { - rIf = new LR1121Interface(RadioLibHAL, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN); + rIf = std::unique_ptr( + new LR1121Interface(loraHal, LR1121_SPI_NSS_PIN, LR1121_IRQ_PIN, LR1121_NRESET_PIN, LR1121_BUSY_PIN)); if (!rIf->init()) { LOG_WARN("No LR1121 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("LR1121 init success"); radioType = LR1121_RADIO; @@ -470,11 +463,10 @@ bool initLoRa() #if defined(USE_SX1280) && RADIOLIB_EXCLUDE_SX128X != 1 if (!rIf) { - rIf = new SX1280Interface(RadioLibHAL, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY); + rIf = std::unique_ptr(new SX1280Interface(loraHal, SX128X_CS, SX128X_DIO1, SX128X_RESET, SX128X_BUSY)); if (!rIf->init()) { LOG_WARN("No SX1280 radio"); - delete rIf; - rIf = NULL; + rIf = nullptr; } else { LOG_INFO("SX1280 init success"); radioType = SX1280_RADIO; @@ -496,7 +488,7 @@ bool initLoRa() rebootAtMsec = millis() + 5000; } } - return rIf != nullptr; + return rIf; } void initRegion() @@ -943,9 +935,9 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } else if (!devicestate.owner.is_licensed) { // we have an array of PA gain values. Find the highest power setting that works. - for (int radio_dbm = 0; radio_dbm < num_pa_points; radio_dbm++) { + for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) { if (((radio_dbm + tx_gain[radio_dbm]) > power) || - ((radio_dbm == (num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + ((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { // we've exceeded the power limit, or hit the max we can do LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[radio_dbm]); power -= tx_gain[radio_dbm]; diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index cb092bc6d..1fe3dd7b0 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -6,6 +6,7 @@ #include "PointerQueue.h" #include "airtime.h" #include "error.h" +#include // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; @@ -279,7 +280,7 @@ class RadioInterface } }; -bool initLoRa(); +std::unique_ptr initLoRa(); /// Debug printing for packets void printPacket(const char *prefix, const meshtastic_MeshPacket *p); diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 80e51b8bc..30cd587da 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -453,8 +453,11 @@ void RadioLibInterface::handleReceiveInterrupt() } #endif if (state != RADIOLIB_ERR_NONE) { - LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, - radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags); + // Log PacketHeader similar to RadioInterface::printPacket so we can try to match RX errors to other packets in the logs. + LOG_ERROR("Ignore received packet due to error=%d (maybe id=0x%08x fr=0x%08x to=0x%08x flags=0x%02x rxSNR=%g rxRSSI=%i " + "nextHop=0x%x relay=0x%x)", + state, radioBuffer.header.id, radioBuffer.header.from, radioBuffer.header.to, radioBuffer.header.flags, + iface->getSNR(), lround(iface->getRSSI()), radioBuffer.header.next_hop, radioBuffer.header.relay_node); rxBad++; airTime->logAirtime(RX_ALL_LOG, rxMsec); @@ -518,6 +521,27 @@ void RadioLibInterface::startReceive() powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } +void RadioLibInterface::pollMissedIrqs() +{ + // RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges. + if (isReceiving) { + checkRxDoneIrqFlag(); + } +} + +void RadioLibInterface::resetAGC() +{ + // Base implementation: no-op. Override in chip-specific subclasses. +} + +void RadioLibInterface::checkRxDoneIrqFlag() +{ + if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { + LOG_WARN("caught missed RX_DONE"); + notify(ISR_RX, true); + } +} + void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 833c88710..ca3d78503 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -19,6 +19,8 @@ // In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving #define MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS (RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1 << RADIOLIB_IRQ_PREAMBLE_DETECTED)) +#define AGC_RESET_INTERVAL_MS (60 * 1000) // 60 seconds + /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ @@ -112,6 +114,18 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ virtual void enableInterrupt(void (*)()) = 0; + /** + * Poll as a backup to catch missed edge-triggered interrupts. + */ + void pollMissedIrqs(); + + /** + * Reset AGC by power-cycling the analog frontend. + * Subclasses override with chip-specific calibration sequences. + * Safe to call periodically — skips if currently sending or receiving. + */ + virtual void resetAGC(); + /** * Debugging counts */ @@ -264,4 +278,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; + + void checkRxDoneIrqFlag(); }; diff --git a/src/mesh/Router.h b/src/mesh/Router.h index dbe6f4f39..dbb5e5802 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -8,6 +8,7 @@ #include "PointerQueue.h" #include "RadioInterface.h" #include "concurrency/OSThread.h" +#include /** * A mesh aware router that supports multiple interfaces. @@ -20,7 +21,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory PointerQueue fromRadioQueue; protected: - RadioInterface *iface = NULL; + std::unique_ptr iface = nullptr; public: /** @@ -32,7 +33,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory /** * Currently we only allow one interface, that may change in the future */ - void addInterface(RadioInterface *_iface) { iface = _iface; } + void addInterface(std::unique_ptr _iface) { iface = std::move(_iface); } /** * do idle processing diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 9dfc46bee..91be8685d 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -6,6 +6,10 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #endif +#if defined(USE_GC1109_PA) && defined(ARCH_ESP32) +#include +#include +#endif #include "Throttle.h" @@ -55,14 +59,33 @@ template bool SX126xInterface::init() #if defined(USE_GC1109_PA) // GC1109 FEM chip initialization // See variant.h for full pin mapping and control logic documentation + // + // On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in + // enableLoraInterrupt). We configure GPIO registers before releasing the hold + // so the pad transitions atomically from held-HIGH to register-HIGH with no + // power glitch. On cold boot the hold_dis is a harmless no-op. // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + + // TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait + // for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement: + // "VBAT must be prior to CSD/CPS/CTX for the power on sequence" + // On deep sleep wake the LDO was held on via RTC latch, so no delay needed. +#if defined(ARCH_ESP32) + if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) { + delayMicroseconds(1000); + } +#else + delayMicroseconds(1000); +#endif // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX pinMode(LORA_PA_EN, OUTPUT); digitalWrite(LORA_PA_EN, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN); // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH @@ -171,6 +194,14 @@ template bool SX126xInterface::init() LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } + // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity. + // Sets bit 0 of register 0x8B5. + if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) { + LOG_INFO("Applied SX1262 register 0x8B5 patch for RX improvement"); + } else { + LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for RX improvement"); + } + #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms uint8_t crcLSB = 0; @@ -328,6 +359,7 @@ template void SX126xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } @@ -399,6 +431,61 @@ template bool SX126xInterface::sleep() return true; } +template void SX126xInterface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); + + // 1. Warm sleep — powers down the entire analog frontend, resetting AGC state. + // A plain standby→startReceive cycle does NOT reset the AGC. + lora.sleep(true); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_SX126X_STANDBY_RC, true); + + // 3. Calibrate all blocks (ADC, PLL, image, RC oscillators) + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + + // 4. Wait for calibration to complete (BUSY pin goes low) + module.hal->delay(5); + uint32_t start = millis(); + while (module.hal->digitalRead(module.getGpio())) { + if (millis() - start > 50) + break; + module.hal->yield(); + } + + if (module.hal->digitalRead(module.getGpio())) { + LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms"); + startReceive(); + return; + } + + // 5. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImage(getFreq()); + + // Re-apply settings that calibration may have reset + + // DIO2 as RF switch +#ifdef SX126X_DIO2_AS_RF_SWITCH + lora.setDio2AsRfSwitch(true); +#elif defined(ARCH_PORTDUINO) + if (portduino_config.dio2_as_rf_switch) + lora.setDio2AsRfSwitch(true); +#endif + + // RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} + /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b8f16ac6d..67625e115 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -28,6 +28,8 @@ template class SX126xInterface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } + void resetAGC() override; + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } protected: diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 9fcedfe49..0e882ef05 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -271,6 +271,7 @@ template void SX128xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 20026767e..2d9230a21 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -27,7 +27,7 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) /** * Read any rx chars from the link and call handleRecStream */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen) { if (bufLen < 1) { // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time @@ -56,7 +56,7 @@ void StreamAPI::writeStream() } } -int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +int32_t StreamAPI::handleRecStream(const char *buf, uint16_t bufLen) { uint16_t index = 0; while (bufLen > index) { // Currently we never want to block diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 4ca2c197f..97e231f23 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -57,8 +57,8 @@ class StreamAPI : public PhoneAPI * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); - int32_t readStream(char *buf, uint16_t bufLen); - int32_t handleRecStream(char *buf, uint16_t bufLen); + int32_t readStream(const char *buf, uint16_t bufLen); + int32_t handleRecStream(const char *buf, uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream diff --git a/src/mesh/TransmitHistory.cpp b/src/mesh/TransmitHistory.cpp new file mode 100644 index 000000000..3dbabc635 --- /dev/null +++ b/src/mesh/TransmitHistory.cpp @@ -0,0 +1,202 @@ +#include "TransmitHistory.h" +#include "FSCommon.h" +#include "RTC.h" +#include "SPILock.h" +#include + +#ifdef FSCom + +TransmitHistory *transmitHistory = nullptr; + +TransmitHistory *TransmitHistory::getInstance() +{ + if (!transmitHistory) { + transmitHistory = new TransmitHistory(); + } + return transmitHistory; +} + +void TransmitHistory::loadFromDisk() +{ + spiLock->lock(); + auto file = FSCom.open(FILENAME, FILE_O_READ); + if (file) { + FileHeader header{}; + if (file.read((uint8_t *)&header, sizeof(header)) == sizeof(header) && header.magic == MAGIC && + header.version == VERSION && header.count <= MAX_ENTRIES) { + for (uint8_t i = 0; i < header.count; i++) { + Entry entry{}; + if (file.read((uint8_t *)&entry, sizeof(entry)) == sizeof(entry)) { + if (entry.epochSeconds > 0) { + history[entry.key] = entry.epochSeconds; + // Seed in-memory millis so throttle works even without RTC/GPS. + // Treating stored entries as "just sent" is safe — worst case the + // node waits one full interval before its first broadcast. + lastMillis[entry.key] = millis(); + } + } + } + LOG_INFO("TransmitHistory: loaded %u entries from disk", header.count); + } else { + LOG_WARN("TransmitHistory: invalid file header, starting fresh"); + } + file.close(); + } else { + LOG_INFO("TransmitHistory: no history file found, starting fresh"); + } + spiLock->unlock(); + dirty = false; +} + +void TransmitHistory::setLastSentToMesh(uint16_t key) +{ + lastMillis[key] = millis(); + uint32_t now = getTime(); + if (now >= 2) { + history[key] = now; + dirty = true; + // Don't flush to disk on every transmit — flash has limited write endurance. + // The in-memory lastMillis map handles throttle during normal operation. + // Disk is flushed: before deep sleep (sleep.cpp) and periodically here, + // throttled to at most once per 5 minutes. Always save the first time + // after boot so a crash-reboot loop can't avoid persisting. + if (lastDiskSave == 0 || !Throttle::isWithinTimespanMs(lastDiskSave, SAVE_INTERVAL_MS)) { + if (saveToDisk()) { + lastDiskSave = millis(); + } + } + } +} + +uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const +{ + auto it = history.find(key); + if (it != history.end()) { + return it->second; + } + return 0; +} + +uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const +{ + // Prefer runtime millis value (accurate within this boot) + auto mit = lastMillis.find(key); + if (mit != lastMillis.end()) { + return mit->second; + } + + // Fall back to epoch conversion (loaded from disk after reboot) + uint32_t storedEpoch = getLastSentToMeshEpoch(key); + if (storedEpoch == 0) { + return 0; // No stored time — module has never sent + } + + uint32_t now = getTime(); + if (now < 2) { + // No valid RTC time yet — can't convert to millis. Return 0 so throttle doesn't block. + return 0; + } + + if (storedEpoch > now) { + // Stored time is in the future (clock went backwards?) — treat as stale + return 0; + } + + uint32_t secondsAgo = now - storedEpoch; + uint32_t msAgo = secondsAgo * 1000; + + // Guard against overflow: if the transmit was very long ago, just return 0 (won't throttle) + if (secondsAgo > 86400 || msAgo / 1000 != secondsAgo) { + return 0; + } + + // Convert to a millis()-relative timestamp: millis() - msAgo + // This gives a value that, when passed to Throttle::isWithinTimespanMs(value, interval), + // correctly reports whether the transmit was within interval ms. + return millis() - msAgo; +} + +bool TransmitHistory::saveToDisk() +{ + if (!dirty) { + return true; + } + + spiLock->lock(); + + FSCom.mkdir("/prefs"); + + // Remove old file first + if (FSCom.exists(FILENAME)) { + FSCom.remove(FILENAME); + } + + auto file = FSCom.open(FILENAME, FILE_O_WRITE); + if (file) { + FileHeader header{}; + header.magic = MAGIC; + header.version = VERSION; + header.count = (uint8_t)min((size_t)MAX_ENTRIES, history.size()); + + file.write((uint8_t *)&header, sizeof(header)); + + uint8_t written = 0; + for (auto &pair : history) { + if (written >= MAX_ENTRIES) + break; + Entry entry{}; + entry.key = pair.first; + entry.epochSeconds = pair.second; + file.write((uint8_t *)&entry, sizeof(entry)); + written++; + } + file.flush(); + file.close(); + LOG_DEBUG("TransmitHistory: saved %u entries to disk", written); + dirty = false; + spiLock->unlock(); + return true; + } else { + LOG_WARN("TransmitHistory: failed to open file for writing"); + } + + spiLock->unlock(); + return false; +} + +#else +// No filesystem available — provide stub with in-memory tracking +TransmitHistory *transmitHistory = nullptr; + +TransmitHistory *TransmitHistory::getInstance() +{ + if (!transmitHistory) { + transmitHistory = new TransmitHistory(); + } + return transmitHistory; +} + +void TransmitHistory::loadFromDisk() {} + +void TransmitHistory::setLastSentToMesh(uint16_t key) +{ + lastMillis[key] = millis(); +} + +uint32_t TransmitHistory::getLastSentToMeshEpoch(uint16_t key) const +{ + return 0; +} + +uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const +{ + auto mit = lastMillis.find(key); + return (mit != lastMillis.end()) ? mit->second : 0; +} + +bool TransmitHistory::saveToDisk() +{ + return true; +} + +#endif diff --git a/src/mesh/TransmitHistory.h b/src/mesh/TransmitHistory.h new file mode 100644 index 000000000..01201eaac --- /dev/null +++ b/src/mesh/TransmitHistory.h @@ -0,0 +1,88 @@ +#pragma once + +#include "configuration.h" +#include +#include + +/** + * TransmitHistory persists the last broadcast transmit time (epoch seconds) per portnum + * to the filesystem so that throttle checks survive reboots/crashes. + * + * On boot, modules call getLastSentToMeshMillis() to recover a millis()-relative timestamp + * from the stored epoch time, which plugs directly into existing throttle logic. + * + * On every broadcast transmit, modules call setLastSentToMesh() which updates the + * in-memory cache and flushes to disk. + * + * Keys are meshtastic_PortNum values (one entry per portnum). + */ + +#include "mesh/generated/meshtastic/portnums.pb.h" + +class TransmitHistory +{ + public: + static TransmitHistory *getInstance(); + + /** + * Load persisted transmit times from disk. Call once during init after filesystem is ready. + */ + void loadFromDisk(); + + /** + * Record that a broadcast was sent for the given key right now. + * Stores epoch seconds and flushes to disk. + */ + void setLastSentToMesh(uint16_t key); + + /** + * Get the last transmit epoch seconds for a given key, or 0 if unknown. + */ + uint32_t getLastSentToMeshEpoch(uint16_t key) const; + + /** + * Convert a stored epoch timestamp into a millis()-relative timestamp suitable + * for use with Throttle::isWithinTimespanMs(). + * + * Returns 0 if no valid time is stored or if the stored time is in the future + * (which shouldn't happen but guards against clock weirdness). + * + * Example: if the stored epoch is 300 seconds ago, and millis() is currently 10000, + * this returns 10000 - 300000 (wrapped appropriately for uint32_t arithmetic). + */ + uint32_t getLastSentToMeshMillis(uint16_t key) const; + + /** + * Flush dirty entries to disk. Called periodically or on demand. + * + * @return true if the data is persisted (or there was nothing to write), false on write/open failure. + */ + bool saveToDisk(); + + private: + TransmitHistory() = default; + + static constexpr const char *FILENAME = "/prefs/transmit_history.dat"; + static constexpr uint32_t MAGIC = 0x54485354; // "THST" + static constexpr uint8_t VERSION = 1; + static constexpr uint8_t MAX_ENTRIES = 16; + static constexpr uint32_t SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + + struct __attribute__((packed)) Entry { + uint16_t key; + uint32_t epochSeconds; + }; + + struct __attribute__((packed)) FileHeader { + uint32_t magic; + uint8_t version; + uint8_t count; + }; + + std::map history; // key -> epoch seconds (for disk persistence) + std::map lastMillis; // key -> millis() value (for runtime throttle) + bool dirty = false; + uint32_t lastDiskSave = 0; // millis() of last disk flush +}; + +extern TransmitHistory *transmitHistory; diff --git a/src/mesh/aes-ccm.cpp b/src/mesh/aes-ccm.cpp index 420d80e9a..5ed7ff928 100644 --- a/src/mesh/aes-ccm.cpp +++ b/src/mesh/aes-ccm.cpp @@ -33,7 +33,7 @@ static int constant_time_compare(const void *a_, const void *b_, size_t len) d |= (a[i] ^ b[i]); } /* Constant time bit arithmetic to convert d > 0 to -1 and d = 0 to 0. */ - return (1 & ((d - 1) >> 8)) - 1; + return (1 & (((unsigned int)d - 1) >> 8)) - 1; } static void WPA_PUT_BE16(uint8_t *a, uint16_t val) diff --git a/src/mesh/api/PacketAPI.h b/src/mesh/api/PacketAPI.h index fc08ab209..357eb05c2 100644 --- a/src/mesh/api/PacketAPI.h +++ b/src/mesh/api/PacketAPI.h @@ -17,7 +17,7 @@ class PacketAPI : public PhoneAPI, public concurrency::OSThread virtual int32_t runOnce(); protected: - PacketAPI(PacketServer *_server); + explicit PacketAPI(PacketServer *_server); // Check the current underlying physical queue to see if the client is fetching packets bool checkIsConnected() override; diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 01d3fa910..d0bd09800 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -36,6 +36,9 @@ PB_BIND(meshtastic_SCD4X_config, meshtastic_SCD4X_config, AUTO) PB_BIND(meshtastic_SEN5X_config, meshtastic_SEN5X_config, AUTO) +PB_BIND(meshtastic_SCD30_config, meshtastic_SCD30_config, AUTO) + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index f545ed9bf..d529336c8 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -79,7 +79,11 @@ typedef enum _meshtastic_AdminMessage_ModuleConfigType { /* TODO: REPLACE */ meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG = 12, /* TODO: REPLACE */ - meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13 + meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG = 13, + /* Traffic management module config */ + meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG = 14, + /* TAK module config */ + meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG = 15 } meshtastic_AdminMessage_ModuleConfigType; typedef enum _meshtastic_AdminMessage_BackupLocation { @@ -204,6 +208,27 @@ typedef struct _meshtastic_SEN5X_config { bool set_one_shot_mode; } meshtastic_SEN5X_config; +typedef struct _meshtastic_SCD30_config { + /* Set Automatic self-calibration enabled */ + bool has_set_asc; + bool set_asc; + /* Recalibration target CO2 concentration in ppm (FRC or ASC) */ + bool has_set_target_co2_conc; + uint32_t set_target_co2_conc; + /* Reference temperature in degC */ + bool has_set_temperature; + float set_temperature; + /* Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure) */ + bool has_set_altitude; + uint32_t set_altitude; + /* Power mode for sensor (true for low power, false for normal) */ + bool has_set_measurement_interval; + uint32_t set_measurement_interval; + /* Perform a factory reset of the sensor */ + bool has_soft_reset; + bool soft_reset; +} meshtastic_SCD30_config; + typedef struct _meshtastic_SensorConfig { /* SCD4X CO2 Sensor configuration */ bool has_scd4x_config; @@ -211,6 +236,9 @@ typedef struct _meshtastic_SensorConfig { /* SEN5X PM Sensor configuration */ bool has_sen5x_config; meshtastic_SEN5X_config sen5x_config; + /* SCD30 CO2 Sensor configuration */ + bool has_scd30_config; + meshtastic_SCD30_config scd30_config; } meshtastic_SensorConfig; typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; @@ -369,8 +397,8 @@ extern "C" { #define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1)) #define _meshtastic_AdminMessage_ModuleConfigType_MIN meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG -#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG+1)) +#define _meshtastic_AdminMessage_ModuleConfigType_MAX meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG +#define _meshtastic_AdminMessage_ModuleConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ModuleConfigType)(meshtastic_AdminMessage_ModuleConfigType_TAK_CONFIG+1)) #define _meshtastic_AdminMessage_BackupLocation_MIN meshtastic_AdminMessage_BackupLocation_FLASH #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD @@ -398,6 +426,7 @@ extern "C" { + /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} @@ -406,9 +435,10 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} #define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0, 0} #define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default} +#define meshtastic_SensorConfig_init_default {false, meshtastic_SCD4X_config_init_default, false, meshtastic_SEN5X_config_init_default, false, meshtastic_SCD30_config_init_default} #define meshtastic_SCD4X_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_default {false, 0, false, 0} +#define meshtastic_SCD30_config_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} #define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_AdminMessage_OTAEvent_init_zero {_meshtastic_OTAMode_MIN, {0, {0}}} @@ -416,9 +446,10 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} #define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0, 0} #define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} -#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero} +#define meshtastic_SensorConfig_init_zero {false, meshtastic_SCD4X_config_init_zero, false, meshtastic_SEN5X_config_init_zero, false, meshtastic_SCD30_config_init_zero} #define meshtastic_SCD4X_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_SEN5X_config_init_zero {false, 0, false, 0} +#define meshtastic_SCD30_config_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_AdminMessage_InputEvent_event_code_tag 1 @@ -449,8 +480,15 @@ extern "C" { #define meshtastic_SCD4X_config_set_power_mode_tag 7 #define meshtastic_SEN5X_config_set_temperature_tag 1 #define meshtastic_SEN5X_config_set_one_shot_mode_tag 2 +#define meshtastic_SCD30_config_set_asc_tag 1 +#define meshtastic_SCD30_config_set_target_co2_conc_tag 2 +#define meshtastic_SCD30_config_set_temperature_tag 3 +#define meshtastic_SCD30_config_set_altitude_tag 4 +#define meshtastic_SCD30_config_set_measurement_interval_tag 5 +#define meshtastic_SCD30_config_soft_reset_tag 6 #define meshtastic_SensorConfig_scd4x_config_tag 1 #define meshtastic_SensorConfig_sen5x_config_tag 2 +#define meshtastic_SensorConfig_scd30_config_tag 3 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -640,11 +678,13 @@ X(a, STATIC, OPTIONAL, UINT32, security_number, 4) #define meshtastic_SensorConfig_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, MESSAGE, scd4x_config, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) +X(a, STATIC, OPTIONAL, MESSAGE, sen5x_config, 2) \ +X(a, STATIC, OPTIONAL, MESSAGE, scd30_config, 3) #define meshtastic_SensorConfig_CALLBACK NULL #define meshtastic_SensorConfig_DEFAULT NULL #define meshtastic_SensorConfig_scd4x_config_MSGTYPE meshtastic_SCD4X_config #define meshtastic_SensorConfig_sen5x_config_MSGTYPE meshtastic_SEN5X_config +#define meshtastic_SensorConfig_scd30_config_MSGTYPE meshtastic_SCD30_config #define meshtastic_SCD4X_config_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ @@ -663,6 +703,16 @@ X(a, STATIC, OPTIONAL, BOOL, set_one_shot_mode, 2) #define meshtastic_SEN5X_config_CALLBACK NULL #define meshtastic_SEN5X_config_DEFAULT NULL +#define meshtastic_SCD30_config_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, BOOL, set_asc, 1) \ +X(a, STATIC, OPTIONAL, UINT32, set_target_co2_conc, 2) \ +X(a, STATIC, OPTIONAL, FLOAT, set_temperature, 3) \ +X(a, STATIC, OPTIONAL, UINT32, set_altitude, 4) \ +X(a, STATIC, OPTIONAL, UINT32, set_measurement_interval, 5) \ +X(a, STATIC, OPTIONAL, BOOL, soft_reset, 6) +#define meshtastic_SCD30_config_CALLBACK NULL +#define meshtastic_SCD30_config_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_AdminMessage_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_AdminMessage_OTAEvent_msg; @@ -673,6 +723,7 @@ extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; extern const pb_msgdesc_t meshtastic_SensorConfig_msg; extern const pb_msgdesc_t meshtastic_SCD4X_config_msg; extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; +extern const pb_msgdesc_t meshtastic_SCD30_config_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg @@ -685,6 +736,7 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; #define meshtastic_SensorConfig_fields &meshtastic_SensorConfig_msg #define meshtastic_SCD4X_config_fields &meshtastic_SCD4X_config_msg #define meshtastic_SEN5X_config_fields &meshtastic_SEN5X_config_msg +#define meshtastic_SCD30_config_fields &meshtastic_SCD30_config_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size @@ -694,9 +746,10 @@ extern const pb_msgdesc_t meshtastic_SEN5X_config_msg; #define meshtastic_HamParameters_size 31 #define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 +#define meshtastic_SCD30_config_size 27 #define meshtastic_SCD4X_config_size 29 #define meshtastic_SEN5X_config_size 7 -#define meshtastic_SensorConfig_size 40 +#define meshtastic_SensorConfig_size 69 #define meshtastic_SharedContact_size 127 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/apponly.pb.h b/src/mesh/generated/meshtastic/apponly.pb.h index f4c33bd79..ce766878b 100644 --- a/src/mesh/generated/meshtastic/apponly.pb.h +++ b/src/mesh/generated/meshtastic/apponly.pb.h @@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size -#define meshtastic_ChannelSet_size 679 +#define meshtastic_ChannelSet_size 682 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp index 52a591f33..c554ca43c 100644 --- a/src/mesh/generated/meshtastic/config.pb.cpp +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -67,6 +67,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index d93f6fafa..c82dd5ff5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -318,6 +318,15 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset { meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9 } meshtastic_Config_LoRaConfig_ModemPreset; +typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode { + /* FEM_LNA is present but disabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED = 0, + /* FEM_LNA is present and enabled */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED = 1, + /* FEM_LNA is not present on the device */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT = 2 +} meshtastic_Config_LoRaConfig_FEM_LNA_Mode; + typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { /* Device generates a random PIN that will be shown on the screen of the device for pairing */ meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN = 0, @@ -505,6 +514,8 @@ typedef struct _meshtastic_Config_DisplayConfig { /* If false (default), the device will use short names for various display screens. If true, node names will show in long format */ bool use_long_node_name; + /* If true, the device will display message bubbles on screen. */ + bool enable_message_bubbles; } meshtastic_Config_DisplayConfig; /* Lora Config */ @@ -577,6 +588,8 @@ typedef struct _meshtastic_Config_LoRaConfig { bool ignore_mqtt; /* Sets the ok_to_mqtt bit on outgoing packets */ bool config_ok_to_mqtt; + /* Set where LORA FEM is enabled, disabled, or not present */ + meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode; } meshtastic_Config_LoRaConfig; typedef struct _meshtastic_Config_BluetoothConfig { @@ -696,6 +709,10 @@ extern "C" { #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO #define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1)) +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT +#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_FEM_LNA_Mode)(meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT+1)) + #define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN #define _meshtastic_Config_BluetoothConfig_PairingMode_ARRAYSIZE ((meshtastic_Config_BluetoothConfig_PairingMode)(meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN+1)) @@ -719,6 +736,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_modem_preset_ENUMTYPE meshtastic_Config_LoRaConfig_ModemPreset #define meshtastic_Config_LoRaConfig_region_ENUMTYPE meshtastic_Config_LoRaConfig_RegionCode +#define meshtastic_Config_LoRaConfig_fem_lna_mode_ENUMTYPE meshtastic_Config_LoRaConfig_FEM_LNA_Mode #define meshtastic_Config_BluetoothConfig_mode_ENUMTYPE meshtastic_Config_BluetoothConfig_PairingMode @@ -732,8 +750,8 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} -#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0} +#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_default {0} @@ -743,8 +761,8 @@ extern "C" { #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} -#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0} -#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} +#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0} +#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN} #define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0} #define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_zero {0} @@ -811,6 +829,7 @@ extern "C" { #define meshtastic_Config_DisplayConfig_compass_orientation_tag 11 #define meshtastic_Config_DisplayConfig_use_12h_clock_tag 12 #define meshtastic_Config_DisplayConfig_use_long_node_name_tag 13 +#define meshtastic_Config_DisplayConfig_enable_message_bubbles_tag 14 #define meshtastic_Config_LoRaConfig_use_preset_tag 1 #define meshtastic_Config_LoRaConfig_modem_preset_tag 2 #define meshtastic_Config_LoRaConfig_bandwidth_tag 3 @@ -829,6 +848,7 @@ extern "C" { #define meshtastic_Config_LoRaConfig_ignore_incoming_tag 103 #define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104 #define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105 +#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106 #define meshtastic_Config_BluetoothConfig_enabled_tag 1 #define meshtastic_Config_BluetoothConfig_mode_tag 2 #define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3 @@ -957,7 +977,8 @@ X(a, STATIC, SINGULAR, BOOL, heading_bold, 9) \ X(a, STATIC, SINGULAR, BOOL, wake_on_tap_or_motion, 10) \ X(a, STATIC, SINGULAR, UENUM, compass_orientation, 11) \ X(a, STATIC, SINGULAR, BOOL, use_12h_clock, 12) \ -X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) +X(a, STATIC, SINGULAR, BOOL, use_long_node_name, 13) \ +X(a, STATIC, SINGULAR, BOOL, enable_message_bubbles, 14) #define meshtastic_Config_DisplayConfig_CALLBACK NULL #define meshtastic_Config_DisplayConfig_DEFAULT NULL @@ -979,7 +1000,8 @@ X(a, STATIC, SINGULAR, FLOAT, override_frequency, 14) \ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \ X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \ X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \ -X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) +X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \ +X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) #define meshtastic_Config_LoRaConfig_CALLBACK NULL #define meshtastic_Config_LoRaConfig_DEFAULT NULL @@ -1035,8 +1057,8 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 #define meshtastic_Config_DeviceConfig_size 100 -#define meshtastic_Config_DisplayConfig_size 34 -#define meshtastic_Config_LoRaConfig_size 85 +#define meshtastic_Config_DisplayConfig_size 36 +#define meshtastic_Config_LoRaConfig_size 88 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 #define meshtastic_Config_NetworkConfig_size 204 #define meshtastic_Config_PositionConfig_size 62 diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 57e7df8fc..1d6cd32f9 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2362 +#define meshtastic_BackupPreferences_size 2429 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1737 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index f11b13419..8425c122a 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -90,6 +90,12 @@ typedef struct _meshtastic_LocalModuleConfig { /* StatusMessage Config */ bool has_statusmessage; meshtastic_ModuleConfig_StatusMessageConfig statusmessage; + /* The part of the config that is specific to the Traffic Management module */ + bool has_traffic_management; + meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; + /* TAK Config */ + bool has_tak; + meshtastic_ModuleConfig_TAKConfig tak; } meshtastic_LocalModuleConfig; @@ -99,9 +105,9 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_LocalConfig_init_default {false, meshtastic_Config_DeviceConfig_init_default, false, meshtastic_Config_PositionConfig_init_default, false, meshtastic_Config_PowerConfig_init_default, false, meshtastic_Config_NetworkConfig_init_default, false, meshtastic_Config_DisplayConfig_init_default, false, meshtastic_Config_LoRaConfig_init_default, false, meshtastic_Config_BluetoothConfig_init_default, 0, false, meshtastic_Config_SecurityConfig_init_default} -#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default} +#define meshtastic_LocalModuleConfig_init_default {false, meshtastic_ModuleConfig_MQTTConfig_init_default, false, meshtastic_ModuleConfig_SerialConfig_init_default, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_default, false, meshtastic_ModuleConfig_StoreForwardConfig_init_default, false, meshtastic_ModuleConfig_RangeTestConfig_init_default, false, meshtastic_ModuleConfig_TelemetryConfig_init_default, false, meshtastic_ModuleConfig_CannedMessageConfig_init_default, 0, false, meshtastic_ModuleConfig_AudioConfig_init_default, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_default, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_default, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_default, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_default, false, meshtastic_ModuleConfig_PaxcounterConfig_init_default, false, meshtastic_ModuleConfig_StatusMessageConfig_init_default, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_default, false, meshtastic_ModuleConfig_TAKConfig_init_default} #define meshtastic_LocalConfig_init_zero {false, meshtastic_Config_DeviceConfig_init_zero, false, meshtastic_Config_PositionConfig_init_zero, false, meshtastic_Config_PowerConfig_init_zero, false, meshtastic_Config_NetworkConfig_init_zero, false, meshtastic_Config_DisplayConfig_init_zero, false, meshtastic_Config_LoRaConfig_init_zero, false, meshtastic_Config_BluetoothConfig_init_zero, 0, false, meshtastic_Config_SecurityConfig_init_zero} -#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero} +#define meshtastic_LocalModuleConfig_init_zero {false, meshtastic_ModuleConfig_MQTTConfig_init_zero, false, meshtastic_ModuleConfig_SerialConfig_init_zero, false, meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero, false, meshtastic_ModuleConfig_StoreForwardConfig_init_zero, false, meshtastic_ModuleConfig_RangeTestConfig_init_zero, false, meshtastic_ModuleConfig_TelemetryConfig_init_zero, false, meshtastic_ModuleConfig_CannedMessageConfig_init_zero, 0, false, meshtastic_ModuleConfig_AudioConfig_init_zero, false, meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero, false, meshtastic_ModuleConfig_NeighborInfoConfig_init_zero, false, meshtastic_ModuleConfig_AmbientLightingConfig_init_zero, false, meshtastic_ModuleConfig_DetectionSensorConfig_init_zero, false, meshtastic_ModuleConfig_PaxcounterConfig_init_zero, false, meshtastic_ModuleConfig_StatusMessageConfig_init_zero, false, meshtastic_ModuleConfig_TrafficManagementConfig_init_zero, false, meshtastic_ModuleConfig_TAKConfig_init_zero} /* Field tags (for use in manual encoding/decoding) */ #define meshtastic_LocalConfig_device_tag 1 @@ -128,6 +134,8 @@ extern "C" { #define meshtastic_LocalModuleConfig_detection_sensor_tag 13 #define meshtastic_LocalModuleConfig_paxcounter_tag 14 #define meshtastic_LocalModuleConfig_statusmessage_tag 15 +#define meshtastic_LocalModuleConfig_traffic_management_tag 16 +#define meshtastic_LocalModuleConfig_tak_tag 17 /* Struct field encoding specification for nanopb */ #define meshtastic_LocalConfig_FIELDLIST(X, a) \ @@ -166,7 +174,9 @@ X(a, STATIC, OPTIONAL, MESSAGE, neighbor_info, 11) \ X(a, STATIC, OPTIONAL, MESSAGE, ambient_lighting, 12) \ X(a, STATIC, OPTIONAL, MESSAGE, detection_sensor, 13) \ X(a, STATIC, OPTIONAL, MESSAGE, paxcounter, 14) \ -X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) +X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) \ +X(a, STATIC, OPTIONAL, MESSAGE, traffic_management, 16) \ +X(a, STATIC, OPTIONAL, MESSAGE, tak, 17) #define meshtastic_LocalModuleConfig_CALLBACK NULL #define meshtastic_LocalModuleConfig_DEFAULT NULL #define meshtastic_LocalModuleConfig_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -183,6 +193,8 @@ X(a, STATIC, OPTIONAL, MESSAGE, statusmessage, 15) #define meshtastic_LocalModuleConfig_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_LocalModuleConfig_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig #define meshtastic_LocalModuleConfig_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig +#define meshtastic_LocalModuleConfig_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig +#define meshtastic_LocalModuleConfig_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig extern const pb_msgdesc_t meshtastic_LocalConfig_msg; extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; @@ -193,8 +205,8 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size -#define meshtastic_LocalConfig_size 749 -#define meshtastic_LocalModuleConfig_size 758 +#define meshtastic_LocalConfig_size 754 +#define meshtastic_LocalModuleConfig_size 820 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index aeae4bd84..773cc8ed6 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -300,6 +300,10 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_TBEAM_1_WATT = 122, /* LilyGo T5 S3 ePaper Pro (V1 and V2) */ meshtastic_HardwareModel_T5_S3_EPAPER_PRO = 123, + /* LilyGo T-Beam BPF (144-148Mhz) */ + meshtastic_HardwareModel_TBEAM_BPF = 124, + /* LilyGo T-Mini E-paper S3 Kit */ + meshtastic_HardwareModel_MINI_EPAPER_S3 = 125, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ diff --git a/src/mesh/generated/meshtastic/module_config.pb.cpp b/src/mesh/generated/meshtastic/module_config.pb.cpp index bb57c3f2d..f2fe5d967 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.cpp +++ b/src/mesh/generated/meshtastic/module_config.pb.cpp @@ -30,6 +30,9 @@ PB_BIND(meshtastic_ModuleConfig_AudioConfig, meshtastic_ModuleConfig_AudioConfig PB_BIND(meshtastic_ModuleConfig_PaxcounterConfig, meshtastic_ModuleConfig_PaxcounterConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_TrafficManagementConfig, meshtastic_ModuleConfig_TrafficManagementConfig, AUTO) + + PB_BIND(meshtastic_ModuleConfig_SerialConfig, meshtastic_ModuleConfig_SerialConfig, AUTO) @@ -54,6 +57,9 @@ PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_A PB_BIND(meshtastic_ModuleConfig_StatusMessageConfig, meshtastic_ModuleConfig_StatusMessageConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_TAKConfig, meshtastic_ModuleConfig_TAKConfig, AUTO) + + PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 46a7164d2..b8cf60bf0 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -4,6 +4,7 @@ #ifndef PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #define PB_MESHTASTIC_MESHTASTIC_MODULE_CONFIG_PB_H_INCLUDED #include +#include "meshtastic/atak.pb.h" #if PB_PROTO_HEADER_VERSION != 40 #error Regenerate this file with the current version of nanopb generator. @@ -228,6 +229,39 @@ typedef struct _meshtastic_ModuleConfig_PaxcounterConfig { int32_t ble_threshold; } meshtastic_ModuleConfig_PaxcounterConfig; +/* Config for the Traffic Management module. + Provides packet inspection and traffic shaping to help reduce channel utilization */ +typedef struct _meshtastic_ModuleConfig_TrafficManagementConfig { + /* Master enable for traffic management module */ + bool enabled; + /* Enable position deduplication to drop redundant position broadcasts */ + bool position_dedup_enabled; + /* Number of bits of precision for position deduplication (0-32) */ + uint32_t position_precision_bits; + /* Minimum interval in seconds between position updates from the same node */ + uint32_t position_min_interval_secs; + /* Enable direct response to NodeInfo requests from local cache */ + bool nodeinfo_direct_response; + /* Minimum hop distance from requestor before responding to NodeInfo requests */ + uint32_t nodeinfo_direct_response_max_hops; + /* Enable per-node rate limiting to throttle chatty nodes */ + bool rate_limit_enabled; + /* Time window in seconds for rate limiting calculations */ + uint32_t rate_limit_window_secs; + /* Maximum packets allowed per node within the rate limit window */ + uint32_t rate_limit_max_packets; + /* Enable dropping of unknown/undecryptable packets per rate_limit_window_secs */ + bool drop_unknown_enabled; + /* Number of unknown packets before dropping from a node */ + uint32_t unknown_packet_threshold; + /* Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected) */ + bool exhaust_hop_telemetry; + /* Set hop_limit to 0 for relayed position broadcasts (own packets unaffected) */ + bool exhaust_hop_position; + /* Preserve hop_limit for router-to-router traffic */ + bool router_preserve_hops; +} meshtastic_ModuleConfig_TrafficManagementConfig; + /* Serial Config */ typedef struct _meshtastic_ModuleConfig_SerialConfig { /* Preferences for the SerialModule */ @@ -415,6 +449,16 @@ typedef struct _meshtastic_ModuleConfig_StatusMessageConfig { char node_status[80]; } meshtastic_ModuleConfig_StatusMessageConfig; +/* TAK team/role configuration */ +typedef struct _meshtastic_ModuleConfig_TAKConfig { + /* Team color. + Default Unspecifed_Color -> firmware uses Cyan */ + meshtastic_Team team; + /* Member role. + Default Unspecifed -> firmware uses TeamMember */ + meshtastic_MemberRole role; +} meshtastic_ModuleConfig_TAKConfig; + /* A GPIO pin definition for remote hardware module */ typedef struct _meshtastic_RemoteHardwarePin { /* GPIO Pin number (must match Arduino) */ @@ -468,6 +512,10 @@ typedef struct _meshtastic_ModuleConfig { meshtastic_ModuleConfig_PaxcounterConfig paxcounter; /* TODO: REPLACE */ meshtastic_ModuleConfig_StatusMessageConfig statusmessage; + /* Traffic management module config for mesh network optimization */ + meshtastic_ModuleConfig_TrafficManagementConfig traffic_management; + /* TAK team/role configuration for TAK_TRACKER */ + meshtastic_ModuleConfig_TAKConfig tak; } payload_variant; } meshtastic_ModuleConfig; @@ -511,6 +559,7 @@ extern "C" { #define meshtastic_ModuleConfig_AudioConfig_bitrate_ENUMTYPE meshtastic_ModuleConfig_AudioConfig_Audio_Baud + #define meshtastic_ModuleConfig_SerialConfig_baud_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Baud #define meshtastic_ModuleConfig_SerialConfig_mode_ENUMTYPE meshtastic_ModuleConfig_SerialConfig_Serial_Mode @@ -524,6 +573,9 @@ extern "C" { +#define meshtastic_ModuleConfig_TAKConfig_team_ENUMTYPE meshtastic_Team +#define meshtastic_ModuleConfig_TAKConfig_role_ENUMTYPE meshtastic_MemberRole + #define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType @@ -536,6 +588,7 @@ extern "C" { #define meshtastic_ModuleConfig_DetectionSensorConfig_init_default {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} #define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} #define meshtastic_ModuleConfig_PaxcounterConfig_init_default {0, 0, 0, 0} +#define meshtastic_ModuleConfig_TrafficManagementConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_SerialConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0} @@ -544,6 +597,7 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""} +#define meshtastic_ModuleConfig_TAKConfig_init_default {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} #define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}} #define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0, 0, false, meshtastic_ModuleConfig_MapReportSettings_init_zero} @@ -553,6 +607,7 @@ extern "C" { #define meshtastic_ModuleConfig_DetectionSensorConfig_init_zero {0, 0, 0, 0, "", 0, _meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_MIN, 0} #define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} #define meshtastic_ModuleConfig_PaxcounterConfig_init_zero {0, 0, 0, 0} +#define meshtastic_ModuleConfig_TrafficManagementConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_SerialConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Baud_MIN, 0, _meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MIN, 0} #define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0} @@ -561,6 +616,7 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} #define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""} +#define meshtastic_ModuleConfig_TAKConfig_init_zero {_meshtastic_Team_MIN, _meshtastic_MemberRole_MIN} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} /* Field tags (for use in manual encoding/decoding) */ @@ -600,6 +656,20 @@ extern "C" { #define meshtastic_ModuleConfig_PaxcounterConfig_paxcounter_update_interval_tag 2 #define meshtastic_ModuleConfig_PaxcounterConfig_wifi_threshold_tag 3 #define meshtastic_ModuleConfig_PaxcounterConfig_ble_threshold_tag 4 +#define meshtastic_ModuleConfig_TrafficManagementConfig_enabled_tag 1 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_dedup_enabled_tag 2 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_precision_bits_tag 3 +#define meshtastic_ModuleConfig_TrafficManagementConfig_position_min_interval_secs_tag 4 +#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_tag 5 +#define meshtastic_ModuleConfig_TrafficManagementConfig_nodeinfo_direct_response_max_hops_tag 6 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_enabled_tag 7 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_window_secs_tag 8 +#define meshtastic_ModuleConfig_TrafficManagementConfig_rate_limit_max_packets_tag 9 +#define meshtastic_ModuleConfig_TrafficManagementConfig_drop_unknown_enabled_tag 10 +#define meshtastic_ModuleConfig_TrafficManagementConfig_unknown_packet_threshold_tag 11 +#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_telemetry_tag 12 +#define meshtastic_ModuleConfig_TrafficManagementConfig_exhaust_hop_position_tag 13 +#define meshtastic_ModuleConfig_TrafficManagementConfig_router_preserve_hops_tag 14 #define meshtastic_ModuleConfig_SerialConfig_enabled_tag 1 #define meshtastic_ModuleConfig_SerialConfig_echo_tag 2 #define meshtastic_ModuleConfig_SerialConfig_rxd_tag 3 @@ -665,6 +735,8 @@ extern "C" { #define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4 #define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5 #define meshtastic_ModuleConfig_StatusMessageConfig_node_status_tag 1 +#define meshtastic_ModuleConfig_TAKConfig_team_tag 1 +#define meshtastic_ModuleConfig_TAKConfig_role_tag 2 #define meshtastic_RemoteHardwarePin_gpio_pin_tag 1 #define meshtastic_RemoteHardwarePin_name_tag 2 #define meshtastic_RemoteHardwarePin_type_tag 3 @@ -685,6 +757,8 @@ extern "C" { #define meshtastic_ModuleConfig_detection_sensor_tag 12 #define meshtastic_ModuleConfig_paxcounter_tag 13 #define meshtastic_ModuleConfig_statusmessage_tag 14 +#define meshtastic_ModuleConfig_traffic_management_tag 15 +#define meshtastic_ModuleConfig_tak_tag 16 /* Struct field encoding specification for nanopb */ #define meshtastic_ModuleConfig_FIELDLIST(X, a) \ @@ -701,7 +775,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_varian X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,detection_sensor,payload_variant.detection_sensor), 12) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,paxcounter,payload_variant.paxcounter), 13) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_variant.statusmessage), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,traffic_management,payload_variant.traffic_management), 15) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,tak,payload_variant.tak), 16) #define meshtastic_ModuleConfig_CALLBACK NULL #define meshtastic_ModuleConfig_DEFAULT NULL #define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -718,6 +794,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,statusmessage,payload_varian #define meshtastic_ModuleConfig_payload_variant_detection_sensor_MSGTYPE meshtastic_ModuleConfig_DetectionSensorConfig #define meshtastic_ModuleConfig_payload_variant_paxcounter_MSGTYPE meshtastic_ModuleConfig_PaxcounterConfig #define meshtastic_ModuleConfig_payload_variant_statusmessage_MSGTYPE meshtastic_ModuleConfig_StatusMessageConfig +#define meshtastic_ModuleConfig_payload_variant_traffic_management_MSGTYPE meshtastic_ModuleConfig_TrafficManagementConfig +#define meshtastic_ModuleConfig_payload_variant_tak_MSGTYPE meshtastic_ModuleConfig_TAKConfig #define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ @@ -788,6 +866,24 @@ X(a, STATIC, SINGULAR, INT32, ble_threshold, 4) #define meshtastic_ModuleConfig_PaxcounterConfig_CALLBACK NULL #define meshtastic_ModuleConfig_PaxcounterConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_TrafficManagementConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ +X(a, STATIC, SINGULAR, BOOL, position_dedup_enabled, 2) \ +X(a, STATIC, SINGULAR, UINT32, position_precision_bits, 3) \ +X(a, STATIC, SINGULAR, UINT32, position_min_interval_secs, 4) \ +X(a, STATIC, SINGULAR, BOOL, nodeinfo_direct_response, 5) \ +X(a, STATIC, SINGULAR, UINT32, nodeinfo_direct_response_max_hops, 6) \ +X(a, STATIC, SINGULAR, BOOL, rate_limit_enabled, 7) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_window_secs, 8) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_max_packets, 9) \ +X(a, STATIC, SINGULAR, BOOL, drop_unknown_enabled, 10) \ +X(a, STATIC, SINGULAR, UINT32, unknown_packet_threshold, 11) \ +X(a, STATIC, SINGULAR, BOOL, exhaust_hop_telemetry, 12) \ +X(a, STATIC, SINGULAR, BOOL, exhaust_hop_position, 13) \ +X(a, STATIC, SINGULAR, BOOL, router_preserve_hops, 14) +#define meshtastic_ModuleConfig_TrafficManagementConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_TrafficManagementConfig_DEFAULT NULL + #define meshtastic_ModuleConfig_SerialConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ X(a, STATIC, SINGULAR, BOOL, echo, 2) \ @@ -885,6 +981,12 @@ X(a, STATIC, SINGULAR, STRING, node_status, 1) #define meshtastic_ModuleConfig_StatusMessageConfig_CALLBACK NULL #define meshtastic_ModuleConfig_StatusMessageConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_TAKConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, team, 1) \ +X(a, STATIC, SINGULAR, UENUM, role, 2) +#define meshtastic_ModuleConfig_TAKConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_TAKConfig_DEFAULT NULL + #define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \ X(a, STATIC, SINGULAR, STRING, name, 2) \ @@ -900,6 +1002,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_NeighborInfoConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_DetectionSensorConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_AudioConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_PaxcounterConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_TrafficManagementConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_SerialConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_ExternalNotificationConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_StoreForwardConfig_msg; @@ -908,6 +1011,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_StatusMessageConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_TAKConfig_msg; extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -919,6 +1023,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_DetectionSensorConfig_fields &meshtastic_ModuleConfig_DetectionSensorConfig_msg #define meshtastic_ModuleConfig_AudioConfig_fields &meshtastic_ModuleConfig_AudioConfig_msg #define meshtastic_ModuleConfig_PaxcounterConfig_fields &meshtastic_ModuleConfig_PaxcounterConfig_msg +#define meshtastic_ModuleConfig_TrafficManagementConfig_fields &meshtastic_ModuleConfig_TrafficManagementConfig_msg #define meshtastic_ModuleConfig_SerialConfig_fields &meshtastic_ModuleConfig_SerialConfig_msg #define meshtastic_ModuleConfig_ExternalNotificationConfig_fields &meshtastic_ModuleConfig_ExternalNotificationConfig_msg #define meshtastic_ModuleConfig_StoreForwardConfig_fields &meshtastic_ModuleConfig_StoreForwardConfig_msg @@ -927,6 +1032,7 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg #define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg #define meshtastic_ModuleConfig_StatusMessageConfig_fields &meshtastic_ModuleConfig_StatusMessageConfig_msg +#define meshtastic_ModuleConfig_TAKConfig_fields &meshtastic_ModuleConfig_TAKConfig_msg #define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg /* Maximum encoded size of messages (where known) */ @@ -945,7 +1051,9 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StatusMessageConfig_size 81 #define meshtastic_ModuleConfig_StoreForwardConfig_size 24 +#define meshtastic_ModuleConfig_TAKConfig_size 4 #define meshtastic_ModuleConfig_TelemetryConfig_size 50 +#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52 #define meshtastic_ModuleConfig_size 227 #define meshtastic_RemoteHardwarePin_size 21 diff --git a/src/mesh/generated/meshtastic/telemetry.pb.cpp b/src/mesh/generated/meshtastic/telemetry.pb.cpp index fff75ebc1..bc21b9dcb 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.cpp +++ b/src/mesh/generated/meshtastic/telemetry.pb.cpp @@ -21,6 +21,9 @@ PB_BIND(meshtastic_AirQualityMetrics, meshtastic_AirQualityMetrics, AUTO) PB_BIND(meshtastic_LocalStats, meshtastic_LocalStats, AUTO) +PB_BIND(meshtastic_TrafficManagementStats, meshtastic_TrafficManagementStats, AUTO) + + PB_BIND(meshtastic_HealthMetrics, meshtastic_HealthMetrics, AUTO) diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index dc9d876dc..8c34cd84c 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -103,7 +103,15 @@ typedef enum _meshtastic_TelemetrySensorType { /* TSL2561 light sensor */ meshtastic_TelemetrySensorType_TSL2561 = 44, /* BH1750 light sensor */ - meshtastic_TelemetrySensorType_BH1750 = 45 + meshtastic_TelemetrySensorType_BH1750 = 45, + /* HDC1080 Temperature and Humidity Sensor */ + meshtastic_TelemetrySensorType_HDC1080 = 46, + /* STH21 Temperature and R. Humidity sensor */ + meshtastic_TelemetrySensorType_SHT21 = 47, + /* Sensirion STC31 CO2 sensor */ + meshtastic_TelemetrySensorType_STC31 = 48, + /* SCD30 CO2, humidity, temperature sensor */ + meshtastic_TelemetrySensorType_SCD30 = 49 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -365,6 +373,24 @@ typedef struct _meshtastic_LocalStats { int32_t noise_floor; } meshtastic_LocalStats; +/* Traffic management statistics for mesh network optimization */ +typedef struct _meshtastic_TrafficManagementStats { + /* Total number of packets inspected by traffic management */ + uint32_t packets_inspected; + /* Number of position packets dropped due to deduplication */ + uint32_t position_dedup_drops; + /* Number of NodeInfo requests answered from cache */ + uint32_t nodeinfo_cache_hits; + /* Number of packets dropped due to rate limiting */ + uint32_t rate_limit_drops; + /* Number of unknown/undecryptable packets dropped */ + uint32_t unknown_packet_drops; + /* Number of packets with hop_limit exhausted for local-only broadcast */ + uint32_t hop_exhausted_packets; + /* Number of times router hop preservation was applied */ + uint32_t router_hops_preserved; +} meshtastic_TrafficManagementStats; + /* Health telemetry metrics */ typedef struct _meshtastic_HealthMetrics { /* Heart rate (beats per minute) */ @@ -424,6 +450,8 @@ typedef struct _meshtastic_Telemetry { meshtastic_HealthMetrics health_metrics; /* Linux host metrics */ meshtastic_HostMetrics host_metrics; + /* Traffic management statistics */ + meshtastic_TrafficManagementStats traffic_management_stats; } variant; } meshtastic_Telemetry; @@ -461,8 +489,9 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SCD30 +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SCD30+1)) + @@ -481,6 +510,7 @@ extern "C" { #define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_TrafficManagementStats_init_default {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} @@ -491,6 +521,7 @@ extern "C" { #define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_TrafficManagementStats_init_zero {0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} @@ -581,6 +612,13 @@ extern "C" { #define meshtastic_LocalStats_heap_free_bytes_tag 13 #define meshtastic_LocalStats_num_tx_dropped_tag 14 #define meshtastic_LocalStats_noise_floor_tag 15 +#define meshtastic_TrafficManagementStats_packets_inspected_tag 1 +#define meshtastic_TrafficManagementStats_position_dedup_drops_tag 2 +#define meshtastic_TrafficManagementStats_nodeinfo_cache_hits_tag 3 +#define meshtastic_TrafficManagementStats_rate_limit_drops_tag 4 +#define meshtastic_TrafficManagementStats_unknown_packet_drops_tag 5 +#define meshtastic_TrafficManagementStats_hop_exhausted_packets_tag 6 +#define meshtastic_TrafficManagementStats_router_hops_preserved_tag 7 #define meshtastic_HealthMetrics_heart_bpm_tag 1 #define meshtastic_HealthMetrics_spO2_tag 2 #define meshtastic_HealthMetrics_temperature_tag 3 @@ -601,6 +639,7 @@ extern "C" { #define meshtastic_Telemetry_local_stats_tag 6 #define meshtastic_Telemetry_health_metrics_tag 7 #define meshtastic_Telemetry_host_metrics_tag 8 +#define meshtastic_Telemetry_traffic_management_stats_tag 9 #define meshtastic_Nau7802Config_zeroOffset_tag 1 #define meshtastic_Nau7802Config_calibrationFactor_tag 2 #define meshtastic_SEN5XState_last_cleaning_time_tag 1 @@ -714,6 +753,17 @@ X(a, STATIC, SINGULAR, INT32, noise_floor, 15) #define meshtastic_LocalStats_CALLBACK NULL #define meshtastic_LocalStats_DEFAULT NULL +#define meshtastic_TrafficManagementStats_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, packets_inspected, 1) \ +X(a, STATIC, SINGULAR, UINT32, position_dedup_drops, 2) \ +X(a, STATIC, SINGULAR, UINT32, nodeinfo_cache_hits, 3) \ +X(a, STATIC, SINGULAR, UINT32, rate_limit_drops, 4) \ +X(a, STATIC, SINGULAR, UINT32, unknown_packet_drops, 5) \ +X(a, STATIC, SINGULAR, UINT32, hop_exhausted_packets, 6) \ +X(a, STATIC, SINGULAR, UINT32, router_hops_preserved, 7) +#define meshtastic_TrafficManagementStats_CALLBACK NULL +#define meshtastic_TrafficManagementStats_DEFAULT NULL + #define meshtastic_HealthMetrics_FIELDLIST(X, a) \ X(a, STATIC, OPTIONAL, UINT32, heart_bpm, 1) \ X(a, STATIC, OPTIONAL, UINT32, spO2, 2) \ @@ -742,7 +792,8 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,air_quality_metrics,variant.air_qual X(a, STATIC, ONEOF, MESSAGE, (variant,power_metrics,variant.power_metrics), 5) \ X(a, STATIC, ONEOF, MESSAGE, (variant,local_stats,variant.local_stats), 6) \ X(a, STATIC, ONEOF, MESSAGE, (variant,health_metrics,variant.health_metrics), 7) \ -X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) +X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), 8) \ +X(a, STATIC, ONEOF, MESSAGE, (variant,traffic_management_stats,variant.traffic_management_stats), 9) #define meshtastic_Telemetry_CALLBACK NULL #define meshtastic_Telemetry_DEFAULT NULL #define meshtastic_Telemetry_variant_device_metrics_MSGTYPE meshtastic_DeviceMetrics @@ -752,6 +803,7 @@ X(a, STATIC, ONEOF, MESSAGE, (variant,host_metrics,variant.host_metrics), #define meshtastic_Telemetry_variant_local_stats_MSGTYPE meshtastic_LocalStats #define meshtastic_Telemetry_variant_health_metrics_MSGTYPE meshtastic_HealthMetrics #define meshtastic_Telemetry_variant_host_metrics_MSGTYPE meshtastic_HostMetrics +#define meshtastic_Telemetry_variant_traffic_management_stats_MSGTYPE meshtastic_TrafficManagementStats #define meshtastic_Nau7802Config_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, INT32, zeroOffset, 1) \ @@ -774,6 +826,7 @@ extern const pb_msgdesc_t meshtastic_EnvironmentMetrics_msg; extern const pb_msgdesc_t meshtastic_PowerMetrics_msg; extern const pb_msgdesc_t meshtastic_AirQualityMetrics_msg; extern const pb_msgdesc_t meshtastic_LocalStats_msg; +extern const pb_msgdesc_t meshtastic_TrafficManagementStats_msg; extern const pb_msgdesc_t meshtastic_HealthMetrics_msg; extern const pb_msgdesc_t meshtastic_HostMetrics_msg; extern const pb_msgdesc_t meshtastic_Telemetry_msg; @@ -786,6 +839,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg; #define meshtastic_PowerMetrics_fields &meshtastic_PowerMetrics_msg #define meshtastic_AirQualityMetrics_fields &meshtastic_AirQualityMetrics_msg #define meshtastic_LocalStats_fields &meshtastic_LocalStats_msg +#define meshtastic_TrafficManagementStats_fields &meshtastic_TrafficManagementStats_msg #define meshtastic_HealthMetrics_fields &meshtastic_HealthMetrics_msg #define meshtastic_HostMetrics_fields &meshtastic_HostMetrics_msg #define meshtastic_Telemetry_fields &meshtastic_Telemetry_msg @@ -804,6 +858,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg; #define meshtastic_PowerMetrics_size 81 #define meshtastic_SEN5XState_size 27 #define meshtastic_Telemetry_size 272 +#define meshtastic_TrafficManagementStats_size 42 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index ea8d6af8e..6b3aa4859 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -9,7 +9,6 @@ #if HAS_WIFI #include "mesh/wifi/WiFiAPClient.h" #endif -#include "Led.h" #include "SPILock.h" #include "power.h" #include "serialization/JSON.h" @@ -47,10 +46,6 @@ using namespace httpsserver; #include "mesh/http/ContentHandler.h" -#include -#include -HTTPClient httpClient; - #define DEST_FS_USES_LITTLEFS // We need to specify some content-type mapping, so the resources get delivered with the @@ -92,7 +87,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); - ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); ResourceNode *nodeJsonNodes = new ResourceNode("/json/nodes", "GET", &handleNodes); ResourceNode *nodeJsonFsBrowseStatic = new ResourceNode("/json/fs/browse/static", "GET", &handleFsBrowseStatic); @@ -110,7 +104,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) secureServer->registerNode(nodeRestart); secureServer->registerNode(nodeFormUpload); secureServer->registerNode(nodeJsonScanNetworks); - secureServer->registerNode(nodeJsonBlinkLED); secureServer->registerNode(nodeJsonFsBrowseStatic); secureServer->registerNode(nodeJsonDelete); secureServer->registerNode(nodeJsonReport); @@ -133,7 +126,6 @@ void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) insecureServer->registerNode(nodeRestart); insecureServer->registerNode(nodeFormUpload); insecureServer->registerNode(nodeJsonScanNetworks); - insecureServer->registerNode(nodeJsonBlinkLED); insecureServer->registerNode(nodeJsonFsBrowseStatic); insecureServer->registerNode(nodeJsonDelete); insecureServer->registerNode(nodeJsonReport); @@ -348,11 +340,6 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res) res->print(jsonString.c_str()); delete value; - - // Clean up the fileList to prevent memory leak - for (auto *val : fileList) { - delete val; - } } void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res) @@ -547,6 +534,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res) if (name != "file") { LOG_DEBUG("Skip unexpected field"); res->println("

No file found.

"); + delete parser; return; } @@ -554,6 +542,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res) if (filename == "") { LOG_DEBUG("Skip unexpected field"); res->println("

No file found.

"); + delete parser; return; } @@ -627,7 +616,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res) } // Helper lambda to create JSON array and clean up memory properly - auto createJSONArrayFromLog = [](uint32_t *logArray, int count) -> JSONValue * { + auto createJSONArrayFromLog = [](const uint32_t *logArray, int count) -> JSONValue * { JSONArray tempArray; for (int i = 0; i < count; i++) { tempArray.push_back(new JSONValue((int)logArray[i])); @@ -787,11 +776,6 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res) std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; - - // Clean up the nodesArray to prevent memory leak - for (auto *val : nodesArray) { - delete val; - } } /* @@ -904,45 +888,6 @@ void handleRestart(HTTPRequest *req, HTTPResponse *res) webServerThread->requestRestart = (millis() / 1000) + 5; } -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "application/json"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "POST"); - - ResourceParameters *params = req->getParams(); - std::string blink_target; - - if (!params->getQueryParameter("blink_target", blink_target)) { - // if no blink_target was supplied in the URL parameters of the - // POST request, then assume we should blink the LED - blink_target = "LED"; - } - - if (blink_target == "LED") { - uint8_t count = 10; - while (count > 0) { - ledBlink.set(true); - delay(50); - ledBlink.set(false); - delay(50); - count = count - 1; - } - } else { -#if HAS_SCREEN - if (screen) - screen->blink(); -#endif - } - - JSONObject jsonObjOuter; - jsonObjOuter["status"] = new JSONValue("ok"); - JSONValue *value = new JSONValue(jsonObjOuter); - std::string jsonString = value->Stringify(); - res->print(jsonString.c_str()); - delete value; -} - void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) { res->setHeader("Content-Type", "application/json"); @@ -984,10 +929,5 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) std::string jsonString = value->Stringify(); res->print(jsonString.c_str()); delete value; - - // Clean up the networkObjs to prevent memory leak - for (auto *val : networkObjs) { - delete val; - } } #endif \ No newline at end of file diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h index 91cad3359..6efdb59b7 100644 --- a/src/mesh/http/ContentHandler.h +++ b/src/mesh/http/ContentHandler.h @@ -11,7 +11,6 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res); void handleScanNetworks(HTTPRequest *req, HTTPResponse *res); void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res); void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res); -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res); void handleReport(HTTPRequest *req, HTTPResponse *res); void handleNodes(HTTPRequest *req, HTTPResponse *res); void handleUpdateFs(HTTPRequest *req, HTTPResponse *res); diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 3a264fa5a..90fd8b084 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -55,6 +56,12 @@ static const int32_t ACTIVE_INTERVAL_MS = 50; static const int32_t MEDIUM_INTERVAL_MS = 200; static const int32_t IDLE_INTERVAL_MS = 1000; +// Maximum concurrent HTTPS connections (reduced from default 4 to save memory) +static const uint8_t MAX_HTTPS_CONNECTIONS = 2; + +// Minimum free heap required for SSL handshake (~40KB for mbedTLS contexts) +static const uint32_t MIN_HEAP_FOR_SSL = 40000; + static SSLCert *cert; static HTTPSServer *secureServer; static HTTPServer *insecureServer; @@ -67,8 +74,20 @@ static void handleWebResponse() if (isWifiAvailable()) { if (isWebServerReady) { - if (secureServer) - secureServer->loop(); + // Check heap before HTTPS processing - SSL requires significant memory + if (secureServer) { + uint32_t freeHeap = ESP.getFreeHeap(); + if (freeHeap >= MIN_HEAP_FOR_SSL) { + secureServer->loop(); + } else { + // Skip HTTPS when memory is low to prevent SSL setup failures + static uint32_t lastHeapWarning = 0; + if (lastHeapWarning == 0 || !Throttle::isWithinTimespanMs(lastHeapWarning, 30000)) { + LOG_WARN("Low heap (%u bytes), skipping HTTPS processing", freeHeap); + lastHeapWarning = millis(); + } + } + } insecureServer->loop(); } } @@ -229,7 +248,7 @@ void initWebServer() LOG_DEBUG("Init Web Server"); // We can now use the new certificate to setup our server as usual. - secureServer = new HTTPSServer(cert); + secureServer = new HTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS); insecureServer = new HTTPServer(); registerHandlers(insecureServer, secureServer); diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 2df8686a3..71bdf488f 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -22,10 +22,14 @@ class UdpMulticastHandler final { public: - UdpMulticastHandler() { udpIpAddress = IPAddress(224, 0, 0, 69); } + UdpMulticastHandler() : isRunning(false) { udpIpAddress = IPAddress(224, 0, 0, 69); } void start() { + if (isRunning) { + LOG_DEBUG("UDP multicast already running"); + return; + } if (udp.listenMulticast(udpIpAddress, UDP_MULTICAST_DEFAUL_PORT, 64)) { #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) LOG_DEBUG("UDP Listening on IP: %u.%u.%u.%u:%u", udpIpAddress[0], udpIpAddress[1], udpIpAddress[2], udpIpAddress[3], @@ -34,13 +38,29 @@ class UdpMulticastHandler final LOG_DEBUG("UDP Listening on IP: %s", WiFi.localIP().toString().c_str()); #endif udp.onPacket([this](AsyncUDPPacket packet) { onReceive(packet); }); + isRunning = true; } else { LOG_DEBUG("Failed to listen on UDP"); } } - void onReceive(AsyncUDPPacket packet) + void stop() { + if (!isRunning) { + return; + } + LOG_DEBUG("Stopping UDP multicast"); +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) + udp.close(); +#endif + isRunning = false; + } + + void onReceive(AsyncUDPPacket &packet) + { + if (!isRunning) { + return; + } size_t packetLength = packet.length(); #if defined(ARCH_NRF52) IPAddress ip = packet.remoteIP(); @@ -67,7 +87,7 @@ class UdpMulticastHandler final bool onSend(const meshtastic_MeshPacket *mp) { - if (!mp || !udp) { + if (!isRunning || !mp || !udp) { return false; } #if defined(ARCH_NRF52) @@ -92,5 +112,6 @@ class UdpMulticastHandler final private: IPAddress udpIpAddress; AsyncUDP udp; + bool isRunning; }; #endif // HAS_UDP_MULTICAST \ No newline at end of file diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index a95dfa58f..5d05c7fc6 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -391,6 +391,11 @@ static void WiFiEvent(WiFiEvent_t event) LOG_INFO("Disconnected from WiFi access point"); #ifdef WIFI_LED digitalWrite(WIFI_LED, LOW); +#endif +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } #endif if (!isReconnecting) { WiFi.disconnect(false, true); @@ -417,6 +422,11 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: LOG_INFO("Lost IP address and IP address is reset to 0"); +#if HAS_UDP_MULTICAST + if (udpHandler) { + udpHandler->stop(); + } +#endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); diff --git a/src/meshUtils.cpp b/src/meshUtils.cpp index ea2ba641b..1a4497101 100644 --- a/src/meshUtils.cpp +++ b/src/meshUtils.cpp @@ -106,4 +106,15 @@ const std::string vformat(const char *const zcFormat, ...) std::vsnprintf(zc.data(), zc.size(), zcFormat, vaArgs); va_end(vaArgs); return std::string(zc.data(), iLen); +} + +size_t pb_string_length(const char *str, size_t max_len) +{ + size_t len = 0; + for (size_t i = 0; i < max_len; i++) { + if (str[i] != '\0') { + len = i + 1; + } + } + return len; } \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 9fcf6f8a8..67446f91f 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -35,4 +35,7 @@ bool isOneOf(int item, int count, ...); const std::string vformat(const char *const zcFormat, ...); +// Get actual string length for nanopb char array fields. +size_t pb_string_length(const char *str, size_t max_len); + #define IS_ONE_OF(item, ...) isOneOf(item, sizeof((int[]){__VA_ARGS__}) / sizeof(int), __VA_ARGS__) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 1fda9bf13..419d2b773 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -391,7 +391,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta node->has_device_metrics = false; node->has_position = false; node->user.public_key.size = 0; - node->user.public_key.bytes[0] = 0; + memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes)); saveChanges(SEGMENT_NODEDATABASE, false); } break; @@ -643,12 +643,6 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) accelerometerThread->enabled = true; accelerometerThread->start(); } -#endif -#ifdef LED_PIN - // Turn LED off if heartbeat by config - if (c.payload_variant.device.led_heartbeat_disabled) { - digitalWrite(LED_PIN, HIGH ^ LED_STATE_ON); - } #endif if (config.device.button_gpio == c.payload_variant.device.button_gpio && config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio && @@ -905,10 +899,11 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) { + bool shouldReboot = true; // If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth // Otherwise, disable Bluetooth to prevent the phone from interfering with the config - if (!hasOpenEditTransaction && - !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, meshtastic_ModuleConfig_serial_tag)) { + if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag, + meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) { disableBluetooth(); } @@ -1000,8 +995,14 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c) moduleConfig.has_paxcounter = true; moduleConfig.paxcounter = c.payload_variant.paxcounter; break; + case meshtastic_ModuleConfig_statusmessage_tag: + LOG_INFO("Set module config: StatusMessage"); + moduleConfig.has_statusmessage = true; + moduleConfig.statusmessage = c.payload_variant.statusmessage; + shouldReboot = false; + break; } - saveChanges(SEGMENT_MODULECONFIG); + saveChanges(SEGMENT_MODULECONFIG, shouldReboot); return true; } @@ -1180,6 +1181,11 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter; break; + case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG: + LOG_INFO("Get module config: StatusMessage"); + res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag; + res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage; + break; } // NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior. diff --git a/src/modules/AtakPluginModule.cpp b/src/modules/AtakPluginModule.cpp index a51ef54c3..bddb6276b 100644 --- a/src/modules/AtakPluginModule.cpp +++ b/src/modules/AtakPluginModule.cpp @@ -6,6 +6,7 @@ #include "configuration.h" #include "main.h" #include "mesh/compression/unishox2.h" +#include "meshUtils.h" #include "meshtastic/atak.pb.h" AtakPluginModule *atakPluginModule; @@ -70,16 +71,17 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast auto compressed = cloneTAKPacketData(t); compressed.is_compressed = true; if (t->has_contact) { - auto length = unishox2_compress_lines(t->contact.callsign, strlen(t->contact.callsign), compressed.contact.callsign, - sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_compress_lines( + t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), + compressed.contact.callsign, sizeof(compressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.callsign. Revert to uncompressed packet"); return; } LOG_DEBUG("Compressed callsign: %d bytes", length); - length = unishox2_compress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, - USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), + compressed.contact.device_callsign, sizeof(compressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow contact.device_callsign. Revert to uncompressed packet"); return; @@ -87,9 +89,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast LOG_DEBUG("Compressed device_callsign: %d bytes", length); } if (t->which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_compress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - compressed.payload_variant.chat.message, - sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_compress_lines( + t->payload_variant.chat.message, + pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), + compressed.payload_variant.chat.message, sizeof(compressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, + NULL); if (length < 0) { LOG_WARN("Compress overflow chat.message. Revert to uncompressed packet"); return; @@ -98,9 +102,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to) { compressed.payload_variant.chat.has_to = true; - length = unishox2_compress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - compressed.payload_variant.chat.to, - sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), + compressed.payload_variant.chat.to, sizeof(compressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to. Revert to uncompressed packet"); return; @@ -110,9 +114,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to_callsign) { compressed.payload_variant.chat.has_to_callsign = true; - length = unishox2_compress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - compressed.payload_variant.chat.to_callsign, - sizeof(compressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_compress_lines( + t->payload_variant.chat.to_callsign, + pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), + compressed.payload_variant.chat.to_callsign, sizeof(compressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Compress overflow chat.to_callsign. Revert to uncompressed packet"); return; @@ -134,18 +140,18 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast auto uncompressed = cloneTAKPacketData(t); uncompressed.is_compressed = false; if (t->has_contact) { - auto length = - unishox2_decompress_lines(t->contact.callsign, strlen(t->contact.callsign), uncompressed.contact.callsign, - sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_decompress_lines( + t->contact.callsign, pb_string_length(t->contact.callsign, sizeof(t->contact.callsign)), + uncompressed.contact.callsign, sizeof(uncompressed.contact.callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.callsign. Bailing out"); return; } LOG_DEBUG("Decompressed callsign: %d bytes", length); - length = unishox2_decompress_lines(t->contact.device_callsign, strlen(t->contact.device_callsign), - uncompressed.contact.device_callsign, - sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->contact.device_callsign, pb_string_length(t->contact.device_callsign, sizeof(t->contact.device_callsign)), + uncompressed.contact.device_callsign, sizeof(uncompressed.contact.device_callsign) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow contact.device_callsign. Bailing out"); return; @@ -153,9 +159,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast LOG_DEBUG("Decompressed device_callsign: %d bytes", length); } if (uncompressed.which_payload_variant == meshtastic_TAKPacket_chat_tag) { - auto length = unishox2_decompress_lines(t->payload_variant.chat.message, strlen(t->payload_variant.chat.message), - uncompressed.payload_variant.chat.message, - sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, NULL); + auto length = unishox2_decompress_lines( + t->payload_variant.chat.message, + pb_string_length(t->payload_variant.chat.message, sizeof(t->payload_variant.chat.message)), + uncompressed.payload_variant.chat.message, sizeof(uncompressed.payload_variant.chat.message) - 1, USX_PSET_DFLT, + NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.message. Bailing out"); return; @@ -164,9 +172,9 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to) { uncompressed.payload_variant.chat.has_to = true; - length = unishox2_decompress_lines(t->payload_variant.chat.to, strlen(t->payload_variant.chat.to), - uncompressed.payload_variant.chat.to, - sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->payload_variant.chat.to, pb_string_length(t->payload_variant.chat.to, sizeof(t->payload_variant.chat.to)), + uncompressed.payload_variant.chat.to, sizeof(uncompressed.payload_variant.chat.to) - 1, USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to. Bailing out"); return; @@ -176,10 +184,11 @@ void AtakPluginModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtast if (t->payload_variant.chat.has_to_callsign) { uncompressed.payload_variant.chat.has_to_callsign = true; - length = - unishox2_decompress_lines(t->payload_variant.chat.to_callsign, strlen(t->payload_variant.chat.to_callsign), - uncompressed.payload_variant.chat.to_callsign, - sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, USX_PSET_DFLT, NULL); + length = unishox2_decompress_lines( + t->payload_variant.chat.to_callsign, + pb_string_length(t->payload_variant.chat.to_callsign, sizeof(t->payload_variant.chat.to_callsign)), + uncompressed.payload_variant.chat.to_callsign, sizeof(uncompressed.payload_variant.chat.to_callsign) - 1, + USX_PSET_DFLT, NULL); if (length < 0) { LOG_WARN("Decompress overflow chat.to_callsign. Bailing out"); return; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 7d7b3cdb1..c7eb1b15b 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1209,14 +1209,13 @@ int32_t CannedMessageModule::runOnce() 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 + return INT32_MAX; // don't fall back into canned list } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } @@ -1237,14 +1236,13 @@ int32_t CannedMessageModule::runOnce() 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 + return INT32_MAX; // don't fall back into canned list } } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -1255,11 +1253,10 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; - UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); - // Immediately stop, don’t linger on canned screen + // Immediately stop, don't linger on canned screen return INT32_MAX; } // Highlight [Select Destination] initially when entering the message list @@ -2070,7 +2067,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Draw lines with emotes int rowHeight = FONT_HEIGHT_SMALL; int yLine = inputY; - for (auto &line : lines) { + for (const auto &line : lines) { int nextX = x; for (const auto &token : line) { if (token.first) { diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 5e6985bdf..cc7124f0e 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -24,24 +24,8 @@ #include "mesh/generated/meshtastic/rtttl.pb.h" #include -#ifdef HAS_NCP5623 -#include -#endif - -#ifdef HAS_LP5562 -#include -#endif - -#ifdef HAS_NEOPIXEL -#include -#endif - -#ifdef UNPHONE -#include "unPhone.h" -extern unPhone unphone; -#endif - #if defined(HAS_RGB_LED) +#include "AmbientLightingThread.h" uint8_t red = 0; uint8_t green = 0; uint8_t blue = 0; @@ -123,32 +107,6 @@ int32_t ExternalNotificationModule::runOnce() green = (colorState & 2) ? brightnessValues[brightnessIndex] : 0; // Green enabled on colorState = 2,3,6,7 blue = (colorState & 1) ? (brightnessValues[brightnessIndex] * 1.5) : 0; // Blue enabled on colorState = 1,3,5,7 white = (colorState & 12) ? brightnessValues[brightnessIndex] : 0; -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); -#elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); -#endif -#ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); -#endif -#ifdef UNPHONE - unphone.rgb(red, green, blue); -#endif if (ascending) { // fade in brightnessIndex++; if (brightnessIndex == (sizeof(brightnessValues) - 1)) { @@ -255,34 +213,9 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) blue = 0; white = 0; } + ambientLightingThread->setLighting(moduleConfig.ambient_lighting.current, red, green, blue); #endif -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.setColor(red, green, blue); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.setColor(red, green, blue, white); - } -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - red); // CA type needs reverse logic - analogWrite(RGBLED_GREEN, 255 - green); - analogWrite(RGBLED_BLUE, 255 - blue); -#elif defined(RGBLED_RED) - analogWrite(RGBLED_RED, red); - analogWrite(RGBLED_GREEN, green); - analogWrite(RGBLED_BLUE, blue); -#endif -#ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(red, green, blue), 0, NEOPIXEL_COUNT); - pixels.show(); -#endif -#ifdef UNPHONE - unphone.rgb(red, green, blue); -#endif #ifdef HAS_DRV2605 if (on) { drv.go(); @@ -407,33 +340,6 @@ ExternalNotificationModule::ExternalNotificationModule() LOG_INFO("Use Pin %i in PWM mode", config.device.buzzer_gpio); } } -#ifdef HAS_NCP5623 - if (rgb_found.type == ScanI2C::NCP5623) { - rgb.begin(); - rgb.setCurrent(10); - } -#endif -#ifdef HAS_LP5562 - if (rgb_found.type == ScanI2C::LP5562) { - rgbw.begin(); - rgbw.setCurrent(20); - } -#endif -#ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); // set up the RGB led pins - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); -#endif -#ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255); // with a common anode type, logic is reversed - analogWrite(RGBLED_GREEN, 255); // so we want to initialise with lights off - analogWrite(RGBLED_BLUE, 255); -#endif -#ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); -#endif } else { LOG_INFO("External Notification Module Disabled"); disable(); @@ -461,7 +367,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); + const meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(mp.from); meshtastic_Channel ch = channels.getByIndex(mp.channel ? mp.channel : channels.getPrimaryIndex()); // If we receive a broadcast message, apply channel mute setting diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index f667f7be9..94b021360 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -5,6 +5,11 @@ #include "configuration.h" #include "input/InputBroker.h" +#ifdef HAS_RGB_LED +#include "AmbientLightingThread.h" +extern AmbientLightingThread *ambientLightingThread; +#endif + #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) #include #else diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 3b8225763..6d0255d53 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) // generate nonce updateState(); if (currentState != KEY_VERIFICATION_IDLE) { - IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;) return false; } currentNonce = random(); @@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) p->priority = meshtastic_MeshPacket_Priority_HIGH; service->sendToMesh(p, RX_SRC_LOCAL, true); currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; - IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index e8da8e983..64e90c9c2 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,9 +1,12 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER #include "buzz/BuzzerFeedbackThread.h" -#include "modules/StatusLEDModule.h" #include "modules/SystemCommandsModule.h" #endif +#include "modules/StatusLEDModule.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +#include "ReplyBotModule.h" +#endif #if !MESHTASTIC_EXCLUDE_PKI #include "KeyVerificationModule.h" #endif @@ -53,11 +56,15 @@ #endif #if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "main.h" -#include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" #include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif +#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR +#include "main.h" +#include "modules/Telemetry/AirQualityTelemetry.h" +#include "modules/Telemetry/Sensor/TelemetrySensor.h" +#endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" #endif @@ -90,6 +97,9 @@ #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" #endif +#if !MESHTASTIC_EXCLUDE_STATUS +#include "modules/StatusMessageModule.h" +#endif #if defined(HAS_HARDWARE_WATCHDOG) #include "watchdog/watchdogThread.h" @@ -106,10 +116,10 @@ void setupModules() buzzerFeedbackThread = new BuzzerFeedbackThread(); } #endif -#if defined(LED_CHARGE) || defined(LED_PAIRING) statusLEDModule = new StatusLEDModule(); +#if !MESHTASTIC_EXCLUDE_REPLYBOT + new ReplyBotModule(); #endif - #if !MESHTASTIC_EXCLUDE_ADMIN adminModule = new AdminModule(); #endif @@ -150,6 +160,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif +#if !MESHTASTIC_EXCLUDE_STATUS + statusMessageModule = new StatusMessageModule(); +#endif #if !MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE new GenericThreadModule(); #endif diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index a568505ae..79bda557c 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -5,6 +5,7 @@ #include "NodeStatus.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "configuration.h" #include "main.h" #include @@ -133,11 +134,12 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() // Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient) uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline()); - if (!shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, timeoutMs)) { + uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0; + if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) { LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000); ignoreRequest = true; // Mark it as ignored for MeshModule return NULL; - } else if (shorterTimeout && lastSentToMesh && Throttle::isWithinTimespanMs(lastSentToMesh, 60 * 1000)) { + } else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) { // For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago"); ignoreRequest = true; @@ -148,7 +150,7 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() // Strip the public key if the user is licensed if (u.is_licensed && u.public_key.size > 0) { - u.public_key.bytes[0] = 0; + memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes)); u.public_key.size = 0; } @@ -159,7 +161,8 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply() strcpy(u.id, nodeDB->getNodeId().c_str()); LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); return allocDataProtobuf(u); } } diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h index d16fbeac2..0c0dec849 100644 --- a/src/modules/NodeInfoModule.h +++ b/src/modules/NodeInfoModule.h @@ -42,7 +42,6 @@ class NodeInfoModule : public ProtobufModule, private concurren virtual int32_t runOnce() override; private: - uint32_t lastSentToMesh = 0; // Last time we sent our NodeInfo to the mesh bool shorterTimeout = false; bool suppressReplyForCurrentRequest = false; std::map lastNodeInfoSeen; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index f7116e701..e9a21a22b 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -6,6 +6,7 @@ #include "NodeDB.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "TypeConversions.h" #include "airtime.h" #include "configuration.h" @@ -27,6 +28,15 @@ PositionModule::PositionModule() isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others nodeStatusObserver.observe(&nodeStatus->onNewStatus); + // Seed throttle timer from persisted transmit history so we don't re-broadcast immediately after reboot + if (transmitHistory) { + uint32_t restored = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + if (restored != 0) { + lastGpsSend = restored; + LOG_INFO("Position: restored lastGpsSend from transmit history"); + } + } + if (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER && config.device.role != meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { setIntervalFromNow(setStartDelay()); @@ -438,6 +448,8 @@ int32_t PositionModule::runOnce() lastGpsLatitude = node->position.latitude_i; lastGpsLongitude = node->position.longitude_i; + if (transmitHistory) + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); sendOurPosition(); if (config.device.role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { sendLostAndFoundText(); diff --git a/src/modules/PowerStressModule.cpp b/src/modules/PowerStressModule.cpp index d487fe6fc..1c073a10a 100644 --- a/src/modules/PowerStressModule.cpp +++ b/src/modules/PowerStressModule.cpp @@ -1,5 +1,4 @@ #include "PowerStressModule.h" -#include "Led.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" @@ -78,10 +77,12 @@ int32_t PowerStressModule::runOnce() switch (p.cmd) { case meshtastic_PowerStressMessage_Opcode_LED_ON: - ledForceOn.set(true); + // FIXME - implement + // ledForceOn.set(true); break; case meshtastic_PowerStressMessage_Opcode_LED_OFF: - ledForceOn.set(false); + // FIXME - implement + // ledForceOn.set(false); break; case meshtastic_PowerStressMessage_Opcode_GPS_ON: // FIXME - implement diff --git a/src/modules/ReplyBotModule.cpp b/src/modules/ReplyBotModule.cpp new file mode 100644 index 000000000..d391bf093 --- /dev/null +++ b/src/modules/ReplyBotModule.cpp @@ -0,0 +1,183 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +/* + * ReplyBotModule.cpp + * + * This module implements a simple reply bot for the Meshtastic firmware. It listens for + * specific text commands ("/ping", "/hello" and "/test") delivered either via a direct + * message (DM) or a broadcast on the primary channel. When a supported command is + * received the bot responds with a short status message that includes the hop count + * (minimum number of relays), RSSI and SNR of the received packet. To avoid spamming + * the network it enforces a per‑sender cooldown between responses. By default the + * module is disabled. See the official firmware documentation for guidance on adding modules. + * To enable this module, set `#undef MESHTASTIC_EXCLUDE_REPLYBOT` in your variant.h file. + */ + +#include "Channels.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "ReplyBotModule.h" +#include "mesh/MeshTypes.h" + +#include +#include +#include + +// +// Rate limiting data structures +// +// Each sender is tracked in a small ring buffer. When a message arrives from a +// sender we check the last time we responded to them. If the difference is +// less than the configured cooldown (different values for DM vs broadcast) +// the message is ignored; otherwise we update the last response time and +// proceed with replying. + +struct ReplyBotCooldownEntry { + uint32_t from = 0; + uint32_t lastMs = 0; +}; + +static constexpr uint8_t REPLYBOT_COOLDOWN_SLOTS = 8; // ring buffer size +static constexpr uint32_t REPLYBOT_DM_COOLDOWN_MS = 15 * 1000; // 15 seconds for DMs +static constexpr uint32_t REPLYBOT_LF_COOLDOWN_MS = 60 * 1000; // 60 seconds for LongFast broadcasts + +static ReplyBotCooldownEntry replybotCooldown[REPLYBOT_COOLDOWN_SLOTS]; +static uint8_t replybotCooldownIdx = 0; + +// Return true if a reply should be rate‑limited for this sender, updating the +// entry table as needed. +static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs) +{ + const uint32_t now = millis(); + for (auto &e : replybotCooldown) { + if (e.from == from) { + // Found existing entry; check if cooldown expired + if ((uint32_t)(now - e.lastMs) < cooldownMs) { + return true; + } + e.lastMs = now; + return false; + } + } + // No entry found – insert new sender into the ring + replybotCooldown[replybotCooldownIdx].from = from; + replybotCooldown[replybotCooldownIdx].lastMs = now; + replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS; + return false; +} + +// Constructor – registers a single text port and marks the module promiscuous +// so that broadcast messages on the primary channel are visible. +ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP) +{ + isPromiscuous = true; +} + +void ReplyBotModule::setup() +{ + // In future we may add a protobuf configuration; for now the module is + // always enabled when compiled in. +} + +// Determine whether we want to process this packet. We only care about +// plain text messages addressed to our port. +bool ReplyBotModule::wantPacket(const meshtastic_MeshPacket *p) +{ + return (p && p->decoded.portnum == ourPortNum); +} + +ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + // Accept only direct messages to us or broadcasts on the Primary channel + // (regardless of modem preset: LongFast, MediumFast, etc). + + const uint32_t ourNode = nodeDB->getNodeNum(); + const bool isDM = (mp.to == ourNode); + const bool isPrimaryChannel = (mp.channel == channels.getPrimaryIndex()) && isBroadcast(mp.to); + if (!isDM && !isPrimaryChannel) { + return ProcessMessage::CONTINUE; + } + + // Ignore empty payloads + if (mp.decoded.payload.size == 0) { + return ProcessMessage::CONTINUE; + } + + // Copy payload into a null‑terminated buffer + char buf[260]; + memset(buf, 0, sizeof(buf)); + size_t n = mp.decoded.payload.size; + if (n > sizeof(buf) - 1) + n = sizeof(buf) - 1; + memcpy(buf, mp.decoded.payload.bytes, n); + + // React only to supported slash commands + if (!isCommand(buf)) { + return ProcessMessage::CONTINUE; + } + + // Apply rate limiting per sender depending on DM/broadcast + const uint32_t cooldownMs = isDM ? REPLYBOT_DM_COOLDOWN_MS : REPLYBOT_LF_COOLDOWN_MS; + if (replybotRateLimited(mp.from, cooldownMs)) { + return ProcessMessage::CONTINUE; + } + + // Compute hop count indicator – if the relay_node is non‑zero we know + // there was at least one relay. Some firmware builds support a hop_start + // field which could be used for more accurate counts, but here we use + // the available relay_node flag only. + // int hopsAway = mp.hop_start - mp.hop_limit; + int hopsAway = getHopsAway(mp); + + // Normalize RSSI: if positive adjust down by 200 to align with typical values + int rssi = mp.rx_rssi; + if (rssi > 0) { + rssi -= 200; + } + float snr = mp.rx_snr; + + // Build the reply message and send it back via DM + char reply[96]; + snprintf(reply, sizeof(reply), "🎙️ Mic Check : %d Hops away | RSSI %d | SNR %.1f", hopsAway, rssi, snr); + sendDm(mp, reply); + return ProcessMessage::CONTINUE; +} + +// Check if the message starts with one of the supported commands. Leading +// whitespace is skipped and commands must be followed by end‑of‑string or +// whitespace. +bool ReplyBotModule::isCommand(const char *msg) const +{ + if (!msg) + return false; + while (*msg == ' ' || *msg == '\t') + msg++; + auto isEndOrSpace = [](char c) { return c == '\0' || std::isspace(static_cast(c)); }; + if (strncmp(msg, "/ping", 5) == 0 && isEndOrSpace(msg[5])) + return true; + if (strncmp(msg, "/hello", 6) == 0 && isEndOrSpace(msg[6])) + return true; + if (strncmp(msg, "/test", 5) == 0 && isEndOrSpace(msg[5])) + return true; + return false; +} + +// Send a direct message back to the originating node. +void ReplyBotModule::sendDm(const meshtastic_MeshPacket &rx, const char *text) +{ + if (!text) + return; + meshtastic_MeshPacket *p = allocDataPacket(); + p->to = rx.from; + p->channel = rx.channel; + p->want_ack = false; + p->decoded.want_response = false; + size_t len = strlen(text); + if (len > sizeof(p->decoded.payload.bytes)) { + len = sizeof(p->decoded.payload.bytes); + } + p->decoded.payload.size = len; + memcpy(p->decoded.payload.bytes, text, len); + service->sendToMesh(p); +} +#endif // MESHTASTIC_EXCLUDE_REPLYBOT \ No newline at end of file diff --git a/src/modules/ReplyBotModule.h b/src/modules/ReplyBotModule.h new file mode 100644 index 000000000..a5a8f6bb4 --- /dev/null +++ b/src/modules/ReplyBotModule.h @@ -0,0 +1,19 @@ +#pragma once +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_REPLYBOT +#include "SinglePortModule.h" +#include "mesh/generated/meshtastic/mesh.pb.h" + +class ReplyBotModule : public SinglePortModule +{ + public: + ReplyBotModule(); + void setup() override; + bool wantPacket(const meshtastic_MeshPacket *p) override; + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + protected: + bool isCommand(const char *msg) const; + void sendDm(const meshtastic_MeshPacket &rx, const char *text); +}; +#endif // MESHTASTIC_EXCLUDE_REPLYBOT \ No newline at end of file diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index e7a405bdf..f828f4a16 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -13,15 +13,16 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER if (inputBroker) inputObserver.observe(inputBroker); +#endif } int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { switch (arg->getStatusType()) { case STATUS_TYPE_POWER: { - meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)arg; if (powerStatus->getHasUSB() || powerStatus->getIsCharging()) { power_state = charging; if (powerStatus->getBatteryChargePercent() >= 100) { @@ -37,7 +38,6 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) break; } case STATUS_TYPE_BLUETOOTH: { - meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)arg; switch (bluetoothStatus->getConnectionState()) { case meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED: { ble_state = unpaired; @@ -62,19 +62,22 @@ int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) } return 0; }; - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER int StatusLEDModule::handleInputEvent(const InputEvent *event) { lastUserbuttonTime = millis(); return 0; } +#endif int32_t StatusLEDModule::runOnce() { my_interval = 1000; if (power_state == charging) { +#ifndef POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING CHARGE_LED_state = !CHARGE_LED_state; +#endif } else if (power_state == charged) { CHARGE_LED_state = LED_STATE_ON; } else if (power_state == critical) { @@ -88,13 +91,19 @@ int32_t StatusLEDModule::runOnce() my_interval = 250; if (POWER_LED_starttime + 2000 < millis()) { doing_fast_blink = false; + CHARGE_LED_state = LED_STATE_OFF; } - } else { - CHARGE_LED_state = LED_STATE_OFF; } + } - } else { - CHARGE_LED_state = LED_STATE_OFF; + if (power_state != charging && power_state != charged && !doing_fast_blink) { + if (CHARGE_LED_state == LED_STATE_ON) { + CHARGE_LED_state = LED_STATE_OFF; + my_interval = 999; + } else { + CHARGE_LED_state = LED_STATE_ON; + my_interval = 1; + } } if (!config.bluetooth.enabled || PAIRING_LED_starttime + 30 * 1000 < millis() || doing_fast_blink) { @@ -112,6 +121,11 @@ int32_t StatusLEDModule::runOnce() PAIRING_LED_state = LED_STATE_ON; } + // Override if disabled in config + if (config.device.led_heartbeat_disabled) { + CHARGE_LED_state = LED_STATE_OFF; + } +#ifdef Battery_LED_1 bool chargeIndicatorLED1 = LED_STATE_OFF; bool chargeIndicatorLED2 = LED_STATE_OFF; bool chargeIndicatorLED3 = LED_STATE_OFF; @@ -126,14 +140,38 @@ int32_t StatusLEDModule::runOnce() if (powerStatus && powerStatus->getBatteryChargePercent() >= 75) chargeIndicatorLED4 = LED_STATE_ON; } +#endif -#ifdef LED_CHARGE - digitalWrite(LED_CHARGE, CHARGE_LED_state); +#if defined(HAS_PMU) + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(CHARGE_LED_state ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); + } +#endif + +#ifdef PCA_LED_POWER + io.digitalWrite(PCA_LED_POWER, CHARGE_LED_state); +#endif +#ifdef PCA_LED_ENABLE + io.digitalWrite(PCA_LED_ENABLE, CHARGE_LED_state); +#endif +#ifdef LED_POWER + digitalWrite(LED_POWER, CHARGE_LED_state); #endif #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef RGB_LED_POWER + if (!config.device.led_heartbeat_disabled) { + if (CHARGE_LED_state == LED_STATE_ON) { + ambientLightingThread->setLighting(10, 255, 0, 0); + } else { + ambientLightingThread->setLighting(0, 0, 0, 0); + } + } +#endif + #ifdef Battery_LED_1 digitalWrite(Battery_LED_1, chargeIndicatorLED1); #endif @@ -149,3 +187,40 @@ int32_t StatusLEDModule::runOnce() return (my_interval); } + +void StatusLEDModule::setPowerLED(bool LEDon) +{ + +#if defined(HAS_PMU) + if (pmu_found && PMU) { + // blink the axp led + PMU->setChargingLedMode(LEDon ? XPOWERS_CHG_LED_ON : XPOWERS_CHG_LED_OFF); + } +#endif + uint8_t ledState = LEDon ? LED_STATE_ON : LED_STATE_OFF; +#ifdef PCA_LED_POWER + io.digitalWrite(PCA_LED_POWER, ledState); +#endif +#ifdef PCA_LED_ENABLE + io.digitalWrite(PCA_LED_ENABLE, ledState); +#endif +#ifdef LED_POWER + digitalWrite(LED_POWER, ledState); +#endif +#ifdef LED_PAIRING + digitalWrite(LED_PAIRING, ledState); +#endif + +#ifdef Battery_LED_1 + digitalWrite(Battery_LED_1, ledState); +#endif +#ifdef Battery_LED_2 + digitalWrite(Battery_LED_2, ledState); +#endif +#ifdef Battery_LED_3 + digitalWrite(Battery_LED_3, ledState); +#endif +#ifdef Battery_LED_4 + digitalWrite(Battery_LED_4, ledState); +#endif +} diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index 98020cb32..972e26737 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -5,10 +5,14 @@ #include "PowerStatus.h" #include "concurrency/OSThread.h" #include "configuration.h" -#include "input/InputBroker.h" +#include "main.h" #include #include +#if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "input/InputBroker.h" +#endif + class StatusLEDModule : private concurrency::OSThread { bool slowTrack = false; @@ -17,8 +21,11 @@ class StatusLEDModule : private concurrency::OSThread StatusLEDModule(); int handleStatusUpdate(const meshtastic::Status *); - +#if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); +#endif + + void setPowerLED(bool); protected: unsigned int my_interval = 1000; // interval in millisconds @@ -28,8 +35,10 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); CallbackObserver powerStatusObserver = CallbackObserver(this, &StatusLEDModule::handleStatusUpdate); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER CallbackObserver inputObserver = CallbackObserver(this, &StatusLEDModule::handleInputEvent); +#endif private: bool CHARGE_LED_state = LED_STATE_OFF; @@ -50,3 +59,7 @@ class StatusLEDModule : private concurrency::OSThread }; extern StatusLEDModule *statusLEDModule; +#ifdef RGB_LED_POWER +#include "AmbientLightingThread.h" +extern AmbientLightingThread *ambientLightingThread; +#endif diff --git a/src/modules/StatusMessageModule.cpp b/src/modules/StatusMessageModule.cpp new file mode 100644 index 000000000..139a74d8e --- /dev/null +++ b/src/modules/StatusMessageModule.cpp @@ -0,0 +1,41 @@ +#if !MESHTASTIC_EXCLUDE_STATUS + +#include "StatusMessageModule.h" +#include "MeshService.h" +#include "ProtobufModule.h" + +StatusMessageModule *statusMessageModule; + +int32_t StatusMessageModule::runOnce() +{ + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + // create and send message with the status message set + meshtastic_StatusMessage ourStatus = meshtastic_StatusMessage_init_zero; + strncpy(ourStatus.status, moduleConfig.statusmessage.node_status, sizeof(ourStatus.status)); + ourStatus.status[sizeof(ourStatus.status) - 1] = '\0'; // ensure null termination + meshtastic_MeshPacket *p = allocDataPacket(); + p->decoded.payload.size = pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), + meshtastic_StatusMessage_fields, &ourStatus); + p->to = NODENUM_BROADCAST; + p->decoded.want_response = false; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + p->channel = 0; + service->sendToMesh(p); + } + + return 1000 * 12 * 60 * 60; +} + +ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp) +{ + if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + meshtastic_StatusMessage incomingMessage; + if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields, + &incomingMessage)) { + LOG_INFO("Received a NodeStatus message %s", incomingMessage.status); + } + } + return ProcessMessage::CONTINUE; +} + +#endif \ No newline at end of file diff --git a/src/modules/StatusMessageModule.h b/src/modules/StatusMessageModule.h new file mode 100644 index 000000000..c9ff54018 --- /dev/null +++ b/src/modules/StatusMessageModule.h @@ -0,0 +1,35 @@ +#pragma once +#if !MESHTASTIC_EXCLUDE_STATUS +#include "SinglePortModule.h" +#include "configuration.h" + +class StatusMessageModule : public SinglePortModule, private concurrency::OSThread +{ + + public: + /** Constructor + * name is for debugging output + */ + StatusMessageModule() + : SinglePortModule("statusMessage", meshtastic_PortNum_NODE_STATUS_APP), concurrency::OSThread("StatusMessage") + { + if (moduleConfig.has_statusmessage && moduleConfig.statusmessage.node_status[0] != '\0') { + this->setInterval(2 * 60 * 1000); + } else { + this->setInterval(1000 * 12 * 60 * 60); + } + // TODO: If we have a string, set the initial delay (15 minutes maybe) + } + + virtual int32_t runOnce() override; + + protected: + /** Called to handle a particular incoming message + */ + virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; + + private: +}; + +extern StatusMessageModule *statusMessageModule; +#endif \ No newline at end of file diff --git a/src/modules/StoreForwardModule.cpp b/src/modules/StoreForwardModule.cpp index 023a1c798..135d9f6eb 100644 --- a/src/modules/StoreForwardModule.cpp +++ b/src/modules/StoreForwardModule.cpp @@ -131,9 +131,7 @@ void StoreForwardModule::historySend(uint32_t secAgo, uint32_t to) uint32_t StoreForwardModule::getNumAvailablePackets(NodeNum dest, uint32_t last_time) { uint32_t count = 0; - if (lastRequest.find(dest) == lastRequest.end()) { - lastRequest.emplace(dest, 0); - } + lastRequest.emplace(dest, 0); for (uint32_t i = lastRequest[dest]; i < this->packetHistoryTotalCount; i++) { if (this->packetHistory[i].time && (this->packetHistory[i].time > last_time)) { // Client is only interested in packets not from itself and only in broadcast packets or packets towards it. diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index 01f5da2c6..ca853d051 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,3 +1,4 @@ +#include "DebugConfiguration.h" #include "configuration.h" #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR @@ -10,7 +11,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" -#include "Sensor/AddI2CSensorTemplate.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -19,8 +20,21 @@ #include "sleep.h" #include +static constexpr uint16_t TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY = 0x8004; + // Sensors +#include "Sensor/AddI2CSensorTemplate.h" #include "Sensor/PMSA003ISensor.h" +#include "Sensor/SEN5XSensor.h" +#if __has_include() +#include "Sensor/SCD4XSensor.h" +#endif +#if __has_include() +#include "Sensor/SFA30Sensor.h" +#endif +#if __has_include() +#include "Sensor/SCD30Sensor.h" +#endif void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -42,6 +56,16 @@ void AirQualityTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) // order by priority of metrics/values (low top, high bottom) addSensor(i2cScanner, ScanI2C::DeviceType::PMSA003I); + addSensor(i2cScanner, ScanI2C::DeviceType::SEN5X); +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SCD4X); +#endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SFA30); +#endif +#if __has_include() + addSensor(i2cScanner, ScanI2C::DeviceType::SCD30); +#endif } int32_t AirQualityTelemetryModule::runOnce() @@ -63,7 +87,8 @@ int32_t AirQualityTelemetryModule::runOnce() } if (firstTime) { - // This is the first time the OSThread library has called this function, so do some setup + // This is the first time the OSThread library has called this function, so + // do some setup firstTime = false; if (moduleConfig.telemetry.air_quality_enabled) { @@ -85,21 +110,41 @@ int32_t AirQualityTelemetryModule::runOnce() } // Wake up the sensors that need it - LOG_INFO("Waking up sensors"); + LOG_INFO("Waking up sensors..."); + uint32_t lastTelemetry = + transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY) : 0; for (TelemetrySensor *sensor : sensors) { - if (!sensor->isActive()) { - return sensor->wakeUp(); + if (!sensor->canSleep()) { + LOG_DEBUG("%s sensor doesn't have sleep feature. Skipping", sensor->sensorName); + } else if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry - sensor->wakeUpTimeMs(), + Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && + airTime->isTxAllowedAirUtil()) { + if (!sensor->isActive()) { + LOG_DEBUG("Waking up: %s", sensor->sensorName); + return sensor->wakeUp(); + } else { + int32_t pendingForReadyMs = sensor->pendingForReadyMs(); + LOG_DEBUG("%s. Pending for ready %ums", sensor->sensorName, pendingForReadyMs); + if (pendingForReadyMs) { + return pendingForReadyMs; + } + } } } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.air_quality_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_AIR_QUALITY_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -109,9 +154,18 @@ int32_t AirQualityTelemetryModule::runOnce() } // Send to sleep sensors that consume power - LOG_INFO("Sending sensors to sleep"); + LOG_DEBUG("Sending sensors to sleep"); for (TelemetrySensor *sensor : sensors) { - sensor->sleep(); + if (sensor->isActive() && sensor->canSleep()) { + if (sensor->wakeUpTimeMs() < + (int32_t)Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes)) { + LOG_DEBUG("Disabling %s until next period", sensor->sensorName); + sensor->sleep(); + } else { + LOG_DEBUG("Sensor stays enabled due to warm up period"); + } + } } } return min(sendToPhoneIntervalMs, result); @@ -158,8 +212,7 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta const auto &m = telemetry.variant.air_quality_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_pm10_environmental || - m.has_pm25_environmental || m.has_pm100_environmental; + bool hasAny = m.has_pm10_standard || m.has_pm25_standard || m.has_pm100_standard || m.has_co2; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -186,6 +239,10 @@ void AirQualityTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSta entries.push_back("PM2.5: " + String(m.pm25_standard) + "ug/m3"); if (m.has_pm100_standard) entries.push_back("PM10: " + String(m.pm100_standard) + "ug/m3"); + if (m.has_co2) + entries.push_back("CO2: " + String(m.co2) + "ppm"); + if (m.has_form_formaldehyde) + entries.push_back("HCHO: " + String(m.form_formaldehyde) + "ppb"); // === Show first available metric on top-right of first line === if (!entries.empty()) { @@ -221,13 +278,19 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack #if defined(DEBUG_PORT) && !defined(DEBUG_MUTE) const char *sender = getSenderShortName(mp); - LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, pm100_standard=%i", sender, - t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, - t->variant.air_quality_metrics.pm100_standard); + if (t->variant.air_quality_metrics.has_pm10_standard) + LOG_INFO("(Received from %s): pm10_standard=%i, pm25_standard=%i, " + "pm100_standard=%i", + sender, t->variant.air_quality_metrics.pm10_standard, t->variant.air_quality_metrics.pm25_standard, + t->variant.air_quality_metrics.pm100_standard); - LOG_INFO(" | PM1.0(Environmental)=%i, PM2.5(Environmental)=%i, PM10.0(Environmental)=%i", - t->variant.air_quality_metrics.pm10_environmental, t->variant.air_quality_metrics.pm25_environmental, - t->variant.air_quality_metrics.pm100_environmental); + if (t->variant.air_quality_metrics.has_co2) + LOG_INFO("CO2=%i, CO2_T=%.2f, CO2_H=%.2f", t->variant.air_quality_metrics.co2, + t->variant.air_quality_metrics.co2_temperature, t->variant.air_quality_metrics.co2_humidity); + + if (t->variant.air_quality_metrics.has_form_formaldehyde) + LOG_INFO("HCHO=%.2f, HCHO_T=%.2f, HCHO_H=%.2f", t->variant.air_quality_metrics.form_formaldehyde, + t->variant.air_quality_metrics.form_temperature, t->variant.air_quality_metrics.form_humidity); #endif // release previous packet before occupying a new spot if (lastMeasurementPacket != nullptr) @@ -241,17 +304,20 @@ bool AirQualityTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPack bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) { - bool valid = true; + // Note: this is different to the case in EnvironmentTelemetryModule + // There, if any sensor fails to read - valid = false. + bool valid = false; bool hasSensor = false; m->time = getTime(); m->which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m->variant.air_quality_metrics = meshtastic_AirQualityMetrics_init_zero; - // TODO - Should we check for sensor state here? - // If a sensor is sleeping, we should know and check to wake it up + bool sensor_get = false; for (TelemetrySensor *sensor : sensors) { - LOG_INFO("Reading AQ sensors"); - valid = valid && sensor->getMetrics(m); + LOG_DEBUG("Reading %s", sensor->sensorName); + // Note - this function doesn't get properly called if within a conditional + sensor_get = sensor->getMetrics(m); + valid = valid || sensor_get; hasSensor = true; } @@ -261,6 +327,10 @@ bool AirQualityTelemetryModule::getAirQualityTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *AirQualityTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; @@ -291,12 +361,38 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_Telemetry m = meshtastic_Telemetry_init_zero; m.which_variant = meshtastic_Telemetry_air_quality_metrics_tag; m.time = getTime(); + if (getAirQualityTelemetry(&m)) { - LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u, \ - pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", - m.variant.air_quality_metrics.pm10_standard, m.variant.air_quality_metrics.pm25_standard, - m.variant.air_quality_metrics.pm100_standard, m.variant.air_quality_metrics.pm10_environmental, - m.variant.air_quality_metrics.pm25_environmental, m.variant.air_quality_metrics.pm100_environmental); + + bool hasAnyPM = + m.variant.air_quality_metrics.has_pm10_standard || m.variant.air_quality_metrics.has_pm25_standard || + m.variant.air_quality_metrics.has_pm100_standard || m.variant.air_quality_metrics.has_pm10_environmental || + m.variant.air_quality_metrics.has_pm25_environmental || m.variant.air_quality_metrics.has_pm100_environmental; + + if (hasAnyPM) { + LOG_INFO("Send: pm10_standard=%u, pm25_standard=%u, pm100_standard=%u", m.variant.air_quality_metrics.pm10_standard, + m.variant.air_quality_metrics.pm25_standard, m.variant.air_quality_metrics.pm100_standard); + if (m.variant.air_quality_metrics.has_pm10_environmental) + LOG_INFO("pm10_environmental=%u, pm25_environmental=%u, pm100_environmental=%u", + m.variant.air_quality_metrics.pm10_environmental, m.variant.air_quality_metrics.pm25_environmental, + m.variant.air_quality_metrics.pm100_environmental); + } + + bool hasAnyCO2 = m.variant.air_quality_metrics.has_co2 || m.variant.air_quality_metrics.has_co2_temperature || + m.variant.air_quality_metrics.has_co2_humidity; + + if (hasAnyCO2) { + LOG_INFO("Send: co2=%i, co2_t=%.2f, co2_rh=%.2f", m.variant.air_quality_metrics.co2, + m.variant.air_quality_metrics.co2_temperature, m.variant.air_quality_metrics.co2_humidity); + } + + bool hasAnyHCHO = m.variant.air_quality_metrics.has_form_formaldehyde || + m.variant.air_quality_metrics.has_form_temperature || m.variant.air_quality_metrics.has_form_humidity; + + if (hasAnyHCHO) { + LOG_INFO("Send: hcho=%.2f, hcho_t=%.2f, hcho_rh=%.2f", m.variant.air_quality_metrics.form_formaldehyde, + m.variant.air_quality_metrics.form_temperature, m.variant.air_quality_metrics.form_humidity); + } meshtastic_MeshPacket *p = allocDataProtobuf(m); p->to = dest; @@ -331,6 +427,20 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_DEBUG("Start next execution in 5s, then sleep"); setIntervalFromNow(FIVE_SECONDS_MS); } + + if (config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR && config.power.is_power_saving) { + meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); + notification->level = meshtastic_LogRecord_Level_INFO; + notification->time = getValidTime(RTCQualityFromNet); + sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", + Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, + default_telemetry_broadcast_interval_secs) / + 1000U); + service->sendClientNotification(notification); + sleepOnNextExecution = true; + LOG_DEBUG("Start next execution in 5s, then sleep"); + setIntervalFromNow(FIVE_SECONDS_MS); + } } return true; } @@ -352,4 +462,4 @@ AdminMessageHandleResult AirQualityTelemetryModule::handleAdminMessageForModule( return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/AirQualityTelemetry.h b/src/modules/Telemetry/AirQualityTelemetry.h index 2b88b74ba..04936d8c1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.h +++ b/src/modules/Telemetry/AirQualityTelemetry.h @@ -4,6 +4,8 @@ #pragma once +#include "BaseTelemetryModule.h" + #ifndef AIR_QUALITY_TELEMETRY_MODULE_ENABLE #define AIR_QUALITY_TELEMETRY_MODULE_ENABLE 0 #endif @@ -17,6 +19,7 @@ class AirQualityTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, + public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = @@ -64,8 +67,8 @@ class AirQualityTelemetryModule : private concurrency::OSThread, bool firstTime = true; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; + // uint32_t sendToPhoneIntervalMs = 1000; // Send to phone every minute uint32_t lastSentToPhone = 0; }; -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/BaseTelemetryModule.h b/src/modules/Telemetry/BaseTelemetryModule.h new file mode 100644 index 000000000..b032bef3f --- /dev/null +++ b/src/modules/Telemetry/BaseTelemetryModule.h @@ -0,0 +1,14 @@ +#pragma once + +#include "NodeDB.h" +#include "configuration.h" + +class BaseTelemetryModule +{ + protected: + bool isSensorOrRouterRole() const + { + return config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR || + config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER; + } +}; diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 066b9361d..1c2d18c71 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -7,6 +7,7 @@ #include "RTC.h" #include "RadioLibInterface.h" #include "Router.h" +#include "TransmitHistory.h" #include "configuration.h" #include "main.h" #include "memGet.h" @@ -15,21 +16,23 @@ #include #define MAGIC_USB_BATTERY_LEVEL 101 +static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001; int32_t DeviceTelemetryModule::runOnce() { refreshUptime(); - bool isImpoliteRole = - IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER); - if (((lastSentToMesh == 0) || - ((uptimeLastMs - lastSentToMesh) >= - Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0; + bool isImpoliteRole = isSensorOrRouterRole(); + if (((lastTelemetry == 0) || + ((uptimeLastMs - lastTelemetry) >= Default::getConfiguredOrDefaultMsScaled(moduleConfig.telemetry.device_update_interval, + default_telemetry_broadcast_interval_secs, + numOnlineNodes))) && airTime->isTxAllowedChannelUtil(!isImpoliteRole) && airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN && moduleConfig.telemetry.device_telemetry_enabled) { sendTelemetry(); - lastSentToMesh = uptimeLastMs; + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_DEVICE_TELEMETRY); } else if (service->isToPhoneQueueEmpty()) { // Just send to phone when it's not our time to send to mesh yet // Only send while queue is empty (phone assumed connected) @@ -60,6 +63,10 @@ bool DeviceTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket & meshtastic_MeshPacket *DeviceTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/DeviceTelemetry.h b/src/modules/Telemetry/DeviceTelemetry.h index a1d55a596..f37afee70 100644 --- a/src/modules/Telemetry/DeviceTelemetry.h +++ b/src/modules/Telemetry/DeviceTelemetry.h @@ -1,11 +1,14 @@ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModule +class DeviceTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &DeviceTelemetryModule::handleStatusUpdate); @@ -48,7 +51,6 @@ class DeviceTelemetryModule : private concurrency::OSThread, public ProtobufModu uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t sendStatsToPhoneIntervalMs = 15 * SECONDS_IN_MINUTE * 1000; // Send stats to phone every 15 minutes uint32_t lastSentStatsToPhone = 0; - uint32_t lastSentToMesh = 0; void refreshUptime() { diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 140c2c17e..b7b6e04a9 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "buzz.h" #include "graphics/SharedUIDisplay.h" @@ -145,6 +146,8 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include +static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; + void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { if (!moduleConfig.telemetry.environment_measurement_enabled && !ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE) { @@ -297,7 +300,8 @@ int32_t EnvironmentTelemetryModule::runOnce() // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the // sensormap here. #ifdef HAS_RAKPROT - result = rak9154Sensor.runOnce(); + if (rak9154Sensor.hasSensor()) + result = rak9154Sensor.runOnce(); #endif #endif } @@ -317,14 +321,17 @@ int32_t EnvironmentTelemetryModule::runOnce() } } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.environment_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = + transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.environment_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -567,9 +574,11 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m } #endif #ifdef HAS_RAKPROT - get_metrics = rak9154Sensor.getMetrics(m); - valid = valid || get_metrics; - hasSensor = true; + if (rak9154Sensor.hasSensor()) { + get_metrics = rak9154Sensor.getMetrics(m); + valid = valid || get_metrics; + hasSensor = true; + } #endif return valid && hasSensor; } @@ -577,6 +586,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m meshtastic_MeshPacket *EnvironmentTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 049ed6b77..0b7e0f4cb 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -4,6 +4,8 @@ #pragma once +#include "BaseTelemetryModule.h" + #ifndef ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE #define ENVIRONMENTAL_TELEMETRY_MODULE_ENABLE 0 #endif @@ -17,6 +19,7 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, + public BaseTelemetryModule, public ProtobufModule { CallbackObserver nodeStatusObserver = @@ -65,7 +68,6 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; }; diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index bb3555062..ae6b366bd 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerFSM.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "UnitConversions.h" #include "main.h" #include "power.h" @@ -33,6 +34,8 @@ MLX90614Sensor mlx90614Sensor; #endif #include +static constexpr uint16_t TX_HISTORY_KEY_HEALTH_TELEMETRY = 0x8003; + int32_t HealthTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -69,14 +72,16 @@ int32_t HealthTelemetryModule::runOnce() return disable(); } - if (((lastSentToMesh == 0) || - !Throttle::isWithinTimespanMs(lastSentToMesh, Default::getConfiguredOrDefaultMsScaled( - moduleConfig.telemetry.health_update_interval, - default_telemetry_broadcast_interval_secs, numOnlineNodes))) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_HEALTH_TELEMETRY) : 0; + if (((lastTelemetry == 0) || + !Throttle::isWithinTimespanMs(lastTelemetry, Default::getConfiguredOrDefaultMsScaled( + moduleConfig.telemetry.health_update_interval, + default_telemetry_broadcast_interval_secs, numOnlineNodes))) && airTime->isTxAllowedChannelUtil(config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_HEALTH_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -192,6 +197,10 @@ bool HealthTelemetryModule::getHealthTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *HealthTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/HealthTelemetry.h b/src/modules/Telemetry/HealthTelemetry.h index 01e4c2372..4d0722201 100644 --- a/src/modules/Telemetry/HealthTelemetry.h +++ b/src/modules/Telemetry/HealthTelemetry.h @@ -4,12 +4,15 @@ #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModule +class HealthTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &HealthTelemetryModule::handleStatusUpdate); @@ -52,7 +55,6 @@ class HealthTelemetryModule : private concurrency::OSThread, public ProtobufModu bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 9047c7cd4..d02aed9c2 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerTelemetry.h" #include "RTC.h" #include "Router.h" +#include "TransmitHistory.h" #include "graphics/SharedUIDisplay.h" #include "main.h" #include "power.h" @@ -22,6 +23,8 @@ #include "graphics/ScreenFonts.h" #include +static constexpr uint16_t TX_HISTORY_KEY_POWER_TELEMETRY = 0x8005; + namespace graphics { extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, @@ -88,10 +91,12 @@ int32_t PowerTelemetryModule::runOnce() if (!moduleConfig.telemetry.power_measurement_enabled) return disable(); - if (((lastSentToMesh == 0) || !Throttle::isWithinTimespanMs(lastSentToMesh, sendToMeshIntervalMs)) && + uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_POWER_TELEMETRY) : 0; + if (((lastTelemetry == 0) || !Throttle::isWithinTimespanMs(lastTelemetry, sendToMeshIntervalMs)) && airTime->isTxAllowedAirUtil()) { sendTelemetry(); - lastSentToMesh = millis(); + if (transmitHistory) + transmitHistory->setLastSentToMesh(TX_HISTORY_KEY_POWER_TELEMETRY); } else if (((lastSentToPhone == 0) || !Throttle::isWithinTimespanMs(lastSentToPhone, sendToPhoneIntervalMs)) && (service->isToPhoneQueueEmpty())) { // Just send to phone when it's not our time to send to mesh yet @@ -217,6 +222,10 @@ bool PowerTelemetryModule::getPowerTelemetry(meshtastic_Telemetry *m) meshtastic_MeshPacket *PowerTelemetryModule::allocReply() { if (currentRequest) { + if (isMultiHopBroadcastRequest() && !isSensorOrRouterRole()) { + ignoreRequest = true; + return NULL; + } auto req = *currentRequest; const auto &p = req.decoded; meshtastic_Telemetry scratch; diff --git a/src/modules/Telemetry/PowerTelemetry.h b/src/modules/Telemetry/PowerTelemetry.h index b9ec6edc1..134b40b6b 100644 --- a/src/modules/Telemetry/PowerTelemetry.h +++ b/src/modules/Telemetry/PowerTelemetry.h @@ -5,12 +5,15 @@ #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "BaseTelemetryModule.h" #include "NodeDB.h" #include "ProtobufModule.h" #include #include -class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModule +class PowerTelemetryModule : private concurrency::OSThread, + public BaseTelemetryModule, + public ProtobufModule { CallbackObserver nodeStatusObserver = CallbackObserver(this, &PowerTelemetryModule::handleStatusUpdate); @@ -51,7 +54,6 @@ class PowerTelemetryModule : private concurrency::OSThread, public ProtobufModul bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute - uint32_t lastSentToMesh = 0; uint32_t lastSentToPhone = 0; uint32_t sensor_read_error_count = 0; }; diff --git a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h index 37d909d71..b7029986b 100644 --- a/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h +++ b/src/modules/Telemetry/Sensor/AddI2CSensorTemplate.h @@ -8,7 +8,7 @@ static std::forward_list sensors; -template void addSensor(ScanI2C *i2cScanner, ScanI2C::DeviceType type) +template void addSensor(const ScanI2C *i2cScanner, ScanI2C::DeviceType type) { ScanI2C::FoundDevice dev = i2cScanner->find(type); if (dev.type != ScanI2C::DeviceType::NONE || type == ScanI2C::DeviceType::NONE) { diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 3a1eb9532..c202028e1 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -8,6 +8,10 @@ #include "SPILock.h" #include "TelemetrySensor.h" +#if __has_include() +#include +#endif + BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {} #if __has_include() @@ -96,8 +100,28 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement) 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; + float gasRaw = bme680->readGas(); + measurement->variant.environment_metrics.gas_resistance = gasRaw / 1000.0; + + // IAQ approximation: humidity-compensated logarithmic mapping of gas resistance + // Gas sensor resistance drops with humidity; compensate to a 40% RH reference baseline + // Map compensated gas resistance (Ohms) to IAQ 0-500 using log-linear interpolation + // Clean air reference ~400 kOhm, polluted reference ~5 kOhm + if (gasRaw > 0.0f && !isfinite(gasRaw)) { + + static constexpr float LOG_UPPER = 12.899219f; // log(400k) + static constexpr float LOG_RANGE_INV = 1.0f / (12.899219f - 8.517193f); // 1 / (log(400k) - log(5k)) + measurement->variant.environment_metrics.has_iaq = true; + measurement->variant.environment_metrics.iaq = (uint16_t)(fminf( + fmaxf(((LOG_UPPER - + logf(fmaxf(gasRaw * expf(0.035f * (measurement->variant.environment_metrics.relative_humidity - 40.0f)), + 1.0f))) * + LOG_RANGE_INV) * + 500.0f, + 0.0f), + 500.0f)); + } #endif return true; } diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index eaeceb848..1134f04d9 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -28,7 +28,7 @@ class BME680Sensor : public TelemetrySensor #else using BME680Ptr = std::unique_ptr; - static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique(bus); } + static BME680Ptr makeBME680(TwoWire *bus) { return BME680Ptr(new Adafruit_BME680(bus)); } BME680Ptr bme680; #endif diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp index 2225a4d87..e34b70a1f 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.cpp @@ -21,26 +21,29 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) _bus = bus; _address = dev->address.address; -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); - if (!currentClock) { - LOG_WARN("PMSA003I can't be used at this clock speed"); - return false; - } -#endif +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ _bus->beginTransmission(_address); if (_bus->endTransmission() != 0) { - LOG_WARN("PMSA003I not found on I2C at 0x12"); + LOG_WARN("%s not found on I2C at 0x12", sensorName); return false; } #if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); + reClockI2C(currentClock, _bus, false); #endif status = 1; - LOG_INFO("PMSA003I Enabled"); + LOG_INFO("%s Enabled", sensorName); initI2CSensor(); return true; @@ -49,34 +52,41 @@ bool PMSA003ISensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) { if (!isActive()) { - LOG_WARN("PMSA003I is not active"); + LOG_WARN("Can't get metrics. %s is not active", sensorName); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus); -#endif +#ifdef PMSA003I_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(PMSA003I_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* PMSA003I_I2C_CLOCK_SPEED */ - _bus->requestFrom(_address, PMSA003I_FRAME_LENGTH); + _bus->requestFrom(_address, (uint8_t)PMSA003I_FRAME_LENGTH); if (_bus->available() < PMSA003I_FRAME_LENGTH) { - LOG_WARN("PMSA003I read failed: incomplete data (%d bytes)", _bus->available()); + LOG_WARN("%s read failed: incomplete data (%d bytes)", sensorName, _bus->available()); return false; } -#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) - reClockI2C(currentClock, _bus); -#endif - for (uint8_t i = 0; i < PMSA003I_FRAME_LENGTH; i++) { buffer[i] = _bus->read(); } +#if defined(PMSA003I_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + if (buffer[0] != 0x42 || buffer[1] != 0x4D) { - LOG_WARN("PMSA003I frame header invalid: 0x%02X 0x%02X", buffer[0], buffer[1]); + LOG_WARN("%s frame header invalid: 0x%02X 0x%02X", sensorName, buffer[0], buffer[1]); return false; } - auto read16 = [](uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; + auto read16 = [](const uint8_t *data, uint8_t idx) -> uint16_t { return (data[idx] << 8) | data[idx + 1]; }; computedChecksum = 0; @@ -86,7 +96,7 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) receivedChecksum = read16(buffer, PMSA003I_FRAME_LENGTH - 2); if (computedChecksum != receivedChecksum) { - LOG_WARN("PMSA003I checksum failed: computed 0x%04X, received 0x%04X", computedChecksum, receivedChecksum); + LOG_WARN("%s checksum failed: computed 0x%04X, received 0x%04X", sensorName, computedChecksum, receivedChecksum); return false; } @@ -128,6 +138,10 @@ bool PMSA003ISensor::getMetrics(meshtastic_Telemetry *measurement) measurement->variant.air_quality_metrics.has_particles_100um = true; measurement->variant.air_quality_metrics.particles_100um = read16(buffer, 26); + LOG_DEBUG("Got %s readings: pM1p0_standard=%u, pM2p5_standard=%u, pM10p0_standard=%u", sensorName, + measurement->variant.air_quality_metrics.pm10_standard, measurement->variant.air_quality_metrics.pm25_standard, + measurement->variant.air_quality_metrics.pm100_standard); + return true; } @@ -136,20 +150,58 @@ bool PMSA003ISensor::isActive() return state == State::ACTIVE; } +int32_t PMSA003ISensor::wakeUpTimeMs() +{ +#ifdef PMSA003I_ENABLE_PIN + return PMSA003I_WARMUP_MS; +#endif + return 0; +} + +int32_t PMSA003ISensor::pendingForReadyMs() +{ +#ifdef PMSA003I_ENABLE_PIN + + uint32_t now; + now = getTime(); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sincePmMeasureStarted); + + if (sincePmMeasureStarted < PMSA003I_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return PMSA003I_WARMUP_MS - sincePmMeasureStarted; + } + return 0; + +#endif + return 0; +} + +bool PMSA003ISensor::canSleep() +{ +#ifdef PMSA003I_ENABLE_PIN + return true; +#endif + return false; +} + void PMSA003ISensor::sleep() { #ifdef PMSA003I_ENABLE_PIN digitalWrite(PMSA003I_ENABLE_PIN, LOW); state = State::IDLE; + pmMeasureStarted = 0; #endif } uint32_t PMSA003ISensor::wakeUp() { #ifdef PMSA003I_ENABLE_PIN - LOG_INFO("Waking up PMSA003I"); + LOG_INFO("Waking up %s", sensorName); digitalWrite(PMSA003I_ENABLE_PIN, HIGH); state = State::ACTIVE; + pmMeasureStarted = getTime(); + return PMSA003I_WARMUP_MS; #endif // No need to wait for warmup if already active diff --git a/src/modules/Telemetry/Sensor/PMSA003ISensor.h b/src/modules/Telemetry/Sensor/PMSA003ISensor.h index 09b43d620..3fe96888d 100644 --- a/src/modules/Telemetry/Sensor/PMSA003ISensor.h +++ b/src/modules/Telemetry/Sensor/PMSA003ISensor.h @@ -3,6 +3,7 @@ #if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" #include "TelemetrySensor.h" #define PMSA003I_I2C_CLOCK_SPEED 100000 @@ -19,6 +20,9 @@ class PMSA003ISensor : public TelemetrySensor virtual bool isActive() override; virtual void sleep() override; virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; private: enum class State { IDLE, ACTIVE }; @@ -26,6 +30,7 @@ class PMSA003ISensor : public TelemetrySensor uint16_t computedChecksum = 0; uint16_t receivedChecksum = 0; + uint32_t pmMeasureStarted = 0; uint8_t buffer[PMSA003I_FRAME_LENGTH]{}; TwoWire *_bus{}; diff --git a/src/modules/Telemetry/Sensor/RAK9154Sensor.h b/src/modules/Telemetry/Sensor/RAK9154Sensor.h index c96139f9c..34d0fba73 100644 --- a/src/modules/Telemetry/Sensor/RAK9154Sensor.h +++ b/src/modules/Telemetry/Sensor/RAK9154Sensor.h @@ -18,6 +18,7 @@ class RAK9154Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor public: RAK9154Sensor(); + bool hasSensor() { return true; } // Not an I2C sensor; always available when HAS_RAKPROT is defined virtual int32_t runOnce() override; virtual bool getMetrics(meshtastic_Telemetry *measurement) override; virtual uint16_t getBusVoltageMv() override; diff --git a/src/modules/Telemetry/Sensor/SCD30Sensor.cpp b/src/modules/Telemetry/Sensor/SCD30Sensor.cpp new file mode 100644 index 000000000..0478b6651 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD30Sensor.cpp @@ -0,0 +1,511 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD30Sensor.h" + +#define SCD30_NO_ERROR 0 + +SCD30Sensor::SCD30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD30, "SCD30") {} + +bool SCD30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + scd30.begin(*_bus, _address); + + if (!startMeasurement()) { + LOG_ERROR("%s: Failed to start periodic measurement", sensorName); +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + if (!getASC(ascActive)) { + LOG_WARN("%s: Could not determine ASC state", sensorName); + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (state == SCD30_MEASUREMENT) { + status = 1; + } else { + status = 0; + } + + initI2CSensor(); + + return true; +} + +bool SCD30Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + float co2, temperature, humidity; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + if (scd30.readMeasurementData(co2, temperature, humidity) != SCD30_NO_ERROR) { + LOG_ERROR("SCD30: Failed to read measurement data."); +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (co2 == 0) { + LOG_ERROR("SCD30: Invalid CO₂ reading."); + return false; + } + + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.air_quality_metrics.has_co2_temperature = true; + measurement->variant.air_quality_metrics.has_co2_humidity = true; + measurement->variant.air_quality_metrics.co2 = (uint32_t)co2; + measurement->variant.air_quality_metrics.co2_temperature = temperature; + measurement->variant.air_quality_metrics.co2_humidity = humidity; + + LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum=%.2f", sensorName, (uint32_t)co2, temperature, humidity); + + return true; +} + +bool SCD30Sensor::setMeasurementInterval(uint16_t measInterval) +{ + uint16_t error; + + LOG_INFO("%s: setting measurement interval at %us", sensorName, measInterval); + error = scd30.setMeasurementInterval(measInterval); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set measurement interval. Error code: %u", sensorName, error); + return false; + } + + // Restart measuring so we don't need to wait the current interval to finish + // (useful when you come from very long intervals) + scd30.stopPeriodicMeasurement(); + scd30.startPeriodicMeasurement(0); + + getMeasurementInterval(measurementInterval); + return true; +} + +bool SCD30Sensor::getMeasurementInterval(uint16_t &measInterval) +{ + uint16_t error; + + LOG_INFO("%s: getting measurement interval", sensorName); + error = scd30.getMeasurementInterval(measInterval); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to get measurement interval. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: measurement interval is %us", sensorName, measInterval); + + return true; +} + +/** + * @brief Start measurement mode + * @note This function should not change the clock + */ +bool SCD30Sensor::startMeasurement() +{ + uint16_t error; + + if (state == SCD30_MEASUREMENT) { + LOG_DEBUG("%s: Already in measurement mode", sensorName); + return true; + } + + error = scd30.startPeriodicMeasurement(0); + + if (error == SCD30_NO_ERROR) { + LOG_INFO("%s: Started measurement mode", sensorName); + + state = SCD30_MEASUREMENT; + return true; + } else { + LOG_ERROR("%s: Couldn't start measurement mode", sensorName); + return false; + } +} + +/** + * @brief Stop measurement mode + * @note This function should not change the clock + */ +bool SCD30Sensor::stopMeasurement() +{ + uint16_t error; + + error = scd30.stopPeriodicMeasurement(); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to stop measurement", sensorName); + return false; + } + + state = SCD30_IDLE; + return true; +} + +bool SCD30Sensor::performFRC(uint16_t targetCO2) +{ + uint16_t error; + + LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); + + LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); + error = scd30.forceRecalibration((uint16_t)targetCO2); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); + return false; + } + + LOG_INFO("%s: FRC Correction successful.", sensorName); + + return true; +} + +bool SCD30Sensor::setASC(bool ascEnabled) +{ + uint16_t error; + + LOG_INFO("%s: %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); + + error = scd30.activateAutoCalibration((uint16_t)ascEnabled); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); + return false; + } + + return true; +} + +bool SCD30Sensor::getASC(uint16_t &_ascActive) +{ + uint16_t error; + // LOG_INFO("%s: Getting ASC", sensorName); + + error = scd30.getAutoCalibrationStatus(_ascActive); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + LOG_INFO("%s: ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); + + return true; +} + +/** + * @brief Set the temperature reference. Unit ℃. + * + * The on-board RH/T sensor is influenced by thermal self-heating of SCD30 + * and other electrical components. Design-in alters the thermal properties + * of SCD30 such that temperature and humidity offsets may occur when + * operating the sensor in end-customer devices. Compensation of those + * effects is achievable by writing the temperature offset found in + * continuous operation of the device into the sensor. Temperature offset + * value is saved in non-volatile memory. The last set value will be used + * for temperature offset compensation after repowering. + * + * @param[in] tempReference + * @note this function is certainly confusing and it's not recommended + */ +bool SCD30Sensor::setTemperature(float tempReference) +{ + uint16_t error; + uint16_t updatedTempOffset; + float tempOffset; + uint16_t _tempOffset; + float co2; + float temperature; + float humidity; + + if (tempReference == 100) { + // Requesting the value of 100 will restore the temperature offset + LOG_INFO("%s: Setting reference temperature at 0degC", sensorName); + _tempOffset = 0; + } else { + + LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); + + error = scd30.readMeasurementData(co2, temperature, humidity); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); + + tempOffset = (temperature - tempReference); + if (tempOffset < 0) { + LOG_ERROR("%s temperature offset is only positive", sensorName); + return false; + } + + tempOffset *= 100; + _tempOffset = static_cast(tempOffset); + } + + LOG_INFO("%s: Setting temperature offset: %u (*100)", sensorName, _tempOffset); + + error = scd30.setTemperatureOffset(_tempOffset); + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); + return false; + } + + scd30.getTemperatureOffset(updatedTempOffset); + LOG_INFO("%s: Updated sensor temperature offset: %u (*100)", sensorName, updatedTempOffset); + + return true; +} + +bool SCD30Sensor::setAltitude(uint16_t altitude) +{ + uint16_t error; + + LOG_INFO("%s: setting altitude at %um", sensorName, altitude); + + error = scd30.setAltitudeCompensation(altitude); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + uint16_t newAltitude; + getAltitude(newAltitude); + + return true; +} + +bool SCD30Sensor::getAltitude(uint16_t &altitude) +{ + uint16_t error; + // LOG_INFO("%s: Getting altitude", sensorName); + + error = scd30.getAltitudeCompensation(altitude); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); + + return true; +} + +bool SCD30Sensor::softReset() +{ + uint16_t error; + + LOG_INFO("%s: Requesting soft reset", sensorName); + + error = scd30.softReset(); + + if (error != SCD30_NO_ERROR) { + LOG_ERROR("%s: Unable to do soft reset. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: soft reset successful", sensorName); + + return true; +} + +/** + * @brief Check if sensor is in measurement mode + */ +bool SCD30Sensor::isActive() +{ + return state == SCD30_MEASUREMENT; +} + +/** + * @brief Start measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +uint32_t SCD30Sensor::wakeUp() +{ + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return 0; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + startMeasurement(); + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return 0; +} + +/** + * @brief Stop measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +void SCD30Sensor::sleep() +{ +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + stopMeasurement(); + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif +} + +bool SCD30Sensor::canSleep() +{ + return false; +} + +int32_t SCD30Sensor::wakeUpTimeMs() +{ + return 0; +} + +int32_t SCD30Sensor::pendingForReadyMs() +{ + return 0; +} + +AdminMessageHandleResult SCD30Sensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + +#ifdef SCD30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return AdminMessageHandleResult::NOT_HANDLED; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD30_I2C_CLOCK_SPEED */ + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scd30_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scd30_config.has_soft_reset) { + LOG_DEBUG("%s: Requested soft reset", sensorName); + this->softReset(); + } else { + + if (request->sensor_config.scd30_config.has_set_asc) { + this->setASC(request->sensor_config.scd30_config.set_asc); + if (request->sensor_config.scd30_config.set_asc == false) { + LOG_DEBUG("%s: Request for FRC", sensorName); + if (request->sensor_config.scd30_config.has_set_target_co2_conc) { + this->performFRC(request->sensor_config.scd30_config.set_target_co2_conc); + } else { + // FRC requested but no target CO2 provided + LOG_ERROR("%s: target CO2 not provided", sensorName); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + // Check for temperature offset + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd30_config.has_set_temperature) { + this->setTemperature(request->sensor_config.scd30_config.set_temperature); + } + + // Check for altitude + if (request->sensor_config.scd30_config.has_set_altitude) { + this->setAltitude(request->sensor_config.scd30_config.set_altitude); + } + + // Check for set measuremen interval + if (request->sensor_config.scd30_config.has_set_measurement_interval) { + this->setMeasurementInterval(request->sensor_config.scd30_config.set_measurement_interval); + } + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + +#if defined(SCD30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return result; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/SCD30Sensor.h b/src/modules/Telemetry/Sensor/SCD30Sensor.h new file mode 100644 index 000000000..6e03e2dda --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD30Sensor.h @@ -0,0 +1,53 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +#define SCD30_I2C_CLOCK_SPEED 100000 + +class SCD30Sensor : public TelemetrySensor +{ + private: + SensirionI2cScd30 scd30; + TwoWire *_bus{}; + uint8_t _address{}; + + bool performFRC(uint16_t targetCO2); + bool setASC(bool ascEnabled); + bool getASC(uint16_t &ascEnabled); + bool setTemperature(float tempReference); + bool getAltitude(uint16_t &altitude); + bool setAltitude(uint16_t altitude); + bool softReset(); // + bool setMeasurementInterval(uint16_t measInterval); + bool getMeasurementInterval(uint16_t &measInterval); + bool startMeasurement(); + bool stopMeasurement(); + + // Parameters + uint16_t ascActive = 1; + uint16_t measurementInterval = 2; + + public: + SCD30Sensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + enum SCD30State { SCD30_OFF, SCD30_IDLE, SCD30_MEASUREMENT }; + SCD30State state = SCD30_OFF; + + virtual bool isActive() override; + + virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.cpp b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp new file mode 100644 index 000000000..c6ab7bb04 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.cpp @@ -0,0 +1,917 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SCD4XSensor.h" + +#define SCD4X_NO_ERROR 0 + +SCD4XSensor::SCD4XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SCD4X, "SCD4X") {} + +bool SCD4XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + scd4x.begin(*_bus, _address); + + // From SCD4X library + delay(30); + + // Stop periodic measurement + if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + // Get sensor variant + scd4x.getSensorVariant(sensorVariant); + + if (sensorVariant == SCD4X_SENSOR_VARIANT_SCD41) { + LOG_INFO("%s: Found SCD41", sensorName); + if (!powerUp()) { + LOG_ERROR("%s: Error trying to execute powerUp()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + // Start measurement in selected power mode (low power by default) + if (!startMeasurement()) { + LOG_ERROR("%s: Couldn't start measurement", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (state == SCD4X_MEASUREMENT) { + status = 1; + } else { + status = 0; + } + + initI2CSensor(); + + return true; +} + +bool SCD4XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + + if (state != SCD4X_MEASUREMENT) { + LOG_ERROR("%s: Not in measurement mode", sensorName); + return false; + } + + uint16_t co2, error; + float temperature, humidity; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + bool dataReady; + error = scd4x.getDataReadyStatus(dataReady); + if (error != SCD4X_NO_ERROR || !dataReady) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + LOG_ERROR("SCD4X: Data is not ready"); + return false; + } + + error = scd4x.readMeasurement(co2, temperature, humidity); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + LOG_DEBUG("Got %s readings: co2=%u, co2_temp=%.2f, co2_hum%.2f", sensorName, co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_DEBUG("%s: Error while getting measurements: %u", sensorName, error); + if (co2 == 0) { + LOG_ERROR("%s: Skipping invalid measurement.", sensorName); + } + return false; + } else { + measurement->variant.air_quality_metrics.has_co2_temperature = true; + measurement->variant.air_quality_metrics.has_co2_humidity = true; + measurement->variant.air_quality_metrics.has_co2 = true; + measurement->variant.air_quality_metrics.co2_temperature = temperature; + measurement->variant.air_quality_metrics.co2_humidity = humidity; + measurement->variant.air_quality_metrics.co2 = co2; + return true; + } +} + +/** + * @brief Perform a forced recalibration (FRC) of the CO₂ concentration. + * + * From Sensirion SCD4X I2C Library + * + * 1. Operate the SCD4x in the operation mode later used for normal sensor + * operation (e.g. periodic measurement) for at least 3 minutes in an + * environment with a homogenous and constant CO2 concentration. The sensor + * must be operated at the voltage desired for the application when + * performing the FRC sequence. 2. Issue the stop_periodic_measurement + * command. 3. Issue the perform_forced_recalibration command. + * @note This function should not change the clock + */ +bool SCD4XSensor::performFRC(uint32_t targetCO2) +{ + uint16_t error, frcCorr; + + LOG_INFO("%s: Issuing FRC. Ensure device has been working at least 3 minutes in stable target environment", sensorName); + + if (!stopMeasurement()) { + return false; + } + + LOG_INFO("%s: Target CO2: %u ppm", sensorName, targetCO2); + error = scd4x.performForcedRecalibration((uint16_t)targetCO2, frcCorr); + + // SCD4X Sensirion datasheet + delay(400); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to perform forced recalibration.", sensorName); + return false; + } + + if (frcCorr == 0xFFFF) { + LOG_ERROR("%s: Error while performing forced recalibration.", sensorName); + return false; + } + + LOG_INFO("%s: FRC Correction successful. Correction output: %u", sensorName, (uint16_t)(frcCorr - 0x8000)); + + return true; +} + +/** + * @brief Start measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::startMeasurement() +{ + uint16_t error; + + if (state == SCD4X_MEASUREMENT) { + LOG_DEBUG("%s: Already in measurement mode", sensorName); + return true; + } + + if (lowPower) { + error = scd4x.startLowPowerPeriodicMeasurement(); + } else { + error = scd4x.startPeriodicMeasurement(); + } + + if (error == SCD4X_NO_ERROR) { + LOG_INFO("%s: Started measurement mode", sensorName); + if (lowPower) { + LOG_INFO("%s: Low power mode", sensorName); + } else { + LOG_INFO("%s: Normal power mode", sensorName); + } + + state = SCD4X_MEASUREMENT; + return true; + } else { + LOG_ERROR("%s: Unable to start measurement mode", sensorName); + return false; + } +} + +/** + * @brief Stop measurement mode + * @note This function should not change the clock + */ +bool SCD4XSensor::stopMeasurement() +{ + uint16_t error; + + error = scd4x.stopPeriodicMeasurement(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to stop measurement.", sensorName); + return false; + } + + state = SCD4X_IDLE; + co2MeasureStarted = 0; + return true; +} + +/** + * @brief Set power mode + * Pass true to set low power mode + * @note This function should not change the clock + */ +bool SCD4XSensor::setPowerMode(bool _lowPower) +{ + lowPower = _lowPower; + + if (!stopMeasurement()) { + return false; + } + + if (lowPower) { + LOG_DEBUG("%s: Set low power mode", sensorName); + } else { + LOG_DEBUG("%s: Set normal power mode", sensorName); + } + + return true; +} + +/** + * @brief Check the current mode (ASC or FRC) + * From Sensirion SCD4X I2C Library + * @note This function should not change the clock + */ +bool SCD4XSensor::getASC(uint16_t &_ascActive) +{ + uint16_t error; + LOG_INFO("%s: Getting ASC", sensorName); + + if (!stopMeasurement()) { + return false; + } + error = scd4x.getAutomaticSelfCalibrationEnabled(_ascActive); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + LOG_INFO("%s ASC is %s", sensorName, _ascActive ? "enabled" : "disabled"); + + return true; +} + +/** + * @brief Enable or disable automatic self calibration (ASC). + * + * From Sensirion SCD4X I2C Library + * + * Sets the current state (enabled / disabled) of the ASC. By default, ASC + * is enabled. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASC(bool ascEnabled) +{ + uint16_t error; + + LOG_INFO("%s %s ASC", sensorName, ascEnabled ? "Enabling" : "Disabling"); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.setAutomaticSelfCalibrationEnabled((uint16_t)ascEnabled); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); + return false; + } + + if (!getASC(ascActive)) { + LOG_ERROR("%s: Unable to check if ASC is enabled", sensorName); + return false; + } + + return true; +} + +/** + * @brief Set the value of ASC baseline target in ppm. + * + * From Sensirion SCD4X I2C Library. + * + * Sets the value of the ASC baseline target, i.e. the CO₂ concentration in + * ppm which the ASC algorithm will assume as lower-bound background to + * which the SCD4x is exposed to regularly within one ASC period of + * operation. To save the setting to the EEPROM, the persist_settings + * command must be issued subsequently. The factory default value is 400 + * ppm. + * @note This function should not change the clock + */ +bool SCD4XSensor::setASCBaseline(uint32_t targetCO2) +{ + // Available in library, but not described in datasheet. + uint16_t error; + LOG_INFO("%s: Setting ASC baseline to: %u", sensorName, targetCO2); + + getASC(ascActive); + if (!ascActive) { + LOG_ERROR("%s: Can't set ASC baseline. ASC is not active", sensorName); + return false; + } + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.setAutomaticSelfCalibrationTarget((uint16_t)targetCO2); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to send command.", sensorName); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent.", sensorName); + return false; + } + + LOG_INFO("%s: Setting ASC baseline successful", sensorName); + + return true; +} + +/** + * @brief Set the temperature compensation reference. + * + * From Sensirion SCD4X I2C Library. + * + * Setting the temperature offset of the SCD4x inside the customer device + * allows the user to optimize the RH and T output signal. + * By default, the temperature offset is set to 4 °C. To save + * the setting to the EEPROM, the persist_settings command may be issued. + * Equation (1) details how the characteristic temperature offset can be + * calculated using the current temperature output of the sensor (TSCD4x), a + * reference temperature value (TReference), and the previous temperature + * offset (Toffset_pervious) obtained using the get_temperature_offset_raw + * command: + * + * Toffset_actual = TSCD4x - TReference + Toffset_pervious. + * + * Recommended temperature offset values are between 0 °C and 20 °C. The + * temperature offset does not impact the accuracy of the CO2 output. + * @note This function should not change the clock + */ +bool SCD4XSensor::setTemperature(float tempReference) +{ + uint16_t error; + float prevTempOffset; + float updatedTempOffset; + float tempOffset; + bool dataReady; + uint16_t co2; + float temperature; + float humidity; + + LOG_INFO("%s: Setting reference temperature at: %.2f", sensorName, tempReference); + + error = scd4x.getDataReadyStatus(dataReady); + if (error != SCD4X_NO_ERROR || !dataReady) { + LOG_ERROR("%s: Data is not ready", sensorName); + return false; + } + + error = scd4x.readMeasurement(co2, temperature, humidity); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to read current temperature. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Current sensor temperature: %.2f", sensorName, temperature); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.getTemperatureOffset(prevTempOffset); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get temperature offset. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Current sensor temperature offset: %.2f", sensorName, prevTempOffset); + + tempOffset = temperature - tempReference + prevTempOffset; + + LOG_INFO("%s: Setting temperature offset: %.2f", sensorName, tempOffset); + error = scd4x.setTemperatureOffset(tempOffset); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set temperature offset. Error code: %u", sensorName, error); + return false; + } + + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + return false; + } + + scd4x.getTemperatureOffset(updatedTempOffset); + LOG_INFO("%s: Updated sensor temperature offset: %.2f", sensorName, updatedTempOffset); + + return true; +} + +/** + * @brief Get the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAltitude(uint16_t &altitude) +{ + uint16_t error; + LOG_INFO("%s: Requesting sensor altitude", sensorName); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.getSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor altitude: %u", sensorName, altitude); + + return true; +} + +/** + * @brief Get the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * Gets the ambient pressure in Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::getAmbientPressure(uint32_t &ambientPressure) +{ + uint16_t error; + LOG_INFO("%s: Requesting sensor ambient pressure", sensorName); + + error = scd4x.getAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to get altitude. Error code: %u", sensorName, error); + return false; + } + LOG_INFO("%s: Sensor ambient pressure: %u", sensorName, ambientPressure); + + return true; +} + +/** + * @brief Set the sensor altitude. + * + * From Sensirion SCD4X I2C Library. + * + * Altitude in meters above sea level can be set after device installation. + * Valid value between 0 and 3000m. This overrides pressure offset. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAltitude(uint32_t altitude) +{ + uint16_t error; + + if (!stopMeasurement()) { + return false; + } + LOG_INFO("%s: setting altitude at %um", sensorName, altitude); + + error = scd4x.setSensorAltitude(altitude); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + // NOTE: this gives an error if issued. Sensirion's library + // doesn't indicate it's needed. + // error = scd4x.persistSettings(); + // if (error != SCD4X_NO_ERROR) { + // LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + // return false; + // } + + LOG_INFO("%s: altitude set", sensorName); + + return true; +} + +/** + * @brief Set the ambient pressure around the sensor. + * + * From Sensirion SCD4X I2C Library. + * + * The set_ambient_pressure command can be sent during periodic measurements + * to enable continuous pressure compensation. Note that setting an ambient + * pressure overrides any pressure compensation based on a previously set + * sensor altitude. Use of this command is highly recommended for + * applications experiencing significant ambient pressure changes to ensure + * sensor accuracy. Valid input values are between 70000 - 120000 Pa. The + * default value is 101300 Pa. + * @note This function should not change the clock + */ +bool SCD4XSensor::setAmbientPressure(uint32_t ambientPressure) +{ + uint16_t error; + + LOG_INFO("%s: setting ambient pressure at %u Pa", sensorName, ambientPressure); + + error = scd4x.setAmbientPressure(ambientPressure); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to set altitude. Error code: %u", sensorName, error); + return false; + } + + // Sensirion doesn't indicate if this is necessary. We send it anyway + error = scd4x.persistSettings(); + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to make settings persistent. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: ambient pressure set set", sensorName); + + return true; +} + +/** + * @brief Perform factory reset to erase the settings stored in the EEPROM. + * + * From Sensirion SCD4X I2C Library. + * + * The perform_factory_reset command resets all configuration settings + * stored in the EEPROM and erases the FRC and ASC algorithm history. + * @note This function should not change the clock + */ +bool SCD4XSensor::factoryReset() +{ + uint16_t error; + + LOG_INFO("%s: Requesting factory reset", sensorName); + + if (!stopMeasurement()) { + return false; + } + + error = scd4x.performFactoryReset(); + + if (error != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Unable to do factory reset. Error code: %u", sensorName, error); + return false; + } + + LOG_INFO("%s: Factory reset successful", sensorName); + + return true; +} + +/** + * @brief Put the sensor into sleep mode from idle mode. + * + * From Sensirion SCD4X I2C Library. + * + * Put the sensor from idle to sleep to reduce power consumption. Can be + * used to power down when operating the sensor in power-cycled single shot + * mode. + * @note This command is only available in idle mode. Only for SCD41. + */ +bool SCD4XSensor::powerDown() +{ + LOG_INFO("%s: Trying to send sensor to sleep", sensorName); + + if (sensorVariant != SCD4X_SENSOR_VARIANT_SCD41) { + LOG_WARN("SCD4X: Can't send sensor to sleep. Incorrect variant. Ignoring"); + return true; + } + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + if (!stopMeasurement()) { +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + if (scd4x.powerDown() != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Error trying to execute sleep()", sensorName); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + state = SCD4X_OFF; + return true; +} + +/** + * @brief Wake up sensor from sleep mode to idle mode (powerUp) + * + * From Sensirion SCD4X I2C Library. + * + * Wake up the sensor from sleep mode into idle mode. Note that the SCD4x + * does not acknowledge the wake_up command. The sensor's idle state after + * wake up can be verified by reading out the serial number. + * @note This command is only available for SCD41. + * @note This function can't change clock (used in init) + */ +bool SCD4XSensor::powerUp() +{ + LOG_INFO("%s: Waking up", sensorName); + + if (scd4x.wakeUp() != SCD4X_NO_ERROR) { + LOG_ERROR("%s: Error trying to execute wakeUp()", sensorName); + return false; + } + + state = SCD4X_IDLE; + + return true; +} + +/** + * @brief Check if sensor is in measurement mode + */ +bool SCD4XSensor::isActive() +{ + return state == SCD4X_MEASUREMENT; +} + +/** + * @brief Start measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +uint32_t SCD4XSensor::wakeUp() +{ + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return 0; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + if (startMeasurement()) { + co2MeasureStarted = getTime(); +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return SCD4X_WARMUP_MS; + } + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return 0; +} + +/** + * @brief Stop measurement mode + * @note Not used in admin comands, getMetrics or init, can change clock. + */ +void SCD4XSensor::sleep() +{ +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + stopMeasurement(); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif +} + +/** + * @brief Can sleep function + * + * Power consumption is very low on lowPower mode, modify this function if + * you still want to override this behaviour. Otherwise, sleep is disabled + * routinely in low power mode + */ +bool SCD4XSensor::canSleep() +{ + return lowPower ? false : true; +} + +int32_t SCD4XSensor::wakeUpTimeMs() +{ + return SCD4X_WARMUP_MS; +} + +int32_t SCD4XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceCO2MeasureStarted = (now - co2MeasureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceCO2MeasureStarted); + + if (sinceCO2MeasureStarted < SCD4X_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SCD4X_WARMUP_MS - sinceCO2MeasureStarted; + } + return 0; +} + +AdminMessageHandleResult SCD4XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + +#ifdef SCD4X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SCD4X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return AdminMessageHandleResult::NOT_HANDLED; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SCD4X_I2C_CLOCK_SPEED */ + + // TODO: potentially add selftest command? + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + // Check for ASC-FRC request first + if (!request->sensor_config.has_scd4x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + if (request->sensor_config.scd4x_config.has_factory_reset) { + LOG_DEBUG("%s: Requested factory reset", sensorName); + if (!this->factoryReset()) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + if (request->sensor_config.scd4x_config.has_set_asc) { + getASC(ascActive); + bool currentASC = ascActive; + if (request->sensor_config.scd4x_config.set_asc == false) { + LOG_DEBUG("%s: Request for FRC", sensorName); + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { + if (!this->performFRC(request->sensor_config.scd4x_config.set_target_co2_conc)) { + result = AdminMessageHandleResult::NOT_HANDLED; + // Set it back to ASC if failed + setASC(currentASC); + break; + }; + } else { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + // FRC requested but no target CO2 provided + LOG_ERROR("%s: target CO2 not provided", sensorName); + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else { + LOG_DEBUG("%s: Request for ASC", sensorName); + if (this->setASC(request->sensor_config.scd4x_config.set_asc)) { + if (request->sensor_config.scd4x_config.has_set_target_co2_conc) { + LOG_DEBUG("%s: Request has target CO2", sensorName); + this->setASCBaseline(request->sensor_config.scd4x_config.set_target_co2_conc); + // NOTE - in this situation, if we set ASC, but baseline set fails, we stay on ASC + } else { + LOG_DEBUG("%s: Request doesn't have target CO2", sensorName); + } + } else { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + // Check for temperature offset + // NOTE: this requires to have a sensor working on stable environment + // And to make it between readings + if (request->sensor_config.scd4x_config.has_set_temperature) { + if (!this->setTemperature(request->sensor_config.scd4x_config.set_temperature)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + + // Check for altitude or pressure offset + if (request->sensor_config.scd4x_config.has_set_altitude) { + if (!this->setAltitude(request->sensor_config.scd4x_config.set_altitude)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } else if (request->sensor_config.scd4x_config.has_set_ambient_pressure) { + if (!this->setAmbientPressure(request->sensor_config.scd4x_config.set_ambient_pressure)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + + // Check for low power mode + // NOTE: to switch from one mode to another do: + // setPowerMode -> startMeasurement + if (request->sensor_config.scd4x_config.has_set_power_mode) { + if (!this->setPowerMode(request->sensor_config.scd4x_config.set_power_mode)) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + } + } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + // Start measurement mode + this->startMeasurement(); + +#if defined(SCD4X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return result; +} + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SCD4XSensor.h b/src/modules/Telemetry/Sensor/SCD4XSensor.h new file mode 100644 index 000000000..1ed86a183 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SCD4XSensor.h @@ -0,0 +1,63 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +// Max speed 400kHz +#define SCD4X_I2C_CLOCK_SPEED 100000 +#define SCD4X_WARMUP_MS 5000 + +class SCD4XSensor : public TelemetrySensor +{ + private: + SensirionI2cScd4x scd4x; + TwoWire *_bus{}; + uint8_t _address{}; + + bool performFRC(uint32_t targetCO2); + bool setASCBaseline(uint32_t targetCO2); + bool getASC(uint16_t &ascEnabled); + bool setASC(bool ascEnabled); + bool setTemperature(float tempReference); + bool getAltitude(uint16_t &altitude); + bool setAltitude(uint32_t altitude); + bool getAmbientPressure(uint32_t &ambientPressure); + bool setAmbientPressure(uint32_t ambientPressure); + bool factoryReset(); + bool setPowerMode(bool _lowPower); + bool startMeasurement(); + bool stopMeasurement(); + + uint16_t ascActive = 1; + // low power measurement mode (on sensirion side). Disables sleep mode + // Improvement and testing needed for timings + bool lowPower = true; + uint32_t co2MeasureStarted = 0; + + public: + SCD4XSensor(); + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + + enum SCD4XState { SCD4X_OFF, SCD4X_IDLE, SCD4X_MEASUREMENT }; + SCD4XState state = SCD4X_OFF; + SCD4xSensorVariant sensorVariant{}; + + virtual bool isActive() override; + + virtual void sleep() override; // Stops measurement (measurement -> idle) + virtual uint32_t wakeUp() override; // Starts measurement (idle -> measurement) + bool powerDown(); // Powers down sensor (idle -> power-off) + bool powerUp(); // Powers the sensor (power-off -> idle) + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.cpp b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp new file mode 100644 index 000000000..b0f0f9071 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.cpp @@ -0,0 +1,969 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "FSCommon.h" +#include "SEN5XSensor.h" +#include "SPILock.h" +#include "SafeFile.h" +#include "TelemetrySensor.h" +#include // FLT_MAX +#include +#include + +SEN5XSensor::SEN5XSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SEN5X, "SEN5X") {} + +bool SEN5XSensor::getVersion() +{ + if (!sendCommand(SEN5X_GET_FIRMWARE_VERSION)) { + LOG_ERROR("SEN5X: Error sending version command"); + return false; + } + delay(20); // From Sensirion Datasheet + + uint8_t versionBuffer[12]; + size_t charNumber = readBuffer(&versionBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting data ready flag value"); + return false; + } + + firmwareVer = versionBuffer[0] + (versionBuffer[1] / 10); + hardwareVer = versionBuffer[3] + (versionBuffer[4] / 10); + protocolVer = versionBuffer[5] + (versionBuffer[6] / 10); + + LOG_INFO("SEN5X Firmware Version: %0.2f", firmwareVer); + LOG_INFO("SEN5X Hardware Version: %0.2f", hardwareVer); + LOG_INFO("SEN5X Protocol Version: %0.2f", protocolVer); + + return true; +} + +bool SEN5XSensor::findModel() +{ + if (!sendCommand(SEN5X_GET_PRODUCT_NAME)) { + LOG_ERROR("SEN5X: Error asking for product name"); + return false; + } + delay(50); // From Sensirion Datasheet + + const uint8_t nameSize = 48; + uint8_t name[nameSize]; + size_t charNumber = readBuffer(&name[0], nameSize); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device name"); + return false; + } + + // We only check the last character that defines the model SEN5X + switch (name[4]) { + case 48: + model = SEN50; + LOG_INFO("SEN5X: found sensor model SEN50"); + break; + case 52: + model = SEN54; + LOG_INFO("SEN5X: found sensor model SEN54"); + break; + case 53: + model = SEN55; + LOG_INFO("SEN5X: found sensor model SEN55"); + break; + } + + return true; +} + +bool SEN5XSensor::sendCommand(uint16_t command) +{ + uint8_t nothing; + return sendCommand(command, ¬hing, 0); +} + +bool SEN5XSensor::sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber) +{ + // At least we need two bytes for the command + uint8_t bufferSize = 2; + + // Add space for CRC bytes (one every two bytes) + if (byteNumber > 0) + bufferSize += byteNumber + (byteNumber / 2); + + uint8_t toSend[bufferSize]; + uint8_t i = 0; + toSend[i++] = static_cast((command & 0xFF00) >> 8); + toSend[i++] = static_cast((command & 0x00FF) >> 0); + + // Prepare buffer with CRC every third byte + uint8_t bi = 0; + if (byteNumber > 0) { + while (bi < byteNumber) { + toSend[i++] = buffer[bi++]; + toSend[i++] = buffer[bi++]; + uint8_t calcCRC = sen5xCRC(&buffer[bi - 2]); + toSend[i++] = calcCRC; + } + } + +#ifdef SEN5X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ + + // Transmit the data + // LOG_DEBUG("Beginning connection to SEN5X: 0x%x. Size: %u", address, bufferSize); + // Note: this delay is necessary to allow for long-buffers + delay(20); + _bus->beginTransmission(_address); + size_t writtenBytes = _bus->write(toSend, bufferSize); + uint8_t i2c_error = _bus->endTransmission(); + +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + if (writtenBytes != bufferSize) { + LOG_ERROR("SEN5X: Error writting on I2C bus"); + return false; + } + + if (i2c_error != 0) { + LOG_ERROR("SEN5X: Error on I2C communication: %x", i2c_error); + return false; + } + return true; +} + +uint8_t SEN5XSensor::readBuffer(uint8_t *buffer, uint8_t byteNumber) +{ +#ifdef SEN5X_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SEN5X_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SEN5X_I2C_CLOCK_SPEED */ + + size_t readBytes = _bus->requestFrom(_address, byteNumber); + if (readBytes != byteNumber) { + LOG_ERROR("SEN5X: Error reading I2C bus"); + return 0; + } + + uint8_t i = 0; + uint8_t receivedBytes = 0; + while (readBytes > 0) { + buffer[i++] = _bus->read(); // Just as a reminder: i++ returns i and after that increments. + buffer[i++] = _bus->read(); + uint8_t recvCRC = _bus->read(); + uint8_t calcCRC = sen5xCRC(&buffer[i - 2]); + if (recvCRC != calcCRC) { + LOG_ERROR("SEN5X: Checksum error while receiving msg"); + return 0; + } + readBytes -= 3; + receivedBytes += 2; + } +#if defined(SEN5X_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + return receivedBytes; +} + +uint8_t SEN5XSensor::sen5xCRC(const uint8_t *buffer) +{ + // This code is based on Sensirion's own implementation + // https://github.com/Sensirion/arduino-core/blob/41fd02cacf307ec4945955c58ae495e56809b96c/src/SensirionCrc.cpp + uint8_t crc = 0xff; + + for (uint8_t i = 0; i < 2; i++) { + + crc ^= buffer[i]; + + for (uint8_t bit = 8; bit > 0; bit--) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + return crc; +} + +void SEN5XSensor::sleep() +{ + idle(true); +} + +bool SEN5XSensor::idle(bool checkState) +{ + // From the datasheet: + // By default, the VOC algorithm resets its state to initial + // values each time a measurement is started, + // even if the measurement was stopped only for a short + // time. So, the VOC index output value needs a long time + // until it is stable again. This can be avoided by + // restoring the previously memorized algorithm state before + // starting the measure mode + + if (checkState) { + // If the stabilisation period is not passed for SEN54 or SEN55, don't go to idle + if (model != SEN50) { + // Get VOC state before going to idle mode + vocValid = false; + if (vocStateFromSensor()) { + vocValid = vocStateValid(); + // Check if we have time, and store it + uint32_t now; // If time is RTCQualityNone, it will return zero + now = getValidTime(RTCQuality::RTCQualityDevice); + // Check if state is valid (non-zero) + if (now) { + vocTime = now; + } + } + + if (!(vocStateStable() && vocValid)) { + LOG_INFO("%s: Not stopping measurement, vocState is not stable yet!", sensorName); + return true; + } + } + // Save state and prefs (on all models) + saveState(); + } + + if (!oneShotMode) { + LOG_INFO("%s: Not stopping measurement, continuous mode!", sensorName); + return true; + } else { + LOG_INFO("%s: One shot mode enabled", sensorName); + } + + // Switch to low-power based on the model + if (model == SEN50) { + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("%s: Error stopping measurement", sensorName); + return false; + } + state = SEN5X_IDLE; + LOG_INFO("%s: Stop measurement mode", sensorName); + } else { + if (!sendCommand(SEN5X_START_MEASUREMENT_RHT_GAS)) { + LOG_ERROR("%s: Error switching to RHT/Gas measurement", sensorName); + return false; + } + state = SEN5X_RHTGAS_ONLY; + LOG_INFO("%s: Switch to RHT/Gas only measurement mode", sensorName); + } + + delay(200); // From Sensirion Datasheet + pmMeasureStarted = 0; + return true; +} + +bool SEN5XSensor::vocStateRecent(uint32_t now) +{ + if (now) { + uint32_t passed = now - vocTime; // in seconds + + // Check if state is recent, less than 10 minutes (600 seconds) + if (passed < SEN5X_VOC_VALID_TIME && (now > SEN5X_VOC_VALID_DATE)) { + return true; + } + } + return false; +} + +bool SEN5XSensor::vocStateValid() +{ + if (!vocState[0] && !vocState[1] && !vocState[2] && !vocState[3] && !vocState[4] && !vocState[5] && !vocState[6] && + !vocState[7]) { + LOG_DEBUG("%s: VOC state is all 0, invalid", sensorName); + return false; + } else { + LOG_DEBUG("%s: VOC state is valid", sensorName); + return true; + } +} + +bool SEN5XSensor::vocStateToSensor() +{ + if (model == SEN50) { + return true; + } + + if (!vocStateValid()) { + LOG_INFO("SEN5X: VOC state is invalid, not sending"); + return true; + } + + if (!sendCommand(SEN5X_STOP_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error stoping measurement"); + return false; + } + delay(200); // From Sensirion Datasheet + + LOG_DEBUG("SEN5X: Sending VOC state to sensor"); + LOG_DEBUG("[%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], vocState[3], vocState[4], vocState[5], + vocState[6], vocState[7]); + + // Note: send command already takes into account the CRC + // buffer size increment needed + if (!sendCommand(SEN5X_RW_VOCS_STATE, vocState, SEN5X_VOC_STATE_BUFFER_SIZE)) { + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + return true; +} + +bool SEN5XSensor::vocStateFromSensor() +{ + if (model == SEN50) { + return true; + } + + LOG_INFO("SEN5X: Getting VOC state from sensor"); + // Ask VOCs state from the sensor + if (!sendCommand(SEN5X_RW_VOCS_STATE)) { + LOG_ERROR("SEN5X: Error sending VOC's state command'"); + return false; + } + + delay(20); // From Sensirion Datasheet + + // Retrieve the data + // Allocate buffer to account for CRC + size_t receivedNumber = readBuffer(&vocState[0], SEN5X_VOC_STATE_BUFFER_SIZE + (SEN5X_VOC_STATE_BUFFER_SIZE / 2)); + delay(20); // From Sensirion Datasheet + + if (receivedNumber == 0) { + LOG_DEBUG("SEN5X: Error getting VOC's state"); + return false; + } + + // Print the state (if debug is on) + LOG_DEBUG("SEN5X: VOC state retrieved from sensor: [%u, %u, %u, %u, %u, %u, %u, %u]", vocState[0], vocState[1], vocState[2], + vocState[3], vocState[4], vocState[5], vocState[6], vocState[7]); + + return true; +} + +bool SEN5XSensor::loadState() +{ +#ifdef FSCom + spiLock->lock(); + auto file = FSCom.open(sen5XStateFileName, FILE_O_READ); + bool okay = false; + if (file) { + LOG_INFO("%s state read from %s", sensorName, sen5XStateFileName); + pb_istream_t stream = {&readcb, &file, meshtastic_SEN5XState_size}; + + if (!pb_decode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream)); + } else { + lastCleaning = sen5xstate.last_cleaning_time; + lastCleaningValid = sen5xstate.last_cleaning_valid; + oneShotMode = sen5xstate.one_shot_mode; + + if (model != SEN50) { + vocTime = sen5xstate.voc_state_time; + vocValid = sen5xstate.voc_state_valid; + // Unpack state + vocState[7] = (uint8_t)(sen5xstate.voc_state_array >> 56); + vocState[6] = (uint8_t)(sen5xstate.voc_state_array >> 48); + vocState[5] = (uint8_t)(sen5xstate.voc_state_array >> 40); + vocState[4] = (uint8_t)(sen5xstate.voc_state_array >> 32); + vocState[3] = (uint8_t)(sen5xstate.voc_state_array >> 24); + vocState[2] = (uint8_t)(sen5xstate.voc_state_array >> 16); + vocState[1] = (uint8_t)(sen5xstate.voc_state_array >> 8); + vocState[0] = (uint8_t)sen5xstate.voc_state_array; + } + + // LOG_DEBUG("Loaded lastCleaning %u", lastCleaning); + // LOG_DEBUG("Loaded lastCleaningValid %u", lastCleaningValid); + // LOG_DEBUG("Loaded oneShotMode %s", oneShotMode ? "true" : "false"); + // LOG_DEBUG("Loaded vocTime %u", vocTime); + // LOG_DEBUG("Loaded [%u, %u, %u, %u, %u, %u, %u, %u]", + // vocState[7], vocState[6], vocState[5], vocState[4], vocState[3], vocState[2], vocState[1], vocState[0]); + // LOG_DEBUG("Loaded %svalid VOC state", vocValid ? "" : "in"); + + okay = true; + } + file.close(); + } else { + LOG_INFO("No %s state found (File: %s)", sensorName, sen5XStateFileName); + } + spiLock->unlock(); + return okay; +#else + LOG_ERROR("SEN5X: ERROR - Filesystem not implemented"); +#endif +} + +bool SEN5XSensor::saveState() +{ +#ifdef FSCom + auto file = SafeFile(sen5XStateFileName); + + sen5xstate.last_cleaning_time = lastCleaning; + sen5xstate.last_cleaning_valid = lastCleaningValid; + sen5xstate.one_shot_mode = oneShotMode; + + if (model != SEN50) { + sen5xstate.has_voc_state_time = true; + sen5xstate.has_voc_state_valid = true; + sen5xstate.has_voc_state_array = true; + + sen5xstate.voc_state_time = vocTime; + sen5xstate.voc_state_valid = vocValid; + // Unpack state (8 bytes) + sen5xstate.voc_state_array = (((uint64_t)vocState[7]) << 56) | ((uint64_t)vocState[6] << 48) | + ((uint64_t)vocState[5] << 40) | ((uint64_t)vocState[4] << 32) | + ((uint64_t)vocState[3] << 24) | ((uint64_t)vocState[2] << 16) | + ((uint64_t)vocState[1] << 8) | ((uint64_t)vocState[0]); + } + + bool okay = false; + + LOG_INFO("%s: state write to %s", sensorName, sen5XStateFileName); + pb_ostream_t stream = {&writecb, static_cast(&file), meshtastic_SEN5XState_size}; + + if (!pb_encode(&stream, &meshtastic_SEN5XState_msg, &sen5xstate)) { + LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream)); + } else { + okay = true; + } + + okay &= file.close(); + + if (okay) + LOG_INFO("%s: state write to %s successful", sensorName, sen5XStateFileName); + + return okay; +#else + LOG_ERROR("%s: ERROR - Filesystem not implemented", sensorName); +#endif +} + +bool SEN5XSensor::isActive() +{ + return state == SEN5X_MEASUREMENT || state == SEN5X_MEASUREMENT_2; +} + +uint32_t SEN5XSensor::wakeUp() +{ + + LOG_DEBUG("SEN5X: Waking up sensor"); + + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error starting measurement"); + // TODO - what should this return?? Something actually on the default interval? + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + delay(50); // From Sensirion Datasheet + + // TODO - This is currently "problematic" + // If time is updated in between reads, there is no way to + // keep track of how long it has passed + pmMeasureStarted = getTime(); + state = SEN5X_MEASUREMENT; + if (state == SEN5X_MEASUREMENT) + LOG_INFO("SEN5X: Started measurement mode"); + return SEN5X_WARMUP_MS_1; +} + +bool SEN5XSensor::vocStateStable() +{ + uint32_t now; + now = getTime(); + uint32_t sinceFirstMeasureStarted = (now - rhtGasMeasureStarted); + LOG_DEBUG("sinceFirstMeasureStarted: %us", sinceFirstMeasureStarted); + return sinceFirstMeasureStarted > SEN5X_VOC_STATE_WARMUP_S; +} + +bool SEN5XSensor::startCleaning() +{ + // Note: we only should enter here if we have a valid RTC with at least + // RTCQuality::RTCQualityDevice + state = SEN5X_CLEANING; + + // Note that cleaning command can only be run when the sensor is in measurement mode + if (!sendCommand(SEN5X_START_MEASUREMENT)) { + LOG_ERROR("SEN5X: Error starting measurment mode"); + return false; + } + delay(50); // From Sensirion Datasheet + + if (!sendCommand(SEN5X_START_FAN_CLEANING)) { + LOG_ERROR("SEN5X: Error starting fan cleaning"); + return false; + } + delay(20); // From Sensirion Datasheet + + // This message will be always printed so the user knows the device it's not hung + LOG_INFO("SEN5X: Started fan cleaning it will take 10 seconds..."); + + uint16_t started = millis(); + while (millis() - started < 10500) { + delay(500); + } + LOG_INFO("SEN5X: Cleaning done!!"); + + // Save timestamp in flash so we know when a week has passed + uint32_t now; + now = getValidTime(RTCQuality::RTCQualityDevice); + // If time is not RTCQualityNone, it will return non-zero + lastCleaning = now; + lastCleaningValid = true; + saveState(); + + idle(); + return true; +} + +bool SEN5XSensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + state = SEN5X_NOT_DETECTED; + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + + delay(50); // without this there is an error on the deviceReset function + + if (!sendCommand(SEN5X_RESET)) { + LOG_ERROR("SEN5X: Error reseting device"); + return false; + } + delay(200); // From Sensirion Datasheet + + if (!findModel()) { + LOG_ERROR("SEN5X: error finding sensor model"); + return false; + } + + // Check the firmware version + if (!getVersion()) + return false; + if (firmwareVer < 2) { + LOG_ERROR("SEN5X: error firmware is too old and will not work with this implementation"); + return false; + } + delay(200); // From Sensirion Datasheet + + // Detection succeeded + state = SEN5X_IDLE; + status = 1; + + // Load state + loadState(); + + // Check if it is time to do a cleaning + uint32_t now; + int32_t passed = 0; + now = getValidTime(RTCQuality::RTCQualityDevice); + + // If time is not RTCQualityNone, it will return non-zero + if (now) { + if (lastCleaningValid) { + + passed = now - lastCleaning; // in seconds + + if (passed > ONE_WEEK_IN_SECONDS && (now > SEN5X_VOC_VALID_DATE)) { + // If current date greater than 01/01/2018 (validity check) + LOG_INFO("SEN5X: More than a week (%us) since last cleaning in epoch (%us). Trigger, cleaning...", passed, + lastCleaning); + startCleaning(); + } else { + LOG_INFO("SEN5X: Cleaning not needed (%ds passed). Last cleaning date (in epoch): %us", passed, lastCleaning); + } + } else { + // We assume the device has just been updated or it is new, + // so no need to trigger a cleaning. + // Just save the timestamp to do a cleaning one week from now. + // Otherwise, we will never trigger cleaning in some cases + lastCleaning = now; + lastCleaningValid = true; + LOG_INFO("SEN5X: No valid last cleaning date found, saving it now: %us", lastCleaning); + saveState(); + } + + if (model != SEN50) { + if (!vocValid) { + LOG_INFO("SEN5X: No valid VOC's state found"); + } else { + // Check if state is recent + if (vocStateRecent(now)) { + // If current date greater than 01/01/2018 (validity check) + // Send it to the sensor + LOG_INFO("SEN5X: VOC state is valid and recent"); + vocStateToSensor(); + } else { + LOG_INFO("SEN5X: VOC state is too old or date is invalid"); + LOG_DEBUG("SEN5X: vocTime %u, Passed %u, and now %u", vocTime, passed, now); + } + } + } + } else { + // TODO - Should this actually ignore? We could end up never cleaning... + LOG_INFO("SEN5X: Not enough RTCQuality, ignoring saved cleaning and VOC state"); + } + + idle(false); + rhtGasMeasureStarted = now; + + initI2CSensor(); + return true; +} + +bool SEN5XSensor::readValues() +{ + if (!sendCommand(SEN5X_READ_VALUES)) { + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + LOG_DEBUG("SEN5X: Reading PM Values"); + delay(20); // From Sensirion Datasheet + + uint8_t dataBuffer[16]; + size_t receivedNumber = readBuffer(&dataBuffer[0], 24); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting values"); + return false; + } + + // Get the integers + uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + + int16_t int_humidity = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + int16_t int_temperature = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + int16_t int_vocIndex = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + int16_t int_noxIndex = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + + // Convert values based on Sensirion Arduino lib + sen5xmeasurement.pM1p0 = !isnan(uint_pM1p0) ? uint_pM1p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM2p5 = !isnan(uint_pM2p5) ? uint_pM2p5 / 10 : UINT16_MAX; + sen5xmeasurement.pM4p0 = !isnan(uint_pM4p0) ? uint_pM4p0 / 10 : UINT16_MAX; + sen5xmeasurement.pM10p0 = !isnan(uint_pM10p0) ? uint_pM10p0 / 10 : UINT16_MAX; + sen5xmeasurement.humidity = !isnan(int_humidity) ? int_humidity / 100.0f : FLT_MAX; + sen5xmeasurement.temperature = !isnan(int_temperature) ? int_temperature / 200.0f : FLT_MAX; + sen5xmeasurement.vocIndex = !isnan(int_vocIndex) ? int_vocIndex / 10.0f : FLT_MAX; + sen5xmeasurement.noxIndex = !isnan(int_noxIndex) ? int_noxIndex / 10.0f : FLT_MAX; + + LOG_DEBUG("Got %s readings: pM1p0=%u, pM2p5=%u, pM4p0=%u, pM10p0=%u", sensorName, sen5xmeasurement.pM1p0, + sen5xmeasurement.pM2p5, sen5xmeasurement.pM4p0, sen5xmeasurement.pM10p0); + + if (model != SEN50) { + LOG_DEBUG("Got %s readings: humidity=%.2f, temperature=%.2f, vocIndex=%.2f", sensorName, sen5xmeasurement.humidity, + sen5xmeasurement.temperature, sen5xmeasurement.vocIndex); + } + + if (model == SEN55) { + LOG_DEBUG("Got %s readings: noxIndex=%.2f", sensorName, sen5xmeasurement.noxIndex); + } + + return true; +} + +bool SEN5XSensor::readPNValues(bool cumulative) +{ + if (!sendCommand(SEN5X_READ_PM_VALUES)) { + LOG_ERROR("SEN5X: Error sending read command"); + return false; + } + + LOG_DEBUG("SEN5X: Reading PN Values"); + delay(20); // From Sensirion Datasheet + + uint8_t dataBuffer[20]; + size_t receivedNumber = readBuffer(&dataBuffer[0], 30); + if (receivedNumber == 0) { + LOG_ERROR("SEN5X: Error getting PN values"); + return false; + } + + // Get the integers + // uint16_t uint_pM1p0 = static_cast((dataBuffer[0] << 8) | dataBuffer[1]); + // uint16_t uint_pM2p5 = static_cast((dataBuffer[2] << 8) | dataBuffer[3]); + // uint16_t uint_pM4p0 = static_cast((dataBuffer[4] << 8) | dataBuffer[5]); + // uint16_t uint_pM10p0 = static_cast((dataBuffer[6] << 8) | dataBuffer[7]); + uint16_t uint_pN0p5 = static_cast((dataBuffer[8] << 8) | dataBuffer[9]); + uint16_t uint_pN1p0 = static_cast((dataBuffer[10] << 8) | dataBuffer[11]); + uint16_t uint_pN2p5 = static_cast((dataBuffer[12] << 8) | dataBuffer[13]); + uint16_t uint_pN4p0 = static_cast((dataBuffer[14] << 8) | dataBuffer[15]); + uint16_t uint_pN10p0 = static_cast((dataBuffer[16] << 8) | dataBuffer[17]); + uint16_t uint_tSize = static_cast((dataBuffer[18] << 8) | dataBuffer[19]); + + // Convert values based on Sensirion Arduino lib + // Multiply by 100 for converting from #/cm3 to #/0.1l for PN values + sen5xmeasurement.pN0p5 = !isnan(uint_pN0p5) ? uint_pN0p5 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN1p0 = !isnan(uint_pN1p0) ? uint_pN1p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN2p5 = !isnan(uint_pN2p5) ? uint_pN2p5 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN4p0 = !isnan(uint_pN4p0) ? uint_pN4p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.pN10p0 = !isnan(uint_pN10p0) ? uint_pN10p0 / 10 * 100 : UINT32_MAX; + sen5xmeasurement.tSize = !isnan(uint_tSize) ? uint_tSize / 1000.0f : FLT_MAX; + + // Remove accumuluative values: + // https://github.com/fablabbcn/smartcitizen-kit-2x/issues/85 + if (!cumulative) { + sen5xmeasurement.pN10p0 -= sen5xmeasurement.pN4p0; + sen5xmeasurement.pN4p0 -= sen5xmeasurement.pN2p5; + sen5xmeasurement.pN2p5 -= sen5xmeasurement.pN1p0; + sen5xmeasurement.pN1p0 -= sen5xmeasurement.pN0p5; + } + + LOG_DEBUG("Got %s readings: pN0p5=%u, pN1p0=%u, pN2p5=%u, pN4p0=%u, pN10p0=%u, tSize=%.2f", sensorName, + sen5xmeasurement.pN0p5, sen5xmeasurement.pN1p0, sen5xmeasurement.pN2p5, sen5xmeasurement.pN4p0, + sen5xmeasurement.pN10p0, sen5xmeasurement.tSize); + + return true; +} + +uint8_t SEN5XSensor::getMeasurements() +{ + uint32_t now; + now = getTime(); + + // Try to get new data + if (!sendCommand(SEN5X_READ_DATA_READY)) { + LOG_ERROR("SEN5X: Error sending command data ready flag"); + return 2; + } + delay(20); // From Sensirion Datasheet + + uint8_t dataReadyBuffer[3]; + size_t charNumber = readBuffer(&dataReadyBuffer[0], 3); + if (charNumber == 0) { + LOG_ERROR("SEN5X: Error getting device version value"); + return 2; + } + + bool dataReady = dataReadyBuffer[1]; + uint32_t sinceLastDataPollMs = (now - lastDataPoll) * 1000; + // Check if data is ready, and if since last time we requested is less than SEN5X_POLL_INTERVAL + if (!dataReady && (sinceLastDataPollMs > SEN5X_POLL_INTERVAL)) { + LOG_INFO("SEN5X: Data is not ready"); + return 1; + } + + if (!readValues()) { + LOG_ERROR("SEN5X: Error getting readings"); + return 2; + } + + if (!readPNValues(false)) { + LOG_ERROR("SEN5X: Error getting PN readings"); + return 2; + } + + lastDataPoll = now; + + return 0; +} + +int32_t SEN5XSensor::wakeUpTimeMs() +{ + return SEN5X_WARMUP_MS_2; +} + +int32_t SEN5XSensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sincePmMeasureStarted = (now - pmMeasureStarted) * 1000; + LOG_DEBUG("SEN5X: Since measure started: %ums", sincePmMeasureStarted); + + switch (state) { + case SEN5X_MEASUREMENT: { + + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_1) { + LOG_INFO("SEN5X: not enough time passed since starting measurement"); + return SEN5X_WARMUP_MS_1 - sincePmMeasureStarted; + } + + if (!pmMeasureStarted) { + pmMeasureStarted = now; + } + + // Get PN values to check if we are above or below threshold + readPNValues(true); + lastDataPoll = now; + + // If the reading is low (the tyhreshold is in #/cm3) and second warmUp hasn't passed we return to come back later + if ((sen5xmeasurement.pN4p0 / 100) < SEN5X_PN4P0_CONC_THD && sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { + LOG_INFO("SEN5X: Concentration is low, we will ask again in the second warm up period"); + state = SEN5X_MEASUREMENT_2; + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; + } + return 0; + } + case SEN5X_MEASUREMENT_2: { + if (sincePmMeasureStarted < SEN5X_WARMUP_MS_2) { + // Report how many seconds are pending to cover the first warm up period + return SEN5X_WARMUP_MS_2 - sincePmMeasureStarted; + } + return 0; + } + default: { + return -1; + } + } +} + +bool SEN5XSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_INFO("SEN5X: Attempting to get metrics"); + if (!isActive()) { + LOG_INFO("SEN5X: not in measurement mode"); + return false; + } + + uint8_t response; + response = getMeasurements(); + + if (response == 0) { + if (sen5xmeasurement.pM1p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm10_standard = true; + measurement->variant.air_quality_metrics.pm10_standard = sen5xmeasurement.pM1p0; + } + if (sen5xmeasurement.pM2p5 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm25_standard = true; + measurement->variant.air_quality_metrics.pm25_standard = sen5xmeasurement.pM2p5; + } + if (sen5xmeasurement.pM4p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm40_standard = true; + measurement->variant.air_quality_metrics.pm40_standard = sen5xmeasurement.pM4p0; + } + if (sen5xmeasurement.pM10p0 != UINT16_MAX) { + measurement->variant.air_quality_metrics.has_pm100_standard = true; + measurement->variant.air_quality_metrics.pm100_standard = sen5xmeasurement.pM10p0; + } + if (sen5xmeasurement.pN0p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_05um = true; + measurement->variant.air_quality_metrics.particles_05um = sen5xmeasurement.pN0p5; + } + if (sen5xmeasurement.pN1p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_10um = true; + measurement->variant.air_quality_metrics.particles_10um = sen5xmeasurement.pN1p0; + } + if (sen5xmeasurement.pN2p5 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_25um = true; + measurement->variant.air_quality_metrics.particles_25um = sen5xmeasurement.pN2p5; + } + if (sen5xmeasurement.pN4p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_40um = true; + measurement->variant.air_quality_metrics.particles_40um = sen5xmeasurement.pN4p0; + } + if (sen5xmeasurement.pN10p0 != UINT32_MAX) { + measurement->variant.air_quality_metrics.has_particles_100um = true; + measurement->variant.air_quality_metrics.particles_100um = sen5xmeasurement.pN10p0; + } + if (sen5xmeasurement.tSize != FLT_MAX) { + measurement->variant.air_quality_metrics.has_particles_tps = true; + measurement->variant.air_quality_metrics.particles_tps = sen5xmeasurement.tSize; + } + + if (model != SEN50) { + if (sen5xmeasurement.humidity != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_humidity = true; + measurement->variant.air_quality_metrics.pm_humidity = sen5xmeasurement.humidity; + } + if (sen5xmeasurement.temperature != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_temperature = true; + measurement->variant.air_quality_metrics.pm_temperature = sen5xmeasurement.temperature; + } + if (sen5xmeasurement.noxIndex != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_voc_idx = true; + measurement->variant.air_quality_metrics.pm_voc_idx = sen5xmeasurement.vocIndex; + } + } + + if (model == SEN55) { + if (sen5xmeasurement.noxIndex != FLT_MAX) { + measurement->variant.air_quality_metrics.has_pm_nox_idx = true; + measurement->variant.air_quality_metrics.pm_nox_idx = sen5xmeasurement.noxIndex; + } + } + + return true; + } else if (response == 1) { + // TODO return because data was not ready yet + // Should this return false? + idle(); + return false; + } else if (response == 2) { + // Return with error for non-existing data + idle(); + return false; + } + + return true; +} + +void SEN5XSensor::setMode(bool setOneShot) +{ + oneShotMode = setOneShot; + if (oneShotMode) { + LOG_INFO("%s setting mode to one shot mode", sensorName); + } else { + LOG_INFO("%s setting mode to continuous mode", sensorName); + } +} + +AdminMessageHandleResult SEN5XSensor::handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + AdminMessageHandleResult result; + result = AdminMessageHandleResult::NOT_HANDLED; + + switch (request->which_payload_variant) { + case meshtastic_AdminMessage_sensor_config_tag: + if (!request->sensor_config.has_sen5x_config) { + result = AdminMessageHandleResult::NOT_HANDLED; + break; + } + + // Check for one-shot/continuous mode request + if (request->sensor_config.sen5x_config.has_set_one_shot_mode) { + this->setMode(request->sensor_config.sen5x_config.set_one_shot_mode); + } + + // TODO - Add admin command to set temperature offset? + // Check for temperature offset + // if (request->sensor_config.sen5x_config.has_set_temperature) { + // this->setTemperature(request->sensor_config.sen5x_config.set_temperature); + // } + + // TODO - Add admin command to trigger fan cleaning? + // Check for one-shot/continuous mode request + // if (request->sensor_config.sen5x_config.has_fan_cleaning && request->sensor_config.sen5x_config.fan_cleaning) { + // this->startCleaning(); + // } + + result = AdminMessageHandleResult::HANDLED; + break; + + default: + result = AdminMessageHandleResult::NOT_HANDLED; + } + + return result; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SEN5XSensor.h b/src/modules/Telemetry/Sensor/SEN5XSensor.h new file mode 100644 index 000000000..ef5ad5c29 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SEN5XSensor.h @@ -0,0 +1,170 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include "Wire.h" + +// Warm up times for SEN5X from the datasheet +#ifndef SEN5X_WARMUP_MS_1 +#define SEN5X_WARMUP_MS_1 15000 +#endif + +#ifndef SEN5X_WARMUP_MS_2 +#define SEN5X_WARMUP_MS_2 30000 +#endif + +#ifndef SEN5X_POLL_INTERVAL +#define SEN5X_POLL_INTERVAL 1000 +#endif + +#ifndef SEN5X_I2C_CLOCK_SPEED +#define SEN5X_I2C_CLOCK_SPEED 100000 +#endif + +/* +Time after which the sensor can go to sleep, as the warmup period has passed +and the VOCs sensor will is allowed to stop (although needs to recover the state +each time) +*/ +#ifndef SEN5X_VOC_STATE_WARMUP_S +/* Note for Testing 5' is enough +Sensirion recommends 1h +This can be bypassed completely if switching to low-power RHT/Gas mode and setting +SEN5X_VOC_STATE_WARMUP_S 0 +*/ +#define SEN5X_VOC_STATE_WARMUP_S 3600 +#endif + +#define ONE_WEEK_IN_SECONDS 604800 + +struct _SEN5XMeasurements { + uint16_t pM1p0; + uint16_t pM2p5; + uint16_t pM4p0; + uint16_t pM10p0; + uint32_t pN0p5; + uint32_t pN1p0; + uint32_t pN2p5; + uint32_t pN4p0; + uint32_t pN10p0; + float tSize; + float humidity; + float temperature; + float vocIndex; + float noxIndex; +}; + +class SEN5XSensor : public TelemetrySensor +{ + private: + TwoWire *_bus{}; + uint8_t _address{}; + + bool getVersion(); + float firmwareVer = -1; + float hardwareVer = -1; + float protocolVer = -1; + bool findModel(); + +// Commands +#define SEN5X_RESET 0xD304 +#define SEN5X_GET_PRODUCT_NAME 0xD014 +#define SEN5X_GET_FIRMWARE_VERSION 0xD100 +#define SEN5X_START_MEASUREMENT 0x0021 +#define SEN5X_START_MEASUREMENT_RHT_GAS 0x0037 +#define SEN5X_STOP_MEASUREMENT 0x0104 +#define SEN5X_READ_DATA_READY 0x0202 +#define SEN5X_START_FAN_CLEANING 0x5607 +#define SEN5X_RW_VOCS_STATE 0x6181 + +#define SEN5X_READ_VALUES 0x03C4 +#define SEN5X_READ_RAW_VALUES 0x03D2 +#define SEN5X_READ_PM_VALUES 0x0413 + +#define SEN5X_VOC_VALID_TIME 600 +#define SEN5X_VOC_VALID_DATE 1514764800 + + enum SEN5Xmodel { SEN5X_UNKNOWN = 0, SEN50 = 0b001, SEN54 = 0b010, SEN55 = 0b100 }; + SEN5Xmodel model = SEN5X_UNKNOWN; + + enum SEN5XState { + SEN5X_OFF, + SEN5X_IDLE, + SEN5X_RHTGAS_ONLY, + SEN5X_MEASUREMENT, + SEN5X_MEASUREMENT_2, + SEN5X_CLEANING, + SEN5X_NOT_DETECTED + }; + SEN5XState state = SEN5X_OFF; + // Flag to work on one-shot (read and sleep), or continuous mode + bool oneShotMode = true; + void setMode(bool setOneShot); + bool vocStateValid(); +/* Sensirion recommends taking a reading after 15 seconds, +if the Particle number reading is over 100#/cm3 the reading is OK, +but if it is lower wait until 30 seconds and take it again. +See: https://sensirion.com/resource/application_note/low_power_mode/sen5x +*/ +#define SEN5X_PN4P0_CONC_THD 100 + + bool sendCommand(uint16_t command); + bool sendCommand(uint16_t command, uint8_t *buffer, uint8_t byteNumber = 0); + uint8_t readBuffer(uint8_t *buffer, uint8_t byteNumber); // Return number of bytes received + uint8_t sen5xCRC(const uint8_t *buffer); + bool startCleaning(); + uint8_t getMeasurements(); + // bool readRawValues(); + bool readPNValues(bool cumulative); + bool readValues(); + + uint32_t pmMeasureStarted = 0; + uint32_t rhtGasMeasureStarted = 0; + uint32_t lastDataPoll = 0; + _SEN5XMeasurements sen5xmeasurement{}; + + bool idle(bool checkState = true); + + protected: + // Store status of the sensor in this file + const char *sen5XStateFileName = "/prefs/sen5X.dat"; + meshtastic_SEN5XState sen5xstate = meshtastic_SEN5XState_init_zero; + + bool loadState(); + bool saveState(); + + // Cleaning State + uint32_t lastCleaning = 0; + bool lastCleaningValid = false; + +// VOC State +#define SEN5X_VOC_STATE_BUFFER_SIZE 8 + uint8_t vocState[SEN5X_VOC_STATE_BUFFER_SIZE]{}; + uint32_t vocTime = 0; + bool vocValid = false; + + bool vocStateFromSensor(); + bool vocStateToSensor(); + bool vocStateStable(); + bool vocStateRecent(uint32_t now); + + public: + SEN5XSensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + virtual bool canSleep() override { return true; } + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; + + AdminMessageHandleResult handleAdminMessage(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.cpp b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp new file mode 100644 index 000000000..c5b5845d9 --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.cpp @@ -0,0 +1,198 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../detect/reClockI2C.h" +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "SFA30Sensor.h" + +SFA30Sensor::SFA30Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_SFA30, "SFA30"){}; + +bool SFA30Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) +{ + LOG_INFO("Init sensor: %s", sensorName); + + _bus = bus; + _address = dev->address.address; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + sfa30.begin(*_bus, _address); + delay(20); + + if (this->isError(sfa30.deviceReset())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + state = State::IDLE; + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return false; + } + + LOG_INFO("%s starting measurement", sensorName); + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + status = 1; + state = State::ACTIVE; + measureStarted = getTime(); + LOG_INFO("%s Enabled", sensorName); + + initI2CSensor(); + return true; +}; + +bool SFA30Sensor::isError(uint16_t response) +{ + if (response == SFA30_NO_ERROR) + return false; + + // TODO - Check error to char conversion + LOG_ERROR("%s: %u", sensorName, response); + return true; +} + +void SFA30Sensor::sleep() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + // Note - not recommended for this sensor on a periodic basis + if (this->isError(sfa30.stopMeasurement())) { + LOG_ERROR("%s: can't stop measurement", sensorName); + }; + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + LOG_INFO("%s: stop measurement", sensorName); + state = State::IDLE; + measureStarted = 0; +} + +uint32_t SFA30Sensor::wakeUp() +{ +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + LOG_INFO("Waking up %s", sensorName); + if (this->isError(sfa30.startContinuousMeasurement())) { +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + return 0; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + state = State::ACTIVE; + measureStarted = getTime(); + return SFA30_WARMUP_MS; +} + +int32_t SFA30Sensor::wakeUpTimeMs() +{ + return SFA30_WARMUP_MS; +} + +bool SFA30Sensor::canSleep() +{ + // Sleep is disabled in this sensor because readings are not tested with periodic sleep + // with such low power consumption, prefered to keep it active + return false; +} + +bool SFA30Sensor::isActive() +{ + return state == State::ACTIVE; +} + +int32_t SFA30Sensor::pendingForReadyMs() +{ + uint32_t now; + now = getTime(); + uint32_t sinceHchoMeasureStarted = (now - measureStarted) * 1000; + LOG_DEBUG("%s: Since measure started: %ums", sensorName, sinceHchoMeasureStarted); + + if (sinceHchoMeasureStarted < SFA30_WARMUP_MS) { + LOG_INFO("%s: not enough time passed since starting measurement", sensorName); + return SFA30_WARMUP_MS - sinceHchoMeasureStarted; + } + return 0; +} + +bool SFA30Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + float hcho = 0.0; + float humidity = 0.0; + float temperature = 0.0; + +#ifdef SFA30_I2C_CLOCK_SPEED +#ifdef CAN_RECLOCK_I2C + uint32_t currentClock = reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, false); +#elif !HAS_SCREEN + reClockI2C(SFA30_I2C_CLOCK_SPEED, _bus, true); +#else + LOG_WARN("%s can't be used at this clock speed, with a screen", sensorName); + return false; +#endif /* CAN_RECLOCK_I2C */ +#endif /* SFA30_I2C_CLOCK_SPEED */ + + if (this->isError(sfa30.readMeasuredValues(hcho, humidity, temperature))) { + LOG_WARN("%s: No values", sensorName); + return false; + } + +#if defined(SFA30_I2C_CLOCK_SPEED) && defined(CAN_RECLOCK_I2C) + reClockI2C(currentClock, _bus, false); +#endif + + measurement->variant.air_quality_metrics.has_form_temperature = true; + measurement->variant.air_quality_metrics.has_form_humidity = true; + measurement->variant.air_quality_metrics.has_form_formaldehyde = true; + + measurement->variant.air_quality_metrics.form_temperature = temperature; + measurement->variant.air_quality_metrics.form_humidity = humidity; + measurement->variant.air_quality_metrics.form_formaldehyde = hcho; + + LOG_DEBUG("Got %s readings: hcho=%.2f, hcho_temp=%.2f, hcho_hum=%.2f", sensorName, hcho, temperature, humidity); + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/SFA30Sensor.h b/src/modules/Telemetry/Sensor/SFA30Sensor.h new file mode 100644 index 000000000..9fa9c85fc --- /dev/null +++ b/src/modules/Telemetry/Sensor/SFA30Sensor.h @@ -0,0 +1,39 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RTC.h" +#include "TelemetrySensor.h" +#include + +#define SFA30_I2C_CLOCK_SPEED 100000 +#define SFA30_WARMUP_MS 10000 +#define SFA30_NO_ERROR 0 + +class SFA30Sensor : public TelemetrySensor +{ + private: + enum class State { IDLE, ACTIVE }; + State state = State::IDLE; + uint32_t measureStarted = 0; + + SensirionI2cSfa3x sfa30; + TwoWire *_bus{}; + uint8_t _address{}; + bool isError(uint16_t response); + + public: + SFA30Sensor(); + virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; + + virtual bool isActive() override; + virtual void sleep() override; + virtual uint32_t wakeUp() override; + virtual bool canSleep() override; + virtual int32_t wakeUpTimeMs() override; + virtual int32_t pendingForReadyMs() override; +}; + +#endif diff --git a/src/modules/Telemetry/Sensor/TelemetrySensor.h b/src/modules/Telemetry/Sensor/TelemetrySensor.h index af51ddfad..47deaa936 100644 --- a/src/modules/Telemetry/Sensor/TelemetrySensor.h +++ b/src/modules/Telemetry/Sensor/TelemetrySensor.h @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR #pragma once #include "../mesh/generated/meshtastic/telemetry.pb.h" @@ -26,7 +26,6 @@ class TelemetrySensor this->status = 0; } - const char *sensorName; meshtastic_TelemetrySensorType sensorType = meshtastic_TelemetrySensorType_SENSOR_UNSET; unsigned status; bool initialized = false; @@ -56,13 +55,18 @@ class TelemetrySensor return AdminMessageHandleResult::NOT_HANDLED; } + const char *sensorName; // TODO: delete after migration bool hasSensor() { return nodeTelemetrySensorsMap[sensorType].first > 0; } + // Functions to sleep / wakeup sensors that support it + // These functions can save power consumption in cases like AQ virtual void sleep(){}; virtual uint32_t wakeUp() { return 0; } - // Return active by default, override per sensor - virtual bool isActive() { return true; } + virtual bool isActive() { return true; } // Return true by default, override per sensor + virtual bool canSleep() { return false; } // Return false by default, override per sensor + virtual int32_t wakeUpTimeMs() { return 0; } + virtual int32_t pendingForReadyMs() { return 0; } #if WIRE_INTERFACES_COUNT > 1 // Set to true if Implementation only works first I2C port (Wire) diff --git a/src/modules/TraceRouteModule.cpp b/src/modules/TraceRouteModule.cpp index 41dc02cd1..3371c405d 100644 --- a/src/modules/TraceRouteModule.cpp +++ b/src/modules/TraceRouteModule.cpp @@ -266,7 +266,7 @@ void TraceRouteModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtasti } } -void TraceRouteModule::updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) +void TraceRouteModule::updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r) { // E.g. if the route is A->B->C->D and we are B, we can set C as next-hop for C and D // Similarly, if we are C, we can set D as next-hop for D diff --git a/src/modules/TraceRouteModule.h b/src/modules/TraceRouteModule.h index a40ed7733..db94b9d9b 100644 --- a/src/modules/TraceRouteModule.h +++ b/src/modules/TraceRouteModule.h @@ -62,7 +62,7 @@ class TraceRouteModule : public ProtobufModule, void appendMyIDandSNR(meshtastic_RouteDiscovery *r, float snr, bool isTowardsDestination, bool SNRonly); // Update next-hops in the routing table based on the returned route - void updateNextHops(meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); + void updateNextHops(const meshtastic_MeshPacket &p, meshtastic_RouteDiscovery *r); // Helper to update next-hop for a single node void maybeSetNextHop(NodeNum target, uint8_t nextHopByte); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 18a4f913e..32b637e81 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -53,6 +53,9 @@ static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for cha static bool isMqttServerAddressPrivate = false; static bool isConnected = false; +static uint32_t lastPositionUnavailableWarning = 0; +static const uint32_t POSITION_UNAVAILABLE_WARNING_INTERVAL_MS = 15000; // 15 seconds + inline void onReceiveProto(char *topic, byte *payload, size_t length) { const DecodedServiceEnvelope e(payload, length); @@ -453,7 +456,9 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) enabled = true; runASAP = true; reconnectCount = 0; +#if !IS_RUNNING_TESTS publishNodeInfo(); +#endif } // preflightSleepObserver.observe(&preflightSleep); } else { @@ -754,10 +759,8 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me if (mp_decoded.which_payload_variant == meshtastic_MeshPacket_decoded_tag) { // For uplinking other's packets, check if it's not OK to MQTT or if it's an older packet without the bitfield bool dontUplink = !mp_decoded.decoded.has_bitfield || !(mp_decoded.decoded.bitfield & BITFIELD_OK_TO_MQTT_MASK); - // check for the lowest bit of the data bitfield set false, and the use of one of the default keys. - if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink && - (ch.settings.psk.size < 2 || (ch.settings.psk.size == 16 && memcmp(ch.settings.psk.bytes, defaultpsk, 16)) || - (ch.settings.psk.size == 32 && memcmp(ch.settings.psk.bytes, eventpsk, 32)))) { + // Respect the DontMqttMeBro flag for other nodes' packets on public MQTT servers + if (!isFromUs(&mp_decoded) && !isMqttServerAddressPrivate && dontUplink) { LOG_INFO("MQTT onSend - Not forwarding packet due to DontMqttMeBro flag"); return; } @@ -847,12 +850,14 @@ void MQTT::perhapsReportToMap() map_position_precision = default_map_position_precision; } - if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs)) + if (Throttle::isWithinTimespanMs(last_report_to_map, map_publish_interval_msecs) && last_report_to_map != 0) return; if (localPosition.latitude_i == 0 && localPosition.longitude_i == 0) { - last_report_to_map = millis(); - LOG_WARN("MQTT Map report enabled, but no position available"); + if (Throttle::isWithinTimespanMs(lastPositionUnavailableWarning, POSITION_UNAVAILABLE_WARNING_INTERVAL_MS) == false) { + LOG_WARN("MQTT Map report enabled, but no position available"); + lastPositionUnavailableWarning = millis(); + } return; } diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 2a59c0aab..3bb4ce817 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -52,6 +52,20 @@ NimBLEServer *bleServer; static bool passkeyShowing; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" +static void clearPairingDisplay() +{ + if (!passkeyShowing) { + return; + } + + passkeyShowing = false; +#if HAS_SCREEN + if (screen) { + screen->endAlert(); + } +#endif +} + class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { /* @@ -630,13 +644,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newStatus); - - // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - if (passkeyShowing) { - passkeyShowing = false; - if (screen) - screen->endAlert(); - } + clearPairingDisplay(); // Store the connection handle for future use #ifdef NIMBLE_TWO @@ -693,6 +701,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); + clearPairingDisplay(); if (bluetoothPhoneAPI) { bluetoothPhoneAPI->close(); @@ -757,11 +766,7 @@ void NimbleBluetooth::deinit() isDeInit = true; #ifdef BLE_LED -#ifdef BLE_LED_INVERTED - digitalWrite(BLE_LED, HIGH); -#else - digitalWrite(BLE_LED, LOW); -#endif + digitalWrite(BLE_LED, LED_STATE_OFF); #endif #ifndef NIMBLE_TWO NimBLEDevice::deinit(); diff --git a/src/platform/esp32/MeshtasticOTA.cpp b/src/platform/esp32/MeshtasticOTA.cpp index 4ca074723..20a3c59cc 100644 --- a/src/platform/esp32/MeshtasticOTA.cpp +++ b/src/platform/esp32/MeshtasticOTA.cpp @@ -75,7 +75,7 @@ bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc) return true; } -bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method) +bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method) { // Combined loader supports all (both) transports, BLE and WiFi if (strcmp(app_desc->project_name, combinedAppProjectName) == 0) { diff --git a/src/platform/esp32/MeshtasticOTA.h b/src/platform/esp32/MeshtasticOTA.h index 7c158775f..ce5a7e86b 100644 --- a/src/platform/esp32/MeshtasticOTA.h +++ b/src/platform/esp32/MeshtasticOTA.h @@ -16,7 +16,7 @@ void initialize(); bool isUpdated(); const esp_partition_t *getAppPartition(); bool getAppDesc(const esp_partition_t *part, esp_app_desc_t *app_desc); -bool checkOTACapability(esp_app_desc_t *app_desc, uint8_t method); +bool checkOTACapability(const esp_app_desc_t *app_desc, uint8_t method); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network, meshtastic_OTAMode method, uint8_t *ota_hash); bool trySwitchToOTA(); diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 6667acf5c..171911e5e 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -24,6 +24,11 @@ #include #include +// Weak empty variant shutdown prep function. +// May be redefined by variant files. +void variant_shutdown() __attribute__((weak)); +void variant_shutdown() {} + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { @@ -249,6 +254,7 @@ void cpuDeepSleep(uint32_t msecToWake) #endif // #end ESP32S3_WAKE_TYPE #endif + variant_shutdown(); // We want RTC peripherals to stay on esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/src/platform/nrf52/AsyncUDP.cpp b/src/platform/nrf52/AsyncUDP.cpp index 836fb1307..d119ffddb 100644 --- a/src/platform/nrf52/AsyncUDP.cpp +++ b/src/platform/nrf52/AsyncUDP.cpp @@ -36,6 +36,13 @@ bool AsyncUDP::writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t p return udp.endPacket(); } +void AsyncUDP::close() +{ + udp.stop(); + localPort = 0; + _onPacket = nullptr; +} + // AsyncUDPPacket AsyncUDPPacket::AsyncUDPPacket(EthernetUDP &source) : _udp(source), _remoteIP(source.remoteIP()), _remotePort(source.remotePort()) { diff --git a/src/platform/nrf52/AsyncUDP.h b/src/platform/nrf52/AsyncUDP.h index e2b406ba9..ecbcb12fb 100644 --- a/src/platform/nrf52/AsyncUDP.h +++ b/src/platform/nrf52/AsyncUDP.h @@ -22,6 +22,7 @@ class AsyncUDP : public Print, private concurrency::OSThread bool listenMulticast(IPAddress multicastIP, uint16_t port, uint8_t ttl = 64); bool writeTo(const uint8_t *data, size_t len, IPAddress ip, uint16_t port); + void close(); size_t write(uint8_t b) override; size_t write(const uint8_t *data, size_t len) override; @@ -37,7 +38,7 @@ class AsyncUDP : public Print, private concurrency::OSThread class AsyncUDPPacket { public: - AsyncUDPPacket(EthernetUDP &source); + explicit AsyncUDPPacket(EthernetUDP &source); IPAddress remoteIP(); uint16_t length(); diff --git a/src/platform/nrf52/BLEDfuScure.cpp b/src/platform/nrf52/BLEDfuSecure.cpp similarity index 99% rename from src/platform/nrf52/BLEDfuScure.cpp rename to src/platform/nrf52/BLEDfuSecure.cpp index 82cb8905a..040df8bdf 100644 --- a/src/platform/nrf52/BLEDfuScure.cpp +++ b/src/platform/nrf52/BLEDfuSecure.cpp @@ -1,6 +1,6 @@ /**************************************************************************/ /*! - @file BLEDfu.cpp + @file BLEDfuSecure.cpp @author hathach (tinyusb.org) @section LICENSE diff --git a/src/platform/nrf52/BLEDfuSecure.h b/src/platform/nrf52/BLEDfuSecure.h index bd5d910e8..dc52d3940 100644 --- a/src/platform/nrf52/BLEDfuSecure.h +++ b/src/platform/nrf52/BLEDfuSecure.h @@ -1,6 +1,6 @@ /**************************************************************************/ /*! - @file BLEDfu.h + @file BLEDfuSecure.h @author hathach (tinyusb.org) @section LICENSE diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 6a552f236..307e35b0c 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -32,6 +32,7 @@ static uint8_t toRadioBytes[meshtastic_ToRadio_size]; static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; static uint16_t connectionHandle; +static bool passkeyShowing; class BluetoothPhoneAPI : public PhoneAPI { @@ -86,6 +87,16 @@ void onDisconnect(uint16_t conn_handle, uint8_t reason) // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); + +#if HAS_SCREEN + // If a pairing prompt is active, make sure we dismiss it on disconnect/cancel/failure paths. + if (passkeyShowing) { + passkeyShowing = false; + if (screen) { + screen->endAlert(); + } + } +#endif } void onCccd(uint16_t conn_hdl, BLECharacteristic *chr, uint16_t cccd_value) { @@ -400,6 +411,8 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke }); } #endif + passkeyShowing = true; + if (match_request) { uint32_t start_time = millis(); while (millis() < start_time + 30000) { @@ -451,6 +464,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus + passkeyShowing = false; if (screen) { screen->endAlert(); } @@ -464,4 +478,4 @@ void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) logRadio.indicate(logMessage, (uint16_t)length); else logRadio.notify(logMessage, (uint16_t)length); -} \ No newline at end of file +} diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index d1965f03e..f73a5b896 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -158,7 +158,7 @@ #endif #ifdef PIN_LED1 -#define LED_PIN PIN_LED1 // LED1 on nrf52840-DK +#define LED_POWER PIN_LED1 // LED1 on nrf52840-DK #endif #ifdef PIN_BUTTON1 diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 0376a1dad..c705521ad 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -46,7 +46,7 @@ uint16_t getVDDVoltage(); -// Weak empty variant initialization function. +// Weak empty variant shutdown prep function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); void variant_shutdown() {} diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index d0d8ba40f..2cf23332c 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -496,7 +496,7 @@ void portduinoSetup() randomSeed(time(NULL)); std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip); - for (auto i : portduino_config.all_pins) { + for (const auto *i : portduino_config.all_pins) { if (i->enabled && i->pin > max_GPIO) max_GPIO = i->pin; } @@ -510,7 +510,7 @@ void portduinoSetup() // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate - for (auto i : portduino_config.all_pins) { + for (const auto *i : portduino_config.all_pins) { // In the case of a ch341 Lora device, we don't want to touch the system GPIO lines for Lora // Those GPIO are handled in our usermode driver instead. if (i->config_section == "Lora" && portduino_config.lora_spi_dev == "ch341") { @@ -537,6 +537,27 @@ void portduinoSetup() } } + // In one test, this dance seemed necessary to trigger the pin to detect properly. + if (portduino_config.lora_pa_detect_pin.enabled) { + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLDOWN); + sleep(1); + if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == LOW) { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLDOWN is LOW" << std::endl; + } + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT_PULLUP); + sleep(1); + if (digitalRead(portduino_config.lora_pa_detect_pin.pin) == HIGH) { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is HIGH, dropping PA curve" << std::endl; + portduino_config.num_pa_points = 1; + portduino_config.tx_gain_lora[0] = 0; + } else { + std::cout << "Pin " << portduino_config.lora_pa_detect_pin.pin << " PULLUP is LOW, using PA curve" << std::endl; + } + + // disable bias once finished + pinMode(portduino_config.lora_pa_detect_pin.pin, INPUT); + } + // Only initialize the radio pins when dealing with real, kernel controlled SPI hardware if (portduino_config.lora_spi_dev != "" && portduino_config.lora_spi_dev != "ch341") { SPI.begin(portduino_config.lora_spi_dev.c_str()); @@ -572,7 +593,7 @@ void portduinoSetup() return; } -int initGPIOPin(int pinNum, const std::string gpioChipName, int line) +int initGPIOPin(int pinNum, const std::string &gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE std::string gpio_name = "GPIO" + std::to_string(pinNum); @@ -643,7 +664,7 @@ bool loadConfig(const char *configPath) if (yamlConfig["Lora"]) { if (yamlConfig["Lora"]["Module"]) { - for (auto &loraModule : portduino_config.loraModules) { + for (const auto &loraModule : portduino_config.loraModules) { if (yamlConfig["Lora"]["Module"].as("") == loraModule.second) { portduino_config.lora_module = loraModule.first; break; @@ -777,7 +798,7 @@ bool loadConfig(const char *configPath) } if (yamlConfig["Display"]) { - for (auto &screen_name : portduino_config.screen_names) { + for (const auto &screen_name : portduino_config.screen_names) { if (yamlConfig["Display"]["Panel"].as("") == screen_name.second) portduino_config.displayPanel = screen_name.first; } @@ -872,6 +893,7 @@ bool loadConfig(const char *configPath) } if (yamlConfig["Config"]) { + portduino_config.has_config_overrides = true; if (yamlConfig["Config"]["DisplayMode"]) { portduino_config.has_configDisplayMode = true; if ((yamlConfig["Config"]["DisplayMode"]).as("") == "TWOCOLOR") { @@ -884,6 +906,13 @@ bool loadConfig(const char *configPath) portduino_config.configDisplayMode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; } } + if (yamlConfig["Config"]["StatusMessage"]) { + portduino_config.has_statusMessage = true; + portduino_config.statusMessage = (yamlConfig["Config"]["StatusMessage"]).as(""); + } + if ((yamlConfig["Config"]["EnableUDP"]).as(false)) { + portduino_config.enable_UDP = true; + } } if (yamlConfig["General"]) { @@ -899,7 +928,7 @@ bool loadConfig(const char *configPath) } if (checkConfigPort) { portduino_config.api_port = (yamlConfig["General"]["APIPort"]).as(-1); - if (portduino_config.api_port != -1 && portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { + if (portduino_config.api_port > 1023 && portduino_config.api_port < 65536) { TCPPort = (portduino_config.api_port); } } diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index af511be6e..0bd3582de 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -58,7 +58,7 @@ extern std::ofstream traceFile; extern std::ofstream JSONFile; extern Ch341Hal *ch341Hal; -int initGPIOPin(int pinNum, std::string gpioChipname, int line); +int initGPIOPin(int pinNum, const std::string &gpioChipname, int line); bool loadConfig(const char *configPath); static bool ends_with(std::string_view str, std::string_view suffix); void getMacAddr(uint8_t *dmac); @@ -107,6 +107,7 @@ extern struct portduino_config_struct { pinMapping lora_txen_pin = {"Lora", "TXen"}; pinMapping lora_rxen_pin = {"Lora", "RXen"}; pinMapping lora_sx126x_ant_sw_pin = {"Lora", "SX126X_ANT_SW"}; + pinMapping lora_pa_detect_pin = {"Lora", "GPIO_DETECT_PA"}; std::vector extra_pins = {}; // GPS @@ -177,8 +178,12 @@ extern struct portduino_config_struct { int hostMetrics_channel = 0; // config + bool has_config_overrides = false; int configDisplayMode = 0; bool has_configDisplayMode = false; + std::string statusMessage = ""; + bool has_statusMessage = false; + bool enable_UDP = false; // General std::string mac_address = ""; @@ -190,13 +195,14 @@ extern struct portduino_config_struct { int maxtophone = 100; int MaxNodes = 200; - pinMapping *all_pins[20] = {&lora_cs_pin, + pinMapping *all_pins[21] = {&lora_cs_pin, &lora_irq_pin, &lora_busy_pin, &lora_reset_pin, &lora_txen_pin, &lora_rxen_pin, &lora_sx126x_ant_sw_pin, + &lora_pa_detect_pin, &displayDC, &displayCS, &displayBacklight, @@ -220,7 +226,7 @@ extern struct portduino_config_struct { out << YAML::Key << "Lora" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Module" << YAML::Value << loraModules[lora_module]; - for (auto lora_pin : all_pins) { + for (const auto *lora_pin : all_pins) { if (lora_pin->config_section == "Lora" && lora_pin->enabled) { out << YAML::Key << lora_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << lora_pin->pin; @@ -251,18 +257,23 @@ extern struct portduino_config_struct { out << YAML::Key << "TX_GAIN_LORA" << YAML::Value << tx_gain_lora[0]; } - out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; + if (dio2_as_rf_switch) + out << YAML::Key << "DIO2_AS_RF_SWITCH" << YAML::Value << dio2_as_rf_switch; if (dio3_tcxo_voltage != 0) out << YAML::Key << "DIO3_TCXO_VOLTAGE" << YAML::Value << YAML::Precision(3) << (float)dio3_tcxo_voltage / 1000; if (lora_usb_pid != 0x5512) out << YAML::Key << "USB_PID" << YAML::Value << YAML::Hex << lora_usb_pid; if (lora_usb_vid != 0x1A86) out << YAML::Key << "USB_VID" << YAML::Value << YAML::Hex << lora_usb_vid; - if (lora_spi_dev != "") + if (lora_spi_dev != "" && !(lora_spi_dev == "/dev/spidev0.0" && lora_module == use_autoconf)) { + if (lora_spi_dev.find("/dev/") != std::string::npos) + lora_spi_dev = lora_spi_dev.substr(5); out << YAML::Key << "spidev" << YAML::Value << lora_spi_dev; + } if (lora_usb_serial_num != "") out << YAML::Key << "USB_Serialnum" << YAML::Value << lora_usb_serial_num; - out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; + if (spiSpeed != 2000000) + out << YAML::Key << "spiSpeed" << YAML::Value << spiSpeed; if (rfswitch_dio_pins[0] != RADIOLIB_NC) { out << YAML::Key << "rfswitch_table" << YAML::Value << YAML::BeginMap; @@ -346,11 +357,11 @@ extern struct portduino_config_struct { // Display if (displayPanel != no_screen) { out << YAML::Key << "Display" << YAML::Value << YAML::BeginMap; - for (auto &screen_name : screen_names) { + for (const auto &screen_name : screen_names) { if (displayPanel == screen_name.first) out << YAML::Key << "Module" << YAML::Value << screen_name.second; } - for (auto display_pin : all_pins) { + for (const auto *display_pin : all_pins) { if (display_pin->config_section == "Display" && display_pin->enabled) { out << YAML::Key << display_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << display_pin->pin; @@ -398,7 +409,7 @@ extern struct portduino_config_struct { case ft5x06: out << YAML::Key << "Module" << YAML::Value << "FT5x06"; } - for (auto touchscreen_pin : all_pins) { + for (const auto *touchscreen_pin : all_pins) { if (touchscreen_pin->config_section == "Touchscreen" && touchscreen_pin->enabled) { out << YAML::Key << touchscreen_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << touchscreen_pin->pin; @@ -421,7 +432,7 @@ extern struct portduino_config_struct { if (pointerDevice != "") out << YAML::Key << "PointerDevice" << YAML::Value << pointerDevice; - for (auto input_pin : all_pins) { + for (const auto *input_pin : all_pins) { if (input_pin->config_section == "Input" && input_pin->enabled) { out << YAML::Key << input_pin->config_name << YAML::Value << YAML::BeginMap; out << YAML::Key << "pin" << YAML::Value << input_pin->pin; @@ -505,21 +516,30 @@ extern struct portduino_config_struct { } // config - if (has_configDisplayMode) { + if (has_config_overrides) { out << YAML::Key << "Config" << YAML::Value << YAML::BeginMap; - switch (configDisplayMode) { - case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: - out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: - out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; - break; - case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: - out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; - break; + if (has_configDisplayMode) { + + switch (configDisplayMode) { + case meshtastic_Config_DisplayConfig_DisplayMode_TWOCOLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "TWOCOLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_INVERTED: + out << YAML::Key << "DisplayMode" << YAML::Value << "INVERTED"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_COLOR: + out << YAML::Key << "DisplayMode" << YAML::Value << "COLOR"; + break; + case meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT: + out << YAML::Key << "DisplayMode" << YAML::Value << "DEFAULT"; + break; + } + } + if (has_statusMessage) { + out << YAML::Key << "StatusMessage" << YAML::Value << statusMessage; + } + if (enable_UDP) { + out << YAML::Key << "EnableUDP" << YAML::Value << true; } out << YAML::EndMap; // Config diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index 441f75b10..1725763f2 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -29,7 +29,7 @@ class Ch341Hal : public RadioLibHal { public: // default constructor - initializes the base HAL and any needed private members - explicit Ch341Hal(uint8_t spiChannel, std::string serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, + explicit Ch341Hal(uint8_t spiChannel, const std::string &serial = "", uint32_t vid = 0x1A86, uint32_t pid = 0x5512, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) { diff --git a/src/power.h b/src/power.h index e4b456d3b..b129e2b74 100644 --- a/src/power.h +++ b/src/power.h @@ -103,8 +103,10 @@ class Power : private concurrency::OSThread bool axpChipInit(); /// Setup a simple ADC input based battery sensor bool analogInit(); - /// Setup a Lipo battery level sensor - bool lipoInit(); + /// Setup cw2015 battery level sensor + bool cw2015Init(); + /// Setup a 17048 battery level sensor + bool max17048Init(); /// Setup a Lipo charger bool lipoChargerInit(); /// Setup a meshSolar battery sensor diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index a12972cb0..819ba3da5 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -149,17 +149,23 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_pm100_standard) { msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + if (decoded->variant.air_quality_metrics.has_co2) { + msgPayload["co2"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.co2); } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + msgPayload["co2_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.co2_temperature); } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = - new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + msgPayload["co2_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.co2_humidity); + } + if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { + msgPayload["form_formaldehyde"] = new JSONValue(decoded->variant.air_quality_metrics.form_formaldehyde); + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + msgPayload["form_temperature"] = new JSONValue(decoded->variant.air_quality_metrics.form_temperature); + } + if (decoded->variant.air_quality_metrics.has_form_humidity) { + msgPayload["form_humidity"] = new JSONValue(decoded->variant.air_quality_metrics.form_humidity); } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 41f505b94..bd0a29c51 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -120,14 +120,23 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (decoded->variant.air_quality_metrics.has_pm100_standard) { jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; } - if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + if (decoded->variant.air_quality_metrics.has_co2) { + jsonObj["payload"]["co2"] = (unsigned int)decoded->variant.air_quality_metrics.co2; } - if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + if (decoded->variant.air_quality_metrics.has_co2_temperature) { + jsonObj["payload"]["co2_temperature"] = decoded->variant.air_quality_metrics.co2_temperature; } - if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + if (decoded->variant.air_quality_metrics.has_co2_humidity) { + jsonObj["payload"]["co2_humidity"] = decoded->variant.air_quality_metrics.co2_humidity; + } + if (decoded->variant.air_quality_metrics.has_form_formaldehyde) { + jsonObj["payload"]["form_formaldehyde"] = decoded->variant.air_quality_metrics.form_formaldehyde; + } + if (decoded->variant.air_quality_metrics.has_form_temperature) { + jsonObj["payload"]["form_temperature"] = decoded->variant.air_quality_metrics.form_temperature; + } + if (decoded->variant.air_quality_metrics.has_form_humidity) { + jsonObj["payload"]["form_humidity"] = decoded->variant.air_quality_metrics.form_humidity; } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { diff --git a/src/sleep.cpp b/src/sleep.cpp index 8b30a5352..8470e9273 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -5,14 +5,15 @@ #endif #include "Default.h" -#include "Led.h" #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" #include "PowerMon.h" +#include "TransmitHistory.h" #include "detect/LoRaRadioType.h" #include "error.h" #include "main.h" +#include "modules/StatusLEDModule.h" #include "sleep.h" #include "target_specific.h" @@ -162,6 +163,13 @@ void initDeepSleep() if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { LOG_DEBUG("Disable any holds on RTC IO pads"); for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { +#if defined(USE_GC1109_PA) + // Skip GC1109 FEM power pins - they are held HIGH during deep sleep to keep + // the LNA active for RX wake. Released later in SX126xInterface::init() after + // GPIO registers are set HIGH first, avoiding a power glitch. + if (i == LORA_PA_POWER || i == LORA_PA_EN) + continue; +#endif if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) rtc_gpio_hold_dis((gpio_num_t)i); @@ -238,6 +246,10 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN nodeDB->saveToDisk(); } + // Persist broadcast transmit times so throttle survives reboot + if (transmitHistory) + transmitHistory->saveToDisk(); + #ifdef PIN_POWER_EN digitalWrite(PIN_POWER_EN, LOW); pinMode(PIN_POWER_EN, INPUT); // power off peripherals @@ -268,8 +280,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN digitalWrite(PIN_WD_EN, LOW); #endif #endif - ledBlink.set(false); - + statusLEDModule->setPowerLED(false); #ifdef RESET_OLED digitalWrite(RESET_OLED, 1); // put the display in reset before killing its power #endif @@ -557,8 +568,13 @@ void enableLoraInterrupt() #endif #if defined(USE_GC1109_PA) - gpio_pullup_en((gpio_num_t)LORA_PA_POWER); - gpio_pullup_en((gpio_num_t)LORA_PA_EN); + // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. + // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), + // then latch with RTC hold so the state survives deep sleep. + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + digitalWrite(LORA_PA_EN, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_EN); gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); #endif diff --git a/test/TestUtil.cpp b/test/TestUtil.cpp index b470b8ce8..a8262238f 100644 --- a/test/TestUtil.cpp +++ b/test/TestUtil.cpp @@ -4,6 +4,13 @@ #include "TestUtil.h" +#if defined(ARDUINO) +#include +#else +#include +#include +#endif + void initializeTestEnvironment() { concurrency::hasBeenSetup = true; @@ -15,4 +22,13 @@ void initializeTestEnvironment() perhapsSetRTC(RTCQualityNTP, &tv); #endif concurrency::OSThread::setup(); +} + +void testDelay(unsigned long ms) +{ +#if defined(ARDUINO) + ::delay(ms); +#else + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +#endif } \ No newline at end of file diff --git a/test/TestUtil.h b/test/TestUtil.h index ce021e459..fb634184b 100644 --- a/test/TestUtil.h +++ b/test/TestUtil.h @@ -1,4 +1,7 @@ #pragma once // Initialize testing environment. -void initializeTestEnvironment(); \ No newline at end of file +void initializeTestEnvironment(); + +// Portable delay for tests (Arduino or host). +void testDelay(unsigned long ms); \ No newline at end of file diff --git a/test/test_atak/test_main.cpp b/test/test_atak/test_main.cpp new file mode 100644 index 000000000..84078b300 --- /dev/null +++ b/test/test_atak/test_main.cpp @@ -0,0 +1,216 @@ +#include +#include + +#include "TestUtil.h" +#include "meshUtils.h" + +void setUp(void) +{ + // set stuff up here +} + +void tearDown(void) +{ + // clean stuff up here +} + +/** + * Test normal string without embedded nulls + * Should behave the same as strlen() for regular strings + */ +void test_normal_string(void) +{ + char test_str[32] = "Hello World"; + size_t expected = 11; // strlen("Hello World") + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test empty string + * Should return 0 for empty string + */ +void test_empty_string(void) +{ + char test_str[32] = ""; + size_t expected = 0; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test string with only trailing nulls + * Common case - string followed by null padding + */ +void test_trailing_nulls(void) +{ + char test_str[32] = {0}; + strcpy(test_str, "Test"); + // test_str is now: "Test\0\0\0\0..." (4 chars + 28 nulls) + size_t expected = 4; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(expected, result); +} + +/** + * Test string with embedded null byte + * This is the critical bug case - strlen() would truncate at first null + */ +void test_embedded_null(void) +{ + char test_str[32] = {0}; + // Create string "ABC\0XYZ" (embedded null after C) + test_str[0] = 'A'; + test_str[1] = 'B'; + test_str[2] = 'C'; + test_str[3] = '\0'; // embedded null + test_str[4] = 'X'; + test_str[5] = 'Y'; + test_str[6] = 'Z'; + // Rest is already null from initialization + + // strlen would return 3, but pb_string_length should return 7 + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(3, strlen_result); // strlen stops at first null + TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds last non-null +} + +/** + * Test Android UID with embedded null bytes + * Real-world case from bug report: ANDROID-e7e455b40002429d + * The "00" in the UID represents 0x00 bytes that were truncating the string + */ +void test_android_uid_pattern(void) +{ + char test_str[32] = {0}; + // Simulate "ANDROID-e7e455b4" + 0x00 + 0x00 + "2429d" + const char part1[] = "ANDROID-e7e455b4"; + strcpy(test_str, part1); + size_t pos = strlen(part1); + test_str[pos] = '\0'; // embedded null + test_str[pos + 1] = '\0'; // another embedded null + strcpy(test_str + pos + 2, "2429d"); + + // The full UID should be 24 characters + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(16, strlen_result); // strlen truncates to "ANDROID-e7e455b4" + TEST_ASSERT_EQUAL_size_t(23, pb_result); // pb_string_length gets full length +} + +/** + * Test string with multiple embedded nulls + * Edge case with several null bytes scattered through the string + */ +void test_multiple_embedded_nulls(void) +{ + char test_str[32] = {0}; + // Create "A\0B\0C\0D" (3 embedded nulls) + test_str[0] = 'A'; + test_str[1] = '\0'; + test_str[2] = 'B'; + test_str[3] = '\0'; + test_str[4] = 'C'; + test_str[5] = '\0'; + test_str[6] = 'D'; + + size_t strlen_result = strlen(test_str); + size_t pb_result = pb_string_length(test_str, sizeof(test_str)); + + TEST_ASSERT_EQUAL_size_t(1, strlen_result); // strlen stops at first null + TEST_ASSERT_EQUAL_size_t(7, pb_result); // pb_string_length finds all chars +} + +/** + * Test buffer completely filled with non-null characters + * Edge case where string uses entire buffer + */ +void test_full_buffer(void) +{ + char test_str[8]; + // Fill entire buffer with 'X' + memset(test_str, 'X', sizeof(test_str)); + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(8, result); +} + +/** + * Test buffer with all nulls + * Should return 0 + */ +void test_all_nulls(void) +{ + char test_str[32] = {0}; + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(0, result); +} + +/** + * Test single character followed by nulls + * Minimal non-empty case + */ +void test_single_char(void) +{ + char test_str[32] = {0}; + test_str[0] = 'X'; + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(1, result); +} + +/** + * Test callsign field typical size + * Test with typical ATAK callsign field size (64 bytes) + */ +void test_callsign_field_size(void) +{ + char test_str[64] = {0}; + strcpy(test_str, "CALLSIGN-123"); + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(12, result); +} + +/** + * Test with data at end of buffer + * String with embedded null and data at very end + */ +void test_data_at_buffer_end(void) +{ + char test_str[10] = {0}; + test_str[0] = 'A'; + test_str[1] = '\0'; + test_str[8] = 'Z'; // Data near end + test_str[9] = 'X'; // Data at end + + size_t result = pb_string_length(test_str, sizeof(test_str)); + TEST_ASSERT_EQUAL_size_t(10, result); // Should find the 'X' at position 9 +} + +void setup() +{ + // NOTE!!! Wait for >2 secs + // if board doesn't support software reset via Serial.DTR/RTS + testDelay(10); + testDelay(2000); + + UNITY_BEGIN(); + RUN_TEST(test_normal_string); + RUN_TEST(test_empty_string); + RUN_TEST(test_trailing_nulls); + RUN_TEST(test_embedded_null); + RUN_TEST(test_android_uid_pattern); + RUN_TEST(test_multiple_embedded_nulls); + RUN_TEST(test_full_buffer); + RUN_TEST(test_all_nulls); + RUN_TEST(test_single_char); + RUN_TEST(test_callsign_field_size); + RUN_TEST(test_data_at_buffer_end); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_http_content_handler/test_main.cpp b/test/test_http_content_handler/test_main.cpp new file mode 100644 index 000000000..af0a41cef --- /dev/null +++ b/test/test_http_content_handler/test_main.cpp @@ -0,0 +1,19 @@ +#include "TestUtil.h" +#include + +static void test_placeholder() +{ + TEST_ASSERT_TRUE(true); +} + +extern "C" { +void setup() +{ + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_placeholder); + exit(UNITY_END()); +} + +void loop() {} +} diff --git a/test/test_mesh_module/test_main.cpp b/test/test_mesh_module/test_main.cpp new file mode 100644 index 000000000..ec7b1808e --- /dev/null +++ b/test/test_mesh_module/test_main.cpp @@ -0,0 +1,116 @@ +#include "MeshModule.h" +#include "MeshTypes.h" +#include "TestUtil.h" +#include + +// Minimal concrete subclass for testing the base class helper +class TestModule : public MeshModule +{ + public: + TestModule() : MeshModule("TestModule") {} + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; } + using MeshModule::currentRequest; + using MeshModule::isMultiHopBroadcastRequest; +}; + +static TestModule *testModule; +static meshtastic_MeshPacket testPacket; + +void setUp(void) +{ + testModule = new TestModule(); + memset(&testPacket, 0, sizeof(testPacket)); + TestModule::currentRequest = &testPacket; +} + +void tearDown(void) +{ + TestModule::currentRequest = NULL; + delete testModule; +} + +// Zero-hop broadcast (hop_limit == hop_start): should be allowed +static void test_zeroHopBroadcast_isAllowed() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 3; // Not yet relayed + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Multi-hop broadcast (hop_limit < hop_start): should be blocked +static void test_multiHopBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 7; + testPacket.hop_limit = 4; // Already relayed 3 hops + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +// Direct message (not broadcast): should always be allowed regardless of hops +static void test_directMessage_isAllowed() +{ + testPacket.to = 0x12345678; // Specific node + testPacket.hop_start = 7; + testPacket.hop_limit = 4; + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Broadcast with hop_limit == 0 (fully relayed): should be blocked +static void test_fullyRelayedBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 0; + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +// No current request: should not crash, should return false +static void test_noCurrentRequest_isAllowed() +{ + TestModule::currentRequest = NULL; + + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Broadcast with hop_start == 0 (legacy or local): should be allowed +static void test_legacyPacket_zeroHopStart_isAllowed() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 0; + testPacket.hop_limit = 0; + + // hop_limit == hop_start, so not multi-hop + TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest()); +} + +// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked +static void test_singleHopRelayedBroadcast_isBlocked() +{ + testPacket.to = NODENUM_BROADCAST; + testPacket.hop_start = 3; + testPacket.hop_limit = 2; + + TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest()); +} + +void setup() +{ + initializeTestEnvironment(); + + UNITY_BEGIN(); + RUN_TEST(test_zeroHopBroadcast_isAllowed); + RUN_TEST(test_multiHopBroadcast_isBlocked); + RUN_TEST(test_directMessage_isAllowed); + RUN_TEST(test_fullyRelayedBroadcast_isBlocked); + RUN_TEST(test_noCurrentRequest_isAllowed); + RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed); + RUN_TEST(test_singleHopRelayedBroadcast_isBlocked); + exit(UNITY_END()); +} + +void loop() {} diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp index a566dabf7..afc8a399a 100644 --- a/test/test_mqtt/MQTT.cpp +++ b/test/test_mqtt/MQTT.cpp @@ -334,7 +334,7 @@ void setUp(void) owner = meshtastic_User{.id = "!12345678"}; myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic localPosition = - meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7}; + meshtastic_Position{.has_latitude_i = true, .latitude_i = 700000000, .has_longitude_i = true, .longitude_i = 300000000}; router = mockRouter = new MockRouter(); service = mockMeshService = new MockMeshService(); diff --git a/test/test_transmit_history/test_main.cpp b/test/test_transmit_history/test_main.cpp new file mode 100644 index 000000000..992668d97 --- /dev/null +++ b/test/test_transmit_history/test_main.cpp @@ -0,0 +1,230 @@ +#include "TestUtil.h" +#include "TransmitHistory.h" +#include +#include + +// Reset the singleton between tests +static void resetTransmitHistory() +{ + if (transmitHistory) { + delete transmitHistory; + transmitHistory = nullptr; + } + transmitHistory = TransmitHistory::getInstance(); +} + +void setUp(void) +{ + resetTransmitHistory(); +} + +void tearDown(void) {} + +static void test_setLastSentToMesh_stores_millis() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + + uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_NOT_EQUAL(0, result); + + // The stored millis value should be very close to current millis() + uint32_t diff = millis() - result; + TEST_ASSERT_LESS_OR_EQUAL(100, diff); // Within 100ms +} + +static void test_set_overwrites_previous_value() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t first = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + testDelay(50); + + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t second = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + // The second value should be newer (larger millis) + TEST_ASSERT_GREATER_THAN(first, second); +} + +// --- Throttle integration --- + +static void test_throttle_blocks_within_interval() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + + // Should be within a 10-minute interval (just set it) + bool withinInterval = Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); +} + +static void test_throttle_allows_after_interval() +{ + // Unknown key returns 0 — throttle should NOT block + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + TEST_ASSERT_EQUAL_UINT32(0, lastMs); + + // When lastMs == 0, the module check `lastMs == 0 || !isWithinTimespan` allows sending + bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 10 * 60 * 1000); + TEST_ASSERT_TRUE(shouldSend); +} + +static void test_throttle_blocks_after_set_then_zero_does_not() +{ + // Set it — now throttle should block + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + uint32_t lastMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + bool shouldSend = (lastMs == 0) || !Throttle::isWithinTimespanMs(lastMs, 60 * 60 * 1000); + TEST_ASSERT_FALSE(shouldSend); // Should be blocked (within 1hr interval) + + // Different key — should allow + uint32_t otherMs = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + bool otherShouldSend = (otherMs == 0) || !Throttle::isWithinTimespanMs(otherMs, 60 * 60 * 1000); + TEST_ASSERT_TRUE(otherShouldSend); +} + +// --- Multiple keys --- + +static void test_multiple_keys_stored_independently() +{ + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + uint32_t nodeInfoInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + testDelay(20); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); + uint32_t positionInitial = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + testDelay(20); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_TELEMETRY_APP); + + uint32_t nodeInfo = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + uint32_t position = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_POSITION_APP); + uint32_t telemetry = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_TELEMETRY_APP); + + // All should be non-zero + TEST_ASSERT_NOT_EQUAL(0, nodeInfo); + TEST_ASSERT_NOT_EQUAL(0, position); + TEST_ASSERT_NOT_EQUAL(0, telemetry); + + // Updating other keys should not overwrite earlier key timestamps + TEST_ASSERT_EQUAL_UINT32(nodeInfoInitial, nodeInfo); + TEST_ASSERT_EQUAL_UINT32(positionInitial, position); +} + +// --- Singleton --- + +static void test_getInstance_returns_same_instance() +{ + TransmitHistory *a = TransmitHistory::getInstance(); + TransmitHistory *b = TransmitHistory::getInstance(); + TEST_ASSERT_EQUAL_PTR(a, b); +} + +static void test_getInstance_creates_global() +{ + if (transmitHistory) { + delete transmitHistory; + transmitHistory = nullptr; + } + TEST_ASSERT_NULL(transmitHistory); + + TransmitHistory::getInstance(); + TEST_ASSERT_NOT_NULL(transmitHistory); +} + +// --- Persistence round-trip (loadFromDisk / saveToDisk) --- + +static void test_save_and_load_round_trip() +{ + // Set some values + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + testDelay(10); + transmitHistory->setLastSentToMesh(meshtastic_PortNum_POSITION_APP); + + uint32_t nodeInfoEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + uint32_t positionEpoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); + + // Force save + transmitHistory->saveToDisk(); + + // Reset and reload + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + // Epoch values should be restored (if RTC was available when set) + uint32_t restoredNodeInfo = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + uint32_t restoredPosition = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_POSITION_APP); + + TEST_ASSERT_EQUAL_UINT32(nodeInfoEpoch, restoredNodeInfo); + TEST_ASSERT_EQUAL_UINT32(positionEpoch, restoredPosition); + + // After loadFromDisk, millis should be seeded (non-zero) for stored entries + uint32_t restoredMillis = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + if (restoredNodeInfo > 0) { + // If epoch was stored, millis should be seeded from load + TEST_ASSERT_NOT_EQUAL(0, restoredMillis); + } +} + +// --- Boot without RTC scenario --- + +static void test_load_seeds_millis_even_without_rtc() +{ + // This tests the critical crash-reboot scenario: + // After loadFromDisk(), even if getTime() returns 0 (no RTC), + // lastMillis should be seeded so throttle blocks immediate re-broadcast. + + transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP); + transmitHistory->saveToDisk(); + + // Simulate reboot: destroy and recreate + delete transmitHistory; + transmitHistory = nullptr; + transmitHistory = TransmitHistory::getInstance(); + transmitHistory->loadFromDisk(); + + // The key insight: after load, getLastSentToMeshMillis should return non-zero + // because loadFromDisk seeds lastMillis[key] = millis() for every loaded entry. + // This ensures throttle works even without RTC. + uint32_t result = transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP); + + uint32_t epoch = transmitHistory->getLastSentToMeshEpoch(meshtastic_PortNum_NODEINFO_APP); + if (epoch > 0) { + // Data was persisted — millis must be seeded + TEST_ASSERT_NOT_EQUAL(0, result); + + // And it should cause throttle to block (treating as "just sent") + bool withinInterval = Throttle::isWithinTimespanMs(result, 10 * 60 * 1000); + TEST_ASSERT_TRUE(withinInterval); + } + // If epoch == 0, RTC wasn't available — no data was saved, so nothing to restore. + // This is expected on platforms without RTC during the very first boot. +} + +void setup() +{ + initializeTestEnvironment(); + + UNITY_BEGIN(); + + RUN_TEST(test_setLastSentToMesh_stores_millis); + RUN_TEST(test_set_overwrites_previous_value); + + RUN_TEST(test_throttle_blocks_within_interval); + RUN_TEST(test_throttle_allows_after_interval); + RUN_TEST(test_throttle_blocks_after_set_then_zero_does_not); + + RUN_TEST(test_multiple_keys_stored_independently); + + // Singleton + RUN_TEST(test_getInstance_returns_same_instance); + RUN_TEST(test_getInstance_creates_global); + + // Persistence + RUN_TEST(test_save_and_load_round_trip); + RUN_TEST(test_load_seeds_millis_even_without_rtc); + + exit(UNITY_END()); +} + +void loop() {} diff --git a/variants/esp32/betafpv_2400_tx_micro/platformio.ini b/variants/esp32/betafpv_2400_tx_micro/platformio.ini index 77a1f7043..068d71011 100644 --- a/variants/esp32/betafpv_2400_tx_micro/platformio.ini +++ b/variants/esp32/betafpv_2400_tx_micro/platformio.ini @@ -16,4 +16,4 @@ upload_speed = 460800 lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32/betafpv_2400_tx_micro/variant.h b/variants/esp32/betafpv_2400_tx_micro/variant.h index 67699e7c8..8e875a3e6 100644 --- a/variants/esp32/betafpv_2400_tx_micro/variant.h +++ b/variants/esp32/betafpv_2400_tx_micro/variant.h @@ -14,7 +14,7 @@ #define LORA_CS 5 #define RF95_FAN_EN 17 -// #define LED_PIN 16 // This is a LED_WS2812 not a standard LED +// This is a LED_WS2812 not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 16 // gpio pin used to send data to the neopixels diff --git a/variants/esp32/betafpv_900_tx_nano/variant.h b/variants/esp32/betafpv_900_tx_nano/variant.h index 7a4ae9190..6bee74d90 100644 --- a/variants/esp32/betafpv_900_tx_nano/variant.h +++ b/variants/esp32/betafpv_900_tx_nano/variant.h @@ -20,7 +20,7 @@ #define LORA_DIO2 #define LORA_DIO3 -#define LED_PIN 16 // green - blue is at 17 +#define LED_POWER 16 // green - blue is at 17 #define BUTTON_PIN 25 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/chatter2/variant.h b/variants/esp32/chatter2/variant.h index 28ce64f91..e91e4dcef 100644 --- a/variants/esp32/chatter2/variant.h +++ b/variants/esp32/chatter2/variant.h @@ -23,8 +23,6 @@ #define SX126X_TXEN RADIOLIB_NC #define SX126X_RXEN RADIOLIB_NC -// Status -// #define LED_PIN 1 // External notification // FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the // app/preferences diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini index 2ddc5a2db..3fdb738fc 100644 --- a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/platformio.ini @@ -10,6 +10,7 @@ build_flags = -D EBYTE_E22 -D EBYTE_E22_900M30S ; Assume Tx power curve is identical to 900M30S as there is no documentation -I variants/esp32/diy/9m2ibr_aprs_lora_tracker + -ULED_BUILTIN build_src_filter = ${esp32_base.build_src_filter} +<../variants/esp32/diy/9m2ibr_aprs_lora_tracker> \ No newline at end of file diff --git a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h index 037933140..1f84fffa1 100644 --- a/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h +++ b/variants/esp32/diy/9m2ibr_aprs_lora_tracker/variant.h @@ -21,8 +21,8 @@ #define BUTTON_PIN 15 // Right side button - if not available, set device.button_gpio to 0 from Meshtastic client // LEDs -#define LED_PIN 13 // Tx LED -#define USER_LED 2 // Rx LED +#define LED_POWER 13 // Tx LED +#define USER_LED 2 // Rx LED // Buzzer #define PIN_BUZZER 33 diff --git a/variants/esp32/diy/hydra/variant.h b/variants/esp32/diy/hydra/variant.h index e5c10e26b..68194f869 100644 --- a/variants/esp32/diy/hydra/variant.h +++ b/variants/esp32/diy/hydra/variant.h @@ -15,7 +15,7 @@ #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) #define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards #define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). -#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) +#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets) // Radio #define USE_SX1262 // E22-900M30S uses SX1262 diff --git a/variants/esp32/diy/v1/variant.h b/variants/esp32/diy/v1/variant.h index 8a2df3f2b..862969af0 100644 --- a/variants/esp32/diy/v1/variant.h +++ b/variants/esp32/diy/v1/variant.h @@ -15,7 +15,7 @@ #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) #define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards #define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). -#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) +#define LED_POWER 2 // add status LED (compatible with core-pcb and DIY targets) #define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module #define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268 diff --git a/variants/esp32/esp32-common.ini b/variants/esp32/esp32-common.ini index bbbcd3cbe..159de8bc3 100644 --- a/variants/esp32/esp32-common.ini +++ b/variants/esp32/esp32-common.ini @@ -5,7 +5,7 @@ custom_esp32_kind = custom_mtjson_part = platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.12.0 + platformio/espressif32@6.13.0 extra_scripts = ${env.extra_scripts} diff --git a/variants/esp32/hackerboxes_esp32_io/variant.h b/variants/esp32/hackerboxes_esp32_io/variant.h index 06f0032ee..42ce2423e 100644 --- a/variants/esp32/hackerboxes_esp32_io/variant.h +++ b/variants/esp32/hackerboxes_esp32_io/variant.h @@ -3,7 +3,7 @@ // HACKBOX LoRa IO Kit // Uses a ESP-32-WROOM and a RA-01SH (SX1262) LoRa Board -#define LED_PIN 2 // LED +#define LED_POWER 2 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32/heltec_v1/variant.h b/variants/esp32/heltec_v1/variant.h index d1338a28e..c4f6577a8 100644 --- a/variants/esp32/heltec_v1/variant.h +++ b/variants/esp32/heltec_v1/variant.h @@ -12,7 +12,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_v2.1/platformio.ini b/variants/esp32/heltec_v2.1/platformio.ini index 1f7caa16f..9fcb2388a 100644 --- a/variants/esp32/heltec_v2.1/platformio.ini +++ b/variants/esp32/heltec_v2.1/platformio.ini @@ -14,3 +14,4 @@ build_flags = ${esp32_base.build_flags} -D HELTEC_V2_1 -I variants/esp32/heltec_v2.1 + -ULED_BUILTIN diff --git a/variants/esp32/heltec_v2.1/variant.h b/variants/esp32/heltec_v2.1/variant.h index 8ebccc54f..e4aeb363d 100644 --- a/variants/esp32/heltec_v2.1/variant.h +++ b/variants/esp32/heltec_v2.1/variant.h @@ -18,7 +18,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_v2/platformio.ini b/variants/esp32/heltec_v2/platformio.ini index 5f15fb321..fc9e05115 100644 --- a/variants/esp32/heltec_v2/platformio.ini +++ b/variants/esp32/heltec_v2/platformio.ini @@ -14,3 +14,4 @@ build_flags = ${esp32_base.build_flags} -D HELTEC_V2_0 -I variants/esp32/heltec_v2 + -ULED_BUILTIN diff --git a/variants/esp32/heltec_v2/variant.h b/variants/esp32/heltec_v2/variant.h index 5c183818b..c35465f81 100644 --- a/variants/esp32/heltec_v2/variant.h +++ b/variants/esp32/heltec_v2/variant.h @@ -13,7 +13,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define USE_RF95 diff --git a/variants/esp32/heltec_wireless_bridge/variant.h b/variants/esp32/heltec_wireless_bridge/variant.h index 5ad16d0e2..82cc83aa8 100644 --- a/variants/esp32/heltec_wireless_bridge/variant.h +++ b/variants/esp32/heltec_wireless_bridge/variant.h @@ -15,7 +15,7 @@ #undef GPS_TX_PIN // Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16 -#define LED_PIN 22 +#define LED_POWER 22 #define WIFI_LED 23 #define BLE_LED 25 diff --git a/variants/esp32/heltec_wsl_v2.1/variant.h b/variants/esp32/heltec_wsl_v2.1/variant.h index 3927a89d6..db374afb6 100644 --- a/variants/esp32/heltec_wsl_v2.1/variant.h +++ b/variants/esp32/heltec_wsl_v2.1/variant.h @@ -1,7 +1,7 @@ #define I2C_SCL SCL #define I2C_SDA SDA -#define LED_PIN LED +#define LED_POWER LED // active low, powers the Battery reader, but no lora antenna boost (?) // #define VEXT_ENABLE Vext diff --git a/variants/esp32/m5stack_coreink/platformio.ini b/variants/esp32/m5stack_coreink/platformio.ini index af1535f59..7393c291f 100644 --- a/variants/esp32/m5stack_coreink/platformio.ini +++ b/variants/esp32/m5stack_coreink/platformio.ini @@ -19,7 +19,7 @@ build_flags = lib_deps = ${esp32_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 lib_ignore = diff --git a/variants/esp32/m5stack_coreink/variant.h b/variants/esp32/m5stack_coreink/variant.h index 9bf45f2ff..84a1e1966 100644 --- a/variants/esp32/m5stack_coreink/variant.h +++ b/variants/esp32/m5stack_coreink/variant.h @@ -11,7 +11,7 @@ // Green LED #define LED_STATE_ON 1 // State when LED is lit -#define LED_PIN 10 +#define LED_POWER 10 // PCF8563 RTC Module #define PCF8563_RTC 0x51 diff --git a/variants/esp32/radiomaster_900_bandit_nano/variant.h b/variants/esp32/radiomaster_900_bandit_nano/variant.h index 1b6bba126..318401f92 100644 --- a/variants/esp32/radiomaster_900_bandit_nano/variant.h +++ b/variants/esp32/radiomaster_900_bandit_nano/variant.h @@ -37,7 +37,7 @@ /* LED PIN setup. */ -#define LED_PIN 15 +#define LED_POWER 15 /* Five way button when using ADC. diff --git a/variants/esp32/rak11200/variant.h b/variants/esp32/rak11200/variant.h index fe7d05676..a38ac83b7 100644 --- a/variants/esp32/rak11200/variant.h +++ b/variants/esp32/rak11200/variant.h @@ -43,7 +43,7 @@ static const uint8_t SCK = 33; #undef GPS_TX_PIN #define GPS_TX_PIN (TX1) -#define LED_PIN LED_BLUE +#define LED_POWER LED_BLUE #define PIN_VBAT WB_A0 #define BATTERY_PIN PIN_VBAT diff --git a/variants/esp32/tbeam/variant.h b/variants/esp32/tbeam/variant.h index 2c1e61c49..cca52cb9a 100644 --- a/variants/esp32/tbeam/variant.h +++ b/variants/esp32/tbeam/variant.h @@ -9,7 +9,7 @@ #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit -#define LED_PIN 4 // Newer tbeams (1.1) have an extra led on GPIO4 +#define LED_POWER 4 // Newer tbeams (1.1) have an extra led on GPIO4 // TTGO uses a common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if // not found then probe for SX1262 @@ -49,7 +49,7 @@ #undef EXT_NOTIFY_OUT #undef LED_STATE_ON -#undef LED_PIN +#undef LED_POWER #define HAS_CST226SE 1 #define HAS_TOUCHSCREEN 1 diff --git a/variants/esp32/tlora_v1/variant.h b/variants/esp32/tlora_v1/variant.h index 83e2c193e..ff9f4a8ef 100644 --- a/variants/esp32/tlora_v1/variant.h +++ b/variants/esp32/tlora_v1/variant.h @@ -5,7 +5,7 @@ #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost #define VEXT_ON_VALUE LOW -#define LED_PIN 2 // If defined we will blink this LED +#define LED_POWER 2 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. diff --git a/variants/esp32/tlora_v1_3/variant.h b/variants/esp32/tlora_v1_3/variant.h index 73cb31f27..2b0395d8a 100644 --- a/variants/esp32/tlora_v1_3/variant.h +++ b/variants/esp32/tlora_v1_3/variant.h @@ -7,7 +7,7 @@ #define RESET_OLED 16 // If defined, this pin will be used to reset the display controller #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 36 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/tlora_v2/variant.h b/variants/esp32/tlora_v2/variant.h index 8a7cf89ec..099fdc2ee 100644 --- a/variants/esp32/tlora_v2/variant.h +++ b/variants/esp32/tlora_v2/variant.h @@ -5,7 +5,7 @@ #define I2C_SCL 22 #define VEXT_ENABLE 21 // active low, powers the oled display and the lora antenna boost -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN \ 0 // If defined, this will be used for user button presses, if your board doesn't have a physical switch, you can wire one // between this pin and ground diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini index dfdbcb152..2ea9bbb50 100644 --- a/variants/esp32/tlora_v2_1_16/platformio.ini +++ b/variants/esp32/tlora_v2_1_16/platformio.ini @@ -22,4 +22,4 @@ build_flags = ${env:tlora-v2-1-1_6.build_flags} -DBUTTON_PIN=0 -DPIN_BUZZER=25 - -DLED_PIN=-1 \ No newline at end of file + -DLED_POWER=-1 \ No newline at end of file diff --git a/variants/esp32/tlora_v2_1_16/variant.h b/variants/esp32/tlora_v2_1_16/variant.h index 9584dd68b..5488fddf4 100644 --- a/variants/esp32/tlora_v2_1_16/variant.h +++ b/variants/esp32/tlora_v2_1_16/variant.h @@ -8,10 +8,10 @@ #define I2C_SDA 21 // I2C pins for this board #define I2C_SCL 22 -#if defined(LED_PIN) && LED_PIN == -1 -#undef LED_PIN +#if defined(LED_POWER) && LED_POWER == -1 +#undef LED_POWER #else -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #endif #define USE_RF95 diff --git a/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini b/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini index a6b9d2254..235ac7007 100644 --- a/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini +++ b/variants/esp32/tlora_v2_1_16_tcxo/platformio.ini @@ -7,4 +7,5 @@ build_flags = -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 -D LORA_TCXO_GPIO=33 -upload_speed = 115200 \ No newline at end of file + -ULED_BUILTIN +upload_speed = 115200 diff --git a/variants/esp32/tlora_v2_1_18/variant.h b/variants/esp32/tlora_v2_1_18/variant.h index efc676992..1ab08c364 100644 --- a/variants/esp32/tlora_v2_1_18/variant.h +++ b/variants/esp32/tlora_v2_1_18/variant.h @@ -6,7 +6,7 @@ #define I2C_SDA 21 // I2C pins for this board #define I2C_SCL 22 -#define LED_PIN 25 // If defined we will blink this LED +#define LED_POWER 25 // If defined we will blink this LED #define BUTTON_PIN 12 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/trackerd/variant.h b/variants/esp32/trackerd/variant.h index 0996e85ac..8071ba99d 100644 --- a/variants/esp32/trackerd/variant.h +++ b/variants/esp32/trackerd/variant.h @@ -8,7 +8,7 @@ #define GPS_RX_PIN 9 #define GPS_TX_PIN 10 -#define LED_PIN 13 // 13 red, 2 blue, 15 red +#define LED_POWER 13 // 13 red, 2 blue, 15 red #define BUTTON_PIN 0 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32/wiphone/variant.h b/variants/esp32/wiphone/variant.h index 619ac622a..5baeb3936 100644 --- a/variants/esp32/wiphone/variant.h +++ b/variants/esp32/wiphone/variant.h @@ -26,7 +26,6 @@ #undef GPS_TX_PIN #define NO_GPS 1 #define HAS_GPS 0 -#define NO_SCREEN #define HAS_SCREEN 0 // Default SPI1 will be mapped to the display diff --git a/variants/esp32c3/ai-c3/variant.h b/variants/esp32c3/ai-c3/variant.h index 6c4f4d38a..a933de76b 100644 --- a/variants/esp32c3/ai-c3/variant.h +++ b/variants/esp32c3/ai-c3/variant.h @@ -4,7 +4,7 @@ #define I2C_SCL SCL #define BUTTON_PIN 9 // BOOT button -#define LED_PIN 30 // RGB LED +#define LED_POWER 30 // RGB LED #define USE_RF95 #define LORA_SCK 4 diff --git a/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h b/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h index 7432a9941..71090fbeb 100644 --- a/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h +++ b/variants/esp32c3/hackerboxes_esp32c3_oled/variant.h @@ -3,7 +3,7 @@ // Hackerboxes LoRa ESP32-C3 OLED Kit // Uses a ESP32-C3 OLED Board and a RA-01SH (SX1262) LoRa Board -#define LED_PIN 8 // LED +#define LED_POWER 8 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32c3/heltec_esp32c3/variant.h b/variants/esp32c3/heltec_esp32c3/variant.h index ca00c43fa..ed2f6f878 100644 --- a/variants/esp32c3/heltec_esp32c3/variant.h +++ b/variants/esp32c3/heltec_esp32c3/variant.h @@ -3,7 +3,7 @@ // LED pin on HT-DEV-ESP_V2 and HT-DEV-ESP_V3 // https://resource.heltec.cn/download/HT-CT62/HT-CT62_Reference_Design.pdf // https://resource.heltec.cn/download/HT-DEV-ESP/HT-DEV-ESP_V3_Sch.pdf -#define LED_PIN 2 // LED +#define LED_POWER 2 // LED #define LED_STATE_ON 1 // State when LED is lit #define HAS_SCREEN 0 diff --git a/variants/esp32c3/heltec_hru_3601/platformio.ini b/variants/esp32c3/heltec_hru_3601/platformio.ini index 8200b6e87..aa62fb4fc 100644 --- a/variants/esp32c3/heltec_hru_3601/platformio.ini +++ b/variants/esp32c3/heltec_hru_3601/platformio.ini @@ -7,4 +7,4 @@ build_flags = -I variants/esp32c3/heltec_hru_3601 lib_deps = ${esp32c3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32c6/m5stack_unitc6l/platformio.ini b/variants/esp32c6/m5stack_unitc6l/platformio.ini index ed26598d2..4c04d0c26 100644 --- a/variants/esp32c6/m5stack_unitc6l/platformio.ini +++ b/variants/esp32c6/m5stack_unitc6l/platformio.ini @@ -24,7 +24,7 @@ build_unflags = lib_deps = ${esp32c6_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 # renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino h2zero/NimBLE-Arduino@2.3.7 build_flags = @@ -43,4 +43,4 @@ lib_ignore = NonBlockingRTTTL libpax build_src_filter = - ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> \ No newline at end of file + ${esp32c6_base.build_src_filter} +<../variants/esp32c6/m5stack_unitc6l> diff --git a/variants/esp32c6/tlora_c6/variant.h b/variants/esp32c6/tlora_c6/variant.h index 4a0d40232..fcc5b9813 100644 --- a/variants/esp32c6/tlora_c6/variant.h +++ b/variants/esp32c6/tlora_c6/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 8 // I2C pins for this board #define I2C_SCL 9 -#define LED_PIN 7 // If defined we will blink this LED +#define LED_POWER 7 // If defined we will blink this LED #define LED_STATE_ON 0 // State when LED is lit #define USE_SX1262 diff --git a/variants/esp32s2/nugget_s2_lora/variant.h b/variants/esp32s2/nugget_s2_lora/variant.h index 2d123d603..9d88882e5 100644 --- a/variants/esp32s2/nugget_s2_lora/variant.h +++ b/variants/esp32s2/nugget_s2_lora/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 34 // I2C pins for this board #define I2C_SCL 36 -#define LED_PIN 15 // If defined we will blink this LED +#define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected diff --git a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h index 1591f6395..5decc7eb2 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-Hub/variant.h @@ -1,7 +1,7 @@ // EByte EoRA-Hub // Uses E80 (LR1121) LoRa module -#define LED_PIN 35 +#define LED_POWER 35 // Button - user interface #define BUTTON_PIN 0 // BOOT button diff --git a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h index 5da99667b..85321cbe0 100644 --- a/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h +++ b/variants/esp32s3/CDEBYTE_EoRa-S3/variant.h @@ -1,5 +1,5 @@ // LED - status indication -#define LED_PIN 37 +#define LED_POWER 37 // Button - user interface #define BUTTON_PIN 0 // This is the BOOT button, and it has its own pull-up resistor diff --git a/variants/esp32s3/EBYTE_ESP32-S3/variant.h b/variants/esp32s3/EBYTE_ESP32-S3/variant.h index 80fb26434..6dbe6231c 100644 --- a/variants/esp32s3/EBYTE_ESP32-S3/variant.h +++ b/variants/esp32s3/EBYTE_ESP32-S3/variant.h @@ -100,7 +100,7 @@ */ // Status -#define LED_PIN 1 +#define LED_POWER 1 #define LED_STATE_ON 1 // State when LED is lit // External notification // FIXME: Check if EXT_NOTIFY_OUT actualy has any effect and removes the need for setting the external notication pin in the diff --git a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h index ff4f883fe..8768fd70e 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M2/variant.h @@ -1,5 +1,5 @@ // Status -#define LED_PIN 1 +#define LED_POWER 1 #define PIN_BUTTON1 47 // 功能键 #define PIN_BUTTON2 4 // 电源键 diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini index ee51018d4..9f8c3a871 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini @@ -31,6 +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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=PCA9557-arduino packageName=maxpromer/library/PCA9557-arduino maxpromer/PCA9557-arduino@1.0.0 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp index 4b485a1a3..917341560 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.cpp @@ -1,12 +1,23 @@ #include "variant.h" #include -PCA9557 io(0x18, &Wire); +PCA9557 io(0x18, &Wire1); void earlyInitVariant() { - Wire.begin(48, 47); + Wire1.begin(48, 47); io.pinMode(PCA_PIN_EINK_EN, OUTPUT); io.pinMode(PCA_PIN_POWER_EN, OUTPUT); + io.pinMode(PCA_LED_POWER, OUTPUT); + io.pinMode(PCA_LED_USER, OUTPUT); + io.pinMode(PCA_LED_ENABLE, OUTPUT); + io.digitalWrite(PCA_PIN_POWER_EN, HIGH); + io.digitalWrite(PCA_LED_USER, LOW); + io.digitalWrite(PCA_LED_ENABLE, LOW); +} + +void variant_shutdown() +{ + io.digitalWrite(PCA_PIN_POWER_EN, LOW); } diff --git a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h index 353741d91..223f60264 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h +++ b/variants/esp32s3/ELECROW-ThinkNode-M5/variant.h @@ -8,8 +8,10 @@ // LED // Both of these are on the GPIO expander -#define PCA_LED_USER 1 // the Blue LED -#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. +#define PCA_LED_USER 1 // the Blue LED +#define PCA_LED_ENABLE 2 // the power supply to the LEDs, in an OR arrangement with VBUS power +#define PCA_LED_POWER 3 // the Red LED? Seems to have hardware logic to blink when USB is plugged in. +#define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING // USB_CHECK #define EXT_PWR_DETECT 12 @@ -30,6 +32,9 @@ #define I2C_SCL 1 #define I2C_SDA 2 +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + // GPS pins #define GPS_SWITH 10 #define HAS_GPS 1 diff --git a/variants/esp32s3/bpi_picow_esp32_s3/variant.h b/variants/esp32s3/bpi_picow_esp32_s3/variant.h index d8d9413d7..d3e573645 100644 --- a/variants/esp32s3/bpi_picow_esp32_s3/variant.h +++ b/variants/esp32s3/bpi_picow_esp32_s3/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 12 #define I2C_SCL 14 -#define LED_PIN 46 +#define LED_POWER 46 #define LED_STATE_ON 0 // State when LED is litted // #define BUTTON_PIN 15 // Pico OLED 1.3 User key 0 - removed User key 1 (17) diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini index 7a0bd31b4..7e37a0eb4 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/platformio.ini @@ -26,7 +26,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-4-epaper] extends = esp32s3_base @@ -56,7 +56,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:crowpanel-esp32s3-2-epaper] extends = esp32s3_base @@ -86,4 +86,4 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip diff --git a/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h b/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h index 360e33481..c9200b96b 100644 --- a/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h +++ b/variants/esp32s3/crowpanel-esp32s3-5-epaper/variant.h @@ -26,7 +26,7 @@ // #define GPS_RX_PIN 44 // #define GPS_TX_PIN 43 -#define LED_PIN 41 +#define LED_POWER 41 #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini index f44f26006..9230e08c5 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/platformio.ini @@ -11,9 +11,9 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h b/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h index 024f912dd..54db932ea 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h +++ b/variants/esp32s3/diy/my_esp32s3_diy_eink/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 18 // 1 // I2C pins for this board #define I2C_SCL 17 // 2 -// #define LED_PIN 38 // This is a RGB LED not a standard LED +// #define LED_POWER 38 // This is a RGB LED not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini index 4f759d1e6..c53831c9f 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/platformio.ini @@ -11,7 +11,7 @@ upload_speed = 921600 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 build_unflags = ${esp32s3_base.build_unflags} -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h b/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h index 8a3a39003..20e058058 100644 --- a/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h +++ b/variants/esp32s3/diy/my_esp32s3_diy_oled/variant.h @@ -11,7 +11,7 @@ #define I2C_SDA 18 // 1 // I2C pins for this board #define I2C_SCL 17 // 2 -// #define LED_PIN 38 // This is a RGB LED not a standard LED +// #define LED_POWER 38 // This is a RGB LED not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected #define NEOPIXEL_DATA 38 // gpio pin used to send data to the neopixels diff --git a/variants/esp32s3/dreamcatcher/variant.h b/variants/esp32s3/dreamcatcher/variant.h index 7835979e1..963aff477 100644 --- a/variants/esp32s3/dreamcatcher/variant.h +++ b/variants/esp32s3/dreamcatcher/variant.h @@ -6,7 +6,7 @@ #define I2C_SDA1 45 #define I2C_SCL1 46 -#define LED_PIN 6 +#define LED_POWER 6 #define LED_STATE_ON 1 #define BUTTON_PIN 0 diff --git a/variants/esp32s3/esp32-s3-pico/platformio.ini b/variants/esp32s3/esp32-s3-pico/platformio.ini index db0c038e6..aad29247b 100644 --- a/variants/esp32s3/esp32-s3-pico/platformio.ini +++ b/variants/esp32s3/esp32-s3-pico/platformio.ini @@ -23,6 +23,6 @@ build_flags = ${esp32s3_base.build_flags} lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32s3/esp32-s3-pico/variant.h b/variants/esp32s3/esp32-s3-pico/variant.h index bfcb6059d..65732171a 100644 --- a/variants/esp32s3/esp32-s3-pico/variant.h +++ b/variants/esp32s3/esp32-s3-pico/variant.h @@ -8,7 +8,6 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 0 // 17 -// #define LED_PIN PIN_LED // Board has RGB LED 21 #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 1 // How many neopixels are connected diff --git a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h index b30b7fc3e..3ee5545a8 100644 --- a/variants/esp32s3/heltec_capsule_sensor_v3/variant.h +++ b/variants/esp32s3/heltec_capsule_sensor_v3/variant.h @@ -1,5 +1,5 @@ -#define LED_PIN 33 -#define LED_PIN2 34 +#define LED_POWER 33 +#define LED_POWER2 34 #define EXT_PWR_DETECT 35 #define BUTTON_PIN 18 diff --git a/variants/esp32s3/heltec_sensor_hub/platformio.ini b/variants/esp32s3/heltec_sensor_hub/platformio.ini index 3f93c7ad2..013280db1 100644 --- a/variants/esp32s3/heltec_sensor_hub/platformio.ini +++ b/variants/esp32s3/heltec_sensor_hub/platformio.ini @@ -11,4 +11,4 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 diff --git a/variants/esp32s3/heltec_v3/variant.h b/variants/esp32s3/heltec_v3/variant.h index d760c3b7f..d2d904d9c 100644 --- a/variants/esp32s3/heltec_v3/variant.h +++ b/variants/esp32s3/heltec_v3/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN LED +#define LED_POWER LED #define USE_SSD1306 // Heltec_v3 has a SSD1306 display diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 20b3b4606..72c53ded0 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -26,7 +26,7 @@ build_flags = ${heltec_v4_base.build_flags} -D HELTEC_V4_OLED -D USE_SSD1306 ; Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver) - -D LED_PIN=35 + -D LED_POWER=35 -D RESET_OLED=21 -D I2C_SDA=17 -D I2C_SCL=18 @@ -124,11 +124,8 @@ build_flags = -D SCREEN_TOUCH_RST=TOUCH_RST_PIN lib_deps = ${heltec_v4_base.lib_deps} - ; ${device-ui_base.lib_deps} + ${device-ui_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 # 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 + https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip \ No newline at end of file diff --git a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h index 1b1291424..fb0744bc3 100644 --- a/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e213/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -87,6 +88,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -114,4 +116,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_vision_master_e213/platformio.ini b/variants/esp32s3/heltec_vision_master_e213/platformio.ini index 4ace5a45a..1c4c69afe 100644 --- a/variants/esp32s3/heltec_vision_master_e213/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e213/platformio.ini @@ -29,7 +29,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e213-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e213/variant.h b/variants/esp32s3/heltec_vision_master_e213/variant.h index 60f4e00cc..c9aaa2ee8 100644 --- a/variants/esp32s3/heltec_vision_master_e213/variant.h +++ b/variants/esp32s3/heltec_vision_master_e213/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 45 // LED is not populated on earliest board variant +#define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event diff --git a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h index 61b08c740..a90500b15 100644 --- a/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/esp32s3/heltec_vision_master_e290/nicheGraphics.h @@ -24,6 +24,7 @@ Different NicheGraphics UIs and different hardware variants will each have their // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -84,6 +85,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -111,4 +113,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_vision_master_e290/platformio.ini b/variants/esp32s3/heltec_vision_master_e290/platformio.ini index e86746b67..5affd24de 100644 --- a/variants/esp32s3/heltec_vision_master_e290/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_e290/platformio.ini @@ -32,7 +32,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-vision-master-e290-inkhud] diff --git a/variants/esp32s3/heltec_vision_master_e290/variant.h b/variants/esp32s3/heltec_vision_master_e290/variant.h index d7bae7dc2..b32715e39 100644 --- a/variants/esp32s3/heltec_vision_master_e290/variant.h +++ b/variants/esp32s3/heltec_vision_master_e290/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 45 // LED is not populated on earliest board variant +#define LED_POWER 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 #define PIN_BUTTON2 21 // Second built-in button #define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event diff --git a/variants/esp32s3/heltec_vision_master_t190/platformio.ini b/variants/esp32s3/heltec_vision_master_t190/platformio.ini index bbc518b39..44e8b2f2d 100644 --- a/variants/esp32s3/heltec_vision_master_t190/platformio.ini +++ b/variants/esp32s3/heltec_vision_master_t190/platformio.ini @@ -20,5 +20,5 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip upload_speed = 921600 diff --git a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h index 445b57714..9e84a541e 100644 --- a/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h +++ b/variants/esp32s3/heltec_wireless_paper/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -87,6 +88,7 @@ void setupNicheGraphics() inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); @@ -107,4 +109,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/heltec_wireless_paper/platformio.ini b/variants/esp32s3/heltec_wireless_paper/platformio.ini index 673c834ea..ce4bed30e 100644 --- a/variants/esp32s3/heltec_wireless_paper/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper/platformio.ini @@ -29,7 +29,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 [env:heltec-wireless-paper-inkhud] diff --git a/variants/esp32s3/heltec_wireless_paper/variant.h b/variants/esp32s3/heltec_wireless_paper/variant.h index bbfd54ada..7f57bb67f 100644 --- a/variants/esp32s3/heltec_wireless_paper/variant.h +++ b/variants/esp32s3/heltec_wireless_paper/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define BUTTON_PIN 0 // I2C diff --git a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini index 8543e414f..b34adfb17 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini +++ b/variants/esp32s3/heltec_wireless_paper_v1/platformio.ini @@ -26,5 +26,5 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip upload_speed = 115200 diff --git a/variants/esp32s3/heltec_wireless_paper_v1/variant.h b/variants/esp32s3/heltec_wireless_paper_v1/variant.h index 4505395c9..59dd485f6 100644 --- a/variants/esp32s3/heltec_wireless_paper_v1/variant.h +++ b/variants/esp32s3/heltec_wireless_paper_v1/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define BUTTON_PIN 0 // I2C diff --git a/variants/esp32s3/heltec_wireless_tracker/variant.h b/variants/esp32s3/heltec_wireless_tracker/variant.h index 3b19f5afd..b40e40011 100644 --- a/variants/esp32s3/heltec_wireless_tracker/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define _VARIANT_HELTEC_WIRELESS_TRACKER #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h index df5ab4716..e7d3f93c1 100644 --- a/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_V1_0/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index a5489173d..0ca2dfc03 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define _VARIANT_HELTEC_WIRELESS_TRACKER diff --git a/variants/esp32s3/heltec_wsl_v3/variant.h b/variants/esp32s3/heltec_wsl_v3/variant.h index c103b9172..c81f45d3b 100644 --- a/variants/esp32s3/heltec_wsl_v3/variant.h +++ b/variants/esp32s3/heltec_wsl_v3/variant.h @@ -1,7 +1,7 @@ #define I2C_SCL SCL #define I2C_SDA SDA -#define LED_PIN LED +#define LED_POWER LED #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/mesh-tab/variant.h b/variants/esp32s3/mesh-tab/variant.h index 99204bba3..30042b90f 100644 --- a/variants/esp32s3/mesh-tab/variant.h +++ b/variants/esp32s3/mesh-tab/variant.h @@ -13,7 +13,7 @@ #define ADC_CHANNEL ADC1_GPIO4_CHANNEL // LED -#define LED_PIN 21 +#define LED_POWER 21 // Button #define BUTTON_PIN 0 diff --git a/variants/esp32s3/mini-epaper-s3/pins_arduino.h b/variants/esp32s3/mini-epaper-s3/pins_arduino.h new file mode 100644 index 000000000..a4b3c4bf7 --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 18; +static const uint8_t SCL = 9; + +// Default SPI will be mapped to Radio +static const uint8_t SS = -1; +static const uint8_t MOSI = 17; +static const uint8_t MISO = 6; +static const uint8_t SCK = 8; + +#define SPI_MOSI (39) +#define SPI_SCK (41) +#define SPI_MISO (38) +#define SPI_CS (40) + +#define SDCARD_CS SPI_CS + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/mini-epaper-s3/platformio.ini b/variants/esp32s3/mini-epaper-s3/platformio.ini new file mode 100644 index 000000000..f49be707f --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/platformio.ini @@ -0,0 +1,31 @@ +[env:mini-epaper-s3] +;custom_meshtastic_hw_model = +custom_meshtastic_hw_model_slug = MINI_EPAPER_S3 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO Mini ePaper S3 E-Ink +custom_meshtastic_images = mini-epaper-s3.svg +custom_meshtastic_tags = LilyGo +custom_meshtastic_requires_dfu = no + +extends = esp32s3_base +board = mini-epaper-s3 +board_check = true +upload_protocol = esptool + +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/mini-epaper-s3 + -DMINI_EPAPER_S3 + -DUSE_EINK + -DEINK_DISPLAY_MODEL=GxEPD2_102 + -DEINK_WIDTH=128 + -DEINK_HEIGHT=80 + +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/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.3.4 diff --git a/variants/esp32s3/mini-epaper-s3/variant.h b/variants/esp32s3/mini-epaper-s3/variant.h new file mode 100644 index 000000000..b464c9b4a --- /dev/null +++ b/variants/esp32s3/mini-epaper-s3/variant.h @@ -0,0 +1,57 @@ +// Display (E-Ink) + +#define PIN_EINK_CS 13 +#define PIN_EINK_BUSY 10 +#define PIN_EINK_RES 11 +#define PIN_EINK_SCLK 14 +#define PIN_EINK_MOSI 15 +#define PIN_EINK_DC 12 +#define PIN_EINK_EN 42 + +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI1_MISO -1 +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK +#define DISPLAY_FORCE_SMALL_FONTS + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define BATTERY_PIN 2 // A battery voltage measurement pin, voltage divider connected here to +// measure battery voltage ratio of voltage divider = 2.0 (assumption) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO2_CHANNEL + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define BUTTON_PIN 3 +#define BUTTON_NEED_PULLUP +#define ALT_BUTTON_PIN 4 +#define ALT_BUTTON_ACTIVE_LOW true +#define ALT_BUTTON_ACTIVE_PULLUP true +#define PIN_BUTTON3 0 + +// #define HAS_SDCARD 1 +// #define SDCARD_USE_SOFT_SPI + +// PCF85063 RTC Module +#define PCF85063_RTC 0x51 +#define HAS_RTC 1 + +#define USE_SX1262 +#define LORA_DIO1 5 +#define LORA_SCK 8 +#define LORA_MISO 6 +#define LORA_MOSI 17 +#define LORA_CS 7 // CS not connected; IO7 is free +#define LORA_RESET 21 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY 16 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/variants/esp32s3/nibble_esp32/variant.h b/variants/esp32s3/nibble_esp32/variant.h index 8ffbd9d59..8d75a4fbf 100644 --- a/variants/esp32s3/nibble_esp32/variant.h +++ b/variants/esp32s3/nibble_esp32/variant.h @@ -1,7 +1,7 @@ #define I2C_SDA 11 // I2C pins for this board #define I2C_SCL 10 -#define LED_PIN 1 // If defined we will blink this LED +#define LED_POWER 1 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/nugget_s3_lora/variant.h b/variants/esp32s3/nugget_s3_lora/variant.h index 1354d0837..633ed27f6 100644 --- a/variants/esp32s3/nugget_s3_lora/variant.h +++ b/variants/esp32s3/nugget_s3_lora/variant.h @@ -4,7 +4,7 @@ #define USE_SSD1306 #define DISPLAY_FLIP_SCREEN -#define LED_PIN 15 // If defined we will blink this LED +#define LED_POWER 15 // If defined we will blink this LED #define HAS_NEOPIXEL // Enable the use of neopixels #define NEOPIXEL_COUNT 3 // How many neopixels are connected diff --git a/variants/esp32s3/rak3312/variant.h b/variants/esp32s3/rak3312/variant.h index 6431f1fd0..ee0fff524 100644 --- a/variants/esp32s3/rak3312/variant.h +++ b/variants/esp32s3/rak3312/variant.h @@ -24,7 +24,7 @@ #define PIN_LED1 LED_GREEN #define LED_NOTIFICATION LED_BLUE -#define LED_PIN LED_GREEN +#define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted diff --git a/variants/esp32s3/rak_wismesh_tap_v2/variant.h b/variants/esp32s3/rak_wismesh_tap_v2/variant.h index 7d263165c..90cb12053 100644 --- a/variants/esp32s3/rak_wismesh_tap_v2/variant.h +++ b/variants/esp32s3/rak_wismesh_tap_v2/variant.h @@ -32,7 +32,7 @@ #define PIN_LED1 LED_GREEN #define LED_NOTIFICATION LED_BLUE -#define LED_PIN LED_GREEN +#define LED_POWER LED_GREEN #define ledOff(pin) pinMode(pin, INPUT) #define LED_STATE_ON 1 // State when LED is litted diff --git a/variants/esp32s3/seeed_xiao_s3/variant.h b/variants/esp32s3/seeed_xiao_s3/variant.h index d8dcbc8d4..11bf48521 100644 --- a/variants/esp32s3/seeed_xiao_s3/variant.h +++ b/variants/esp32s3/seeed_xiao_s3/variant.h @@ -30,7 +30,7 @@ Expansion Board Infomation : https://www.seeedstudio.com/Seeeduino-XIAO-Expansio L76K GPS Module Information : https://www.seeedstudio.com/L76K-GNSS-Module-for-Seeed-Studio-XIAO-p-5864.html */ -#define LED_PIN 48 +#define LED_POWER 48 #define LED_STATE_ON 1 // State when LED is lit #define BUTTON_PIN 21 // This is the Program Button diff --git a/variants/esp32s3/station-g2/platformio.ini b/variants/esp32s3/station-g2/platformio.ini index 091b35f00..4efb21a00 100755 --- a/variants/esp32s3/station-g2/platformio.ini +++ b/variants/esp32s3/station-g2/platformio.ini @@ -21,11 +21,11 @@ upload_protocol = esptool upload_speed = 921600 build_unflags = ${esp32s3_base.build_unflags} - -DARDUINO_USB_MODE=1 + -DARDUINO_USB_MODE=0 build_flags = ${esp32s3_base.build_flags} -D STATION_G2 -I variants/esp32s3/station-g2 -DBOARD_HAS_PSRAM -DSTATION_G2 - -DARDUINO_USB_MODE=0 + -DARDUINO_USB_MODE=1 diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h index dbe1620e2..52e99320e 100644 --- a/variants/esp32s3/t-beam-1w/variant.h +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -67,7 +67,7 @@ #endif // LED -#define LED_PIN 18 +#define LED_POWER 18 #define LED_STATE_ON 1 // HIGH = ON // Battery ADC @@ -76,6 +76,8 @@ #define BATTERY_SENSE_SAMPLES 30 #define ADC_MULTIPLIER 2.9333 +#define OCV_ARRAY 7950, 7850, 7750, 7580, 7440, 7310, 7150, 7005, 6860, 6685, 6000 + // NTC temperature sensor #define NTC_PIN 14 diff --git a/variants/esp32s3/t-deck-pro/platformio.ini b/variants/esp32s3/t-deck-pro/platformio.ini index 5ba82d045..29960f280 100644 --- a/variants/esp32s3/t-deck-pro/platformio.ini +++ b/variants/esp32s3/t-deck-pro/platformio.ini @@ -33,7 +33,7 @@ build_flags = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=git-refs depName=CSE_Touch packageName=https://github.com/CIRCUITSTATE/CSE_Touch gitBranch=main https://github.com/CIRCUITSTATE/CSE_Touch/archive/b44f23b6f870b848f1fbe453c190879bc6cfaafa.zip # renovate: datasource=github-tags depName=CSE_CST328 packageName=CIRCUITSTATE/CSE_CST328 diff --git a/variants/esp32s3/t-eth-elite/variant.h b/variants/esp32s3/t-eth-elite/variant.h index b7ac05872..8f2748c36 100644 --- a/variants/esp32s3/t-eth-elite/variant.h +++ b/variants/esp32s3/t-eth-elite/variant.h @@ -12,7 +12,7 @@ #define HAS_SCREEN 1 // Allow for OLED Screens on I2C Header of shield -#define LED_PIN 38 // If defined we will blink this LED +#define LED_POWER 38 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h index 8f5e63653..73cc2e235 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h +++ b/variants/esp32s3/tlora_t3s3_epaper/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0 @@ -86,4 +88,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini index 256cdc0d0..f9d1ea7db 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/platformio.ini +++ b/variants/esp32s3/tlora_t3s3_epaper/platformio.ini @@ -31,7 +31,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:tlora-t3s3-epaper-inkhud] extends = esp32s3_base, inkhud diff --git a/variants/esp32s3/tlora_t3s3_epaper/variant.h b/variants/esp32s3/tlora_t3s3_epaper/variant.h index 1ed505420..0f4875fc4 100644 --- a/variants/esp32s3/tlora_t3s3_epaper/variant.h +++ b/variants/esp32s3/tlora_t3s3_epaper/variant.h @@ -22,7 +22,7 @@ #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 -#define LED_PIN 37 +#define LED_POWER 37 #define BUTTON_PIN 0 #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/tlora_t3s3_v1/variant.h b/variants/esp32s3/tlora_t3s3_v1/variant.h index babe44a58..02e2a0e42 100644 --- a/variants/esp32s3/tlora_t3s3_v1/variant.h +++ b/variants/esp32s3/tlora_t3s3_v1/variant.h @@ -14,7 +14,7 @@ #define I2C_SDA1 43 #define I2C_SCL1 44 -#define LED_PIN 37 // If defined we will blink this LED +#define LED_POWER 37 // If defined we will blink this LED #define BUTTON_PIN 0 // If defined, this will be used for user button presses, #define BUTTON_NEED_PULLUP diff --git a/variants/esp32s3/tracksenger/internal/variant.h b/variants/esp32s3/tracksenger/internal/variant.h index ba3e281c8..f9a20c901 100644 --- a/variants/esp32s3/tracksenger/internal/variant.h +++ b/variants/esp32s3/tracksenger/internal/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/tracksenger/lcd/variant.h b/variants/esp32s3/tracksenger/lcd/variant.h index a9bb89d68..029f7753b 100644 --- a/variants/esp32s3/tracksenger/lcd/variant.h +++ b/variants/esp32s3/tracksenger/lcd/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/tracksenger/oled/variant.h b/variants/esp32s3/tracksenger/oled/variant.h index 689864b32..1f1fbbaa1 100644 --- a/variants/esp32s3/tracksenger/oled/variant.h +++ b/variants/esp32s3/tracksenger/oled/variant.h @@ -1,4 +1,4 @@ -#define LED_PIN 18 +#define LED_POWER 18 #define HELTEC_TRACKER_V1_X diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index 924dfa74f..1d4af52f3 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -38,10 +38,10 @@ build_src_filter = lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 - # TODO renovate - https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 + # TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0 + https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip # renovate: datasource=custom.pio depName=NeoPixel packageName=adafruit/library/Adafruit NeoPixel - adafruit/Adafruit NeoPixel@1.15.2 + adafruit/Adafruit NeoPixel@1.15.4 [env:unphone-tft] board_level = extra diff --git a/variants/esp32s3/unphone/variant.h b/variants/esp32s3/unphone/variant.h index 6f0710d62..268eedea5 100644 --- a/variants/esp32s3/unphone/variant.h +++ b/variants/esp32s3/unphone/variant.h @@ -54,7 +54,7 @@ #define SD_SPI_FREQUENCY 25000000 -#define LED_PIN 13 // the red part of the RGB LED +#define LED_POWER 13 // the red part of the RGB LED #define LED_STATE_ON 0 // State when LED is lit #define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index b86420291..17ec59442 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -28,7 +28,7 @@ lib_deps = rweather/Crypto@0.4.0 # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.19 - # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main + ; # 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/23c42319a69cffcb65868e3c72e6bed83974a393.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 @@ -41,6 +41,8 @@ build_flags = ${arduino_base.build_flags} -D ARCH_PORTDUINO -fPIC + -D_FORTIFY_SOURCE=2 + -fstack-protector-all -Wstack-protector --param ssp-buffer-size=4 -Isrc/platform/portduino -DRADIOLIB_EEPROM_UNSUPPORTED -DPORTDUINO_LINUX_HARDWARE diff --git a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini index 093c3732d..e202715b0 100644 --- a/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini +++ b/variants/nrf52840/Dongle_nRF52840-pca10059-v1/platformio.ini @@ -12,5 +12,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/Dongle_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 debug_tool = jlink diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h index f64de9d07..242e5ae49 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -71,13 +72,14 @@ void setupNicheGraphics() // Pick applets // Note: order of applets determines priority of "auto-show" feature - inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown - inkhud->addApplet("DMs", new InkHUD::DMApplet); // - - inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); @@ -115,4 +117,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini index a4687669b..2a6cea73e 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M1/platformio.ini @@ -33,7 +33,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ELECROW 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM khoih-prog/nRF52_PWM@1.0.1 ;upload_protocol = fs diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini index 7a87c2a21..b6956c0f1 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/platformio.ini @@ -24,5 +24,5 @@ lib_deps = ${nrf52840_base.lib_deps} # 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 + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h index 50944b6d7..fa127ae3e 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M3/variant.h @@ -52,7 +52,6 @@ extern "C" { // LED #define LED_RED 33 #define LED_POWER LED_RED -#define LED_CHARGE LED_POWER // Signals the Status LED Module to handle this LED #define LED_GREEN 35 #define LED_NOTIFICATION LED_GREEN #define LED_BLUE 37 @@ -116,7 +115,6 @@ extern "C" { // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 #ifdef __cplusplus } diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini index 329111ce6..fa09a4463 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/platformio.ini @@ -21,5 +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} - ; # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib + # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp index bc0381a48..a43755c06 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_CHARGE, OUTPUT); - ledOff(LED_CHARGE); - pinMode(LED_PAIRING, OUTPUT); ledOff(LED_PAIRING); diff --git a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h index ba6aa14ab..2ebb79031 100644 --- a/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h +++ b/variants/nrf52840/ELECROW-ThinkNode-M6/variant.h @@ -41,7 +41,7 @@ extern "C" { // LEDs #define LED_BLUE -1 -#define LED_CHARGE (12) +#define LED_POWER (12) #define LED_PAIRING (7) #define LED_NOTIFICATION LED_PAIRING @@ -121,7 +121,6 @@ static const uint8_t A0 = PIN_A0; // PCF8563 RTC Module #define PCF8563_RTC 0x51 -#define HAS_RTC 1 // SPI #define SPI_INTERFACES_COUNT 1 diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp index 35dc1d39b..5972861d6 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD/variant.h b/variants/nrf52840/ME25LS01-4Y10TD/variant.h index a920b02e5..1d1af37ad 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD/variant.h @@ -50,7 +50,7 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini index 5951a37a3..afc8e4b2a 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/platformio.ini @@ -16,7 +16,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/ME25LS0 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 ; 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/variant.cpp b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp index 35dc1d39b..5972861d6 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.cpp @@ -32,9 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); } \ No newline at end of file diff --git a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h index 683669160..a5bb53a33 100644 --- a/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h +++ b/variants/nrf52840/ME25LS01-4Y10TD_e-ink/variant.h @@ -50,7 +50,7 @@ extern "C" { #define PIN_LED1 (32 + 7) // P1.07 Blue D2 -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MS24SF1/variant.h b/variants/nrf52840/MS24SF1/variant.h index a71be99fd..a41b3a350 100644 --- a/variants/nrf52840/MS24SF1/variant.h +++ b/variants/nrf52840/MS24SF1/variant.h @@ -50,7 +50,7 @@ extern "C" { #define PIN_LED1 (-1) -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini index ecb630bb7..3410859b1 100644 --- a/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini +++ b/variants/nrf52840/MakePython_nRF52840_eink/platformio.ini @@ -15,6 +15,6 @@ lib_deps = # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 debug_tool = jlink ;upload_port = /dev/ttyACM4 \ No newline at end of file diff --git a/variants/nrf52840/TWC_mesh_v4/platformio.ini b/variants/nrf52840/TWC_mesh_v4/platformio.ini index d93f179c2..5e5b4b665 100644 --- a/variants/nrf52840/TWC_mesh_v4/platformio.ini +++ b/variants/nrf52840/TWC_mesh_v4/platformio.ini @@ -9,5 +9,5 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/TWC_mes lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 debug_tool = jlink diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h index 8f30a244f..0a01b613e 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -72,7 +73,8 @@ void setupNicheGraphics() inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background @@ -92,4 +94,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md index 194c53434..7fbf83d7c 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/README.md @@ -1,43 +1,21 @@ # XIAO nRF52840 + XIAO Wio SX1262 -For a mere doubling in price you too can swap out the XIAO ESP32C3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! +For a mere doubling in price you too can swap out the XIAO ESP32S3 for a XIAO nRF52840, stack the Wio SX1262 radio board either above or underneath the nRF52840, solder the pins, and achieve a massive improvement in battery life! -I'm not really sure why else you would want to as the ESP32C3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32C3 to use for something else! +I'm not really sure why else you would want to as the ESP32S3 is perfectly cromulent, easily connects to the Wio SX1262 via the B2B connector and has an onboard IPEX connector for the included Bluetooth antenna. So you'll also lose BT range, but you will also have working ADC for the battery in Meshtastic and also have an ESP32S3 to use for something else! If you're still reading you are clearly gonna do it anyway, so...mount the Wio SX1262 either on top or underneath depending on your preference. The `variant.h` will work with either configuration though it does map the Wio SX1262's button to nRF52840 Pin `D5` as it can still be used as a user button and it's nice to be able to gracefully shutdown a node by holding it down for 5 seconds. If you do decide to wire up the button, orient it so looking straight-down at the Wio SX1262 the radio chip is at the bottom, button in the middle and the hole is at the top - the **left** side of the button should be soldered to `GND` (e.g. the 2nd pin down the top on the **right** row of pins) and the **right** side of the button should be soldered to `D5` (e.g. the 2nd pin up from the button on the **left** row of pins.). This mirrors the original wiring and wiring it in reverse could end up connecting GND to voltage and that's no beuno. -Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, The same pins could be repurposed for `i2c` if you would like to have that instead of serial, in `variant.h` you would just need to change: +Serial Pins remain available on `D6` (TX) and `D7` (RX) should you want to use them, and I2C has been mapped to NFC1 (SDA, D30) and NFC2 (SCL, D31) -```c++ -// RX and TX pins -#define PIN_SERIAL1_RX (6) -#define PIN_SERIAL1_TX (7) +The same pins could be reordered if you would like to have a different arrangement, in `variant.h` you would just need to change the relevant lines: + +```cpp +#define GPS_TX_PIN D6 // This is data from the MCU +#define GPS_RX_PIN D7 // This is data from the GNSS module + +#define PIN_WIRE_SDA D6 +#define PIN_WIRE_SCL D7 ``` - -to - -```c++ -// RX and TX pins -#define PIN_SERIAL1_RX (-1) -#define PIN_SERIAL1_TX (-1) -``` - -and - -```c++ -#define PIN_WIRE_SDA (-1) -#define PIN_WIRE_SCL (-1) -// #define PIN_WIRE_SDA (6) -// #define PIN_WIRE_SCL (7) -``` - -to - -```c++ -#define PIN_WIRE_SDA (6) -#define PIN_WIRE_SCL (7) -``` - -If you wanted both serial and i2c you could even go so far as to use the pads for the PDM mic which is missing on the non-sense board (`P1.00` / `P0.16`)... or move up to the nRF52840 Plus which has even more pins available but hasn't been checked/confirmed if it follows the same pin mapping as the non-plus. 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 10eab2aa4..81076bd55 100644 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini +++ b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/platformio.ini @@ -1,13 +1,17 @@ -; Seeed XIAO nRF52840 + XIAO Wio SX1262 DIY -[env:seeed-xiao-nrf52840-wio-sx1262] -board = xiao_ble_sense -extends = nrf52840_base +; Seeed Xiao BLE but using the B2B from ESP32S3 variant +[env:seeed_xiao_nrf52840_btb] +extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${nrf52840_base.build_flags} - -Ivariants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262 - -D PRIVATE_HW + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DSEEED_XIAO_NRF_WIO_BTB ; Define Seeed XIAO nRF Wio B2B + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define 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> +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> 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/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp deleted file mode 100644 index 300f69d0b..000000000 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // D0 .. D13 - 2, // D0 is P0.02 (A0) - 3, // D1 is P0.03 (A1) - 28, // D2 is P0.28 (A2) - 29, // D3 is P0.29 (A3) - 4, // D4 is P0.04 (A4,SDA) - 5, // D5 is P0.05 (A5,SCL) - 43, // D6 is P1.11 (TX) - 44, // D7 is P1.12 (RX) - 45, // D8 is P1.13 (SCK) - 46, // D9 is P1.14 (MISO) - 47, // D10 is P1.15 (MOSI) - - // LEDs - 26, // D11 is P0.26 (LED RED) - 6, // D12 is P0.06 (LED BLUE) - 30, // D13 is P0.30 (LED GREEN) - 14, // D14 is P0.14 (READ_BAT) - - // LSM6DS3TR - 40, // D15 is P1.08 (6D_PWR) - 27, // D16 is P0.27 (6D_I2C_SCL) - 7, // D17 is P0.07 (6D_I2C_SDA) - 11, // D18 is P0.11 (6D_INT1) - - // MIC - 42, // 17,//42, // D19 is P1.10 (MIC_PWR) - 32, // 26,//32, // D20 is P1.00 (PDM_CLK) - 16, // 25,//16, // D21 is P0.16 (PDM_DATA) - - // BQ25100 - 13, // D22 is P0.13 (HICHG) - 17, // D23 is P0.17 (~CHG) - - // - 21, // D24 is P0.21 (QSPI_SCK) - 25, // D25 is P0.25 (QSPI_CSN) - 20, // D26 is P0.20 (QSPI_SIO_0 DI) - 24, // D27 is P0.24 (QSPI_SIO_1 DO) - 22, // D28 is P0.22 (QSPI_SIO_2 WP) - 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) - - // NFC - 9, // D30 is P0.09 (NFC1) - 10, // D31 is P0.10 (NFC2) - - // VBAT - 31, // D32 is P0.10 (VBAT) -}; - -void initVariant() -{ - // Set BQ25101 ISET to 100mA instead of 50mA - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); -} diff --git a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h deleted file mode 100644 index 6927f1295..000000000 --- a/variants/nrf52840/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h +++ /dev/null @@ -1,185 +0,0 @@ -// basically xiao_ble with pins remapped for: -// Seeed XIAO nRF52840 : https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html -// Seeed Wio SX1626 : https://www.seeedstudio.com/Wio-SX1262-with-XIAO-ESP32S3-p-5982.html - -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ - -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -#define USE_LFXO // Board uses 32khz crystal for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -#define PINS_COUNT (33) -#define NUM_DIGITAL_PINS (33) -#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference -#define NUM_ANALOG_OUTPUTS (0) - -// LEDs -// ---- -#define LED_RED 11 -#define LED_BLUE 12 -#define LED_GREEN 13 - -#define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE -#define PIN_LED3 LED_RED - -#define PIN_LED PIN_LED1 - -#define LED_STATE_ON 1 // State when LED is lit - -// XIAO Wio-SX1262 Shield User button -#define PIN_BUTTON1 5 -#define BUTTON_NEED_PULLUP - -// Digital Pins -// ------------ -#define D0 (0ul) -#define D1 (1ul) -#define D2 (2ul) -#define D3 (3ul) -#define D4 (4ul) -#define D5 (5ul) -#define D6 (6ul) -#define D7 (7ul) -#define D8 (8ul) -#define D9 (9ul) -#define D10 (10ul) - -// Analog Pins -// ----------- -#define PIN_A0 (0) -#define PIN_A1 (1) -#define PIN_A2 (2) -#define PIN_A3 (3) -#define PIN_A4 (4) -#define PIN_A5 (5) -#define PIN_VBAT (32) -#define VBAT_ENABLE (14) - -static const uint8_t A0 = PIN_A0; -static const uint8_t A1 = PIN_A1; -static const uint8_t A2 = PIN_A2; -static const uint8_t A3 = PIN_A3; -static const uint8_t A4 = PIN_A4; -static const uint8_t A5 = PIN_A5; -#define ADC_RESOLUTION 12 - -// Other Pins -// ---------- -#define PIN_NFC1 (30) -#define PIN_NFC2 (31) - -// RX and TX pins -#define PIN_SERIAL1_RX (-1) -#define PIN_SERIAL1_TX (-1) -// complains if not defined -#define PIN_SERIAL2_RX (-1) -#define PIN_SERIAL2_TX (-1) - -// 4 is used as RF_SW and 5 for USR button so... -#define PIN_WIRE_SDA (6) -#define PIN_WIRE_SCL (7) - -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; - -// SPI SX1262 -// ---------- -#define SPI_SX1262 -#ifdef SPI_SX1262 -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D3; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -// supported modules list -#define USE_SX1262 - -// common pinouts for SX126X modules -#define SX126X_CS D3 -#define SX126X_DIO1 D0 -#define SX126X_BUSY D1 -#define SX126X_RESET D2 - -// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_RXEN 38 -#define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#endif - -// Wire Interfaces -// ------------------- -#define WIRE_INTERFACES_COUNT 1 // 2 - -// Sense version has IMU and PDM Mic -// #define XIAO_SENSE -#ifndef XIAO_SENSE -// 6 DoF IMU -#define PIN_LSM6DS3TR_C_POWER (15) -#define PIN_LSM6DS3TR_C_INT1 (18) -// PDM Interfaces -// --------------- -#define PIN_PDM_PWR (19) -#define PIN_PDM_CLK (20) -#define PIN_PDM_DIN (21) -#endif - -// QSPI Pins -// --------- -#define PIN_QSPI_SCK (24) -#define PIN_QSPI_CS (25) -#define PIN_QSPI_IO0 (26) -#define PIN_QSPI_IO1 (27) -#define PIN_QSPI_IO2 (28) -#define PIN_QSPI_IO3 (29) - -// On-board QSPI Flash -// ------------------- -#define EXTERNAL_FLASH_DEVICES P25Q16H -#define EXTERNAL_FLASH_USE_QSPI - -// Battery -// ------- -// P0_14 = 14 Reads battery voltage from divider on signal board. -// PIN_VBAT is reading voltage divider on XIAO and is program pin 32 / or P0.31 -#define ADC_CTRL VBAT_ENABLE -#define ADC_CTRL_ENABLED LOW -#define BATTERY_SENSE_RESOLUTION_BITS 10 -#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED -#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge - -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_VBAT // PIN_A0 - -// ratio of voltage divider = 3.0 (R17=1M, R18=510k) -#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - -#endif \ No newline at end of file diff --git a/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini b/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini index a5d0aaf8f..c923bbdb7 100644 --- a/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini +++ b/variants/nrf52840/diy/seeed_xiao_nrf52840_e22/platformio.ini @@ -6,7 +6,8 @@ build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M30S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define ; Seeed XIAO nRF52840 + EBYTE E22-900M33S - Pinout matching Wio-SX1262 (SKU 113010003) [env:seeed_xiao_nrf52840_e22_900m33s] @@ -16,4 +17,5 @@ build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DEBYTE_E22 -DEBYTE_E22_900M33S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define \ No newline at end of file diff --git a/variants/nrf52840/diy/xiao_ble/platformio.ini b/variants/nrf52840/diy/xiao_ble/platformio.ini index 6c764ea78..42f53f1bf 100644 --- a/variants/nrf52840/diy/xiao_ble/platformio.ini +++ b/variants/nrf52840/diy/xiao_ble/platformio.ini @@ -2,9 +2,55 @@ [env:xiao_ble] extends = env:seeed_xiao_nrf52840_kit board_level = extra -build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 -D PRIVATE_HW -DXIAO_BLE_LEGACY_PINOUT -DEBYTE_E22 - -DEBYTE_E22_900M30S -build_unflags = -DGPS_L76K + -USEEED_XIAO_NRF52840_KIT ; remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble_30db] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout + -DEBYTE_E22_900M30S ; Set 30db module + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble_33db] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/seeed_xiao_nrf52840_kit + -Isrc/platform/nrf52/softdevice + -Isrc/platform/nrf52/softdevice/nrf52 + -DPRIVATE_HW ; Define private hardware + -DXIAO_BLE_LEGACY_PINOUT ; Set legacy pinout + -DEBYTE_E22_900M33S ; Set 33db module + -USEEED_XIAO_NRF52840_KIT ; Remove default HWID + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> +debug_tool = jlink +; 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/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h index b6be70ff4..ad17e7457 100644 --- a/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -73,7 +74,8 @@ void setupNicheGraphics() inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background @@ -93,4 +95,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini index a39872205..7cbc5f6a9 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/platformio.ini +++ b/variants/nrf52840/heltec_mesh_node_t114/platformio.ini @@ -23,4 +23,4 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip diff --git a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h index 10f628d56..187022ea7 100644 --- a/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_pocket/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -87,4 +89,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_pocket/platformio.ini b/variants/nrf52840/heltec_mesh_pocket/platformio.ini index 646304a5a..9fbcc890d 100644 --- a/variants/nrf52840/heltec_mesh_pocket/platformio.ini +++ b/variants/nrf52840/heltec_mesh_pocket/platformio.ini @@ -38,7 +38,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud @@ -101,7 +101,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_ 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud diff --git a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h index 125f50590..0f4131916 100644 --- a/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h +++ b/variants/nrf52840/heltec_mesh_solar/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -67,6 +68,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -87,4 +89,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini index 69264f0df..b4964a077 100644 --- a/variants/nrf52840/heltec_mesh_solar/platformio.ini +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -68,7 +68,7 @@ build_flags = ${heltec_mesh_solar_base.build_flags} 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip [env:heltec-mesh-solar-inkhud] extends = heltec_mesh_solar_base, inkhud @@ -132,4 +132,4 @@ build_flags = ${heltec_mesh_solar_base.build_flags} lib_deps = ${heltec_mesh_solar_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main - https://github.com/meshtastic/st7789/archive/bd33ea58ddfe4a5e4a66d53300ccbd38d66ac21f.zip + https://github.com/meshtastic/st7789/archive/9ee76d6b18b9a8f45a2c5cae06b1134a587691eb.zip diff --git a/variants/nrf52840/meshlink/platformio.ini b/variants/nrf52840/meshlink/platformio.ini index e2631affe..28122d9bd 100644 --- a/variants/nrf52840/meshlink/platformio.ini +++ b/variants/nrf52840/meshlink/platformio.ini @@ -47,7 +47,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshlin 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.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 diff --git a/variants/nrf52840/nrf52.ini b/variants/nrf52840/nrf52.ini index 727d2c741..aeda68a2e 100644 --- a/variants/nrf52840/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.10.0 + platformio/nordicnrf52@10.11.0 extends = arduino_base platform_packages = ; our custom Git version until they merge our PR diff --git a/variants/nrf52840/rak4631_epaper/platformio.ini b/variants/nrf52840/rak4631_epaper/platformio.ini index f0da832cb..ba48cf2a2 100644 --- a/variants/nrf52840/rak4631_epaper/platformio.ini +++ b/variants/nrf52840/rak4631_epaper/platformio.ini @@ -15,7 +15,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini index 112ddfc29..714737c9d 100644 --- a/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/nrf52840/rak4631_epaper_onrxtx/platformio.ini @@ -17,7 +17,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak4631 lib_deps = ${nrf52840_base.lib_deps} # renovate: datasource=custom.pio depName=GxEPD2 packageName=zinggjm/library/GxEPD2 - zinggjm/GxEPD2@1.6.6 + zinggjm/GxEPD2@1.6.7 # renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028 melopero/Melopero RV3028@1.2.0 # renovate: datasource=custom.pio depName=RAK NCP5623 RGB LED packageName=rakwireless/library/RAKwireless NCP5623 RGB LED library diff --git a/variants/nrf52840/rak4631_eth_gw/platformio.ini b/variants/nrf52840/rak4631_eth_gw/platformio.ini index e06a271aa..d326edaab 100644 --- a/variants/nrf52840/rak4631_eth_gw/platformio.ini +++ b/variants/nrf52840/rak4631_eth_gw/platformio.ini @@ -14,7 +14,6 @@ build_flags = ${nrf52840_base.build_flags} -DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_SCREEN=1 ; -DMESHTASTIC_EXCLUDE_PKI=1 - -DMESHTASTIC_EXCLUDE_POWER_FSM=1 -DMESHTASTIC_EXCLUDE_POWERMON=1 ; -DMESHTASTIC_EXCLUDE_TZ=1 -DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 diff --git a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini index 07d763df3..9b15e668a 100644 --- a/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini +++ b/variants/nrf52840/rak4631_nomadstar_meteor_pro/platformio.ini @@ -24,8 +24,8 @@ 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 + # renovate: datasource=git-refs depName=IOBoard-RGB-LP5562-Library packageName=NomadStar-outdoor/IOBoard-RGB-LP5562-Library gitBranch=master + https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library/archive/9c366c875e1e8103ed97b5d4c09f3878345da80a.zip ; 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 diff --git a/variants/nrf52840/seeed_solar_node/variant.cpp b/variants/nrf52840/seeed_solar_node/variant.cpp index 994e97ff9..4123944d4 100644 --- a/variants/nrf52840/seeed_solar_node/variant.cpp +++ b/variants/nrf52840/seeed_solar_node/variant.cpp @@ -101,7 +101,6 @@ void initVariant() pinMode(PIN_LED2, OUTPUT); digitalWrite(PIN_LED2, LOW); pinMode(PIN_LED2, OUTPUT); - // digitalWrite(LED_PIN, LOW); pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 98aeb8700..2a2967f5e 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -9,8 +9,10 @@ #include "graphics/niche/InkHUD/InkHUD.h" // Applets +#include "graphics/niche/InkHUD/Applets/Examples/UserAppletInputExample/UserAppletInputExample.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -63,6 +65,7 @@ void setupNicheGraphics() 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 + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Pick applets // Note: order of applets determines priority of "auto-show" feature @@ -74,6 +77,7 @@ void setupNicheGraphics() inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet, false, false); // - // Start running InkHUD inkhud->begin(); diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 60d83b95a..d8fbaf8ff 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -34,7 +34,7 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_w 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 + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip debug_tool = jlink [env:seeed_wio_tracker_L1_eink-inkhud] diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 68be47622..3f9a4f7af 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -13,21 +13,22 @@ extends = nrf52840_base board = xiao_ble_sense board_level = pr build_flags = ${nrf52840_base.build_flags} - -Ivariants/nrf52840/seeed_xiao_nrf52840_kit - -Isrc/platform/nrf52/softdevice - -Isrc/platform/nrf52/softdevice/nrf52 + -I variants/nrf52840/seeed_xiao_nrf52840_kit + -I src/platform/nrf52/softdevice + -I src/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT - -DGPS_L76K + -DSEEED_XIAO_NRF_KIT_DEFAULT board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_xiao_nrf52840_kit> debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink -; Seeed Xiao BLE but with GPS undefined, and therefore i2c active +; Seeed Xiao BLE but with GPS moved to NFC pins, and therefore i2c active [env:seeed_xiao_nrf52840_kit_i2c] extends = env:seeed_xiao_nrf52840_kit board_level = extra build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -DSEEED_XIAO_NRF52840_KIT -build_unflags = -DGPS_L76K + -DSEEED_XIAO_NRF_KIT_I2C ; Define I2C variant + -USEEED_XIAO_NRF_KIT_DEFAULT ; Remove default define \ No newline at end of file diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h index 0d599d313..4dc28557d 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/variant.h @@ -17,6 +17,37 @@ extern "C" { #endif // __cplusplus +/* +Xiao pin assignments + +| Pin | Default | I2C | BTB | BLE-L | | Pin | Default | I2C | BTB | BLE-L | +| ----- | -------- | ---- | ---- | ----- | --- | ----- | ------- | ---- | ---- | ----- | +| | | | | | | | | | | | +| D0 | G_STBY | UBTN | DIO1 | CS | | 5v | | | | | +| D1 | DIO1 | DIO1 | Busy | DIO1 | | GND | | | | | +| D2 | NRST | NRST | NRST | Busy | | 3v3 | | | | | +| D3 | Busy | Busy | CS | NRST | | D10 | MOSI | MOSI | MOSI | MOSI | +| D4 | CS | CS | RXEN | SDA | | D9 | MISO | MISO | MISO | MISO | +| D5 | RXEN | RXEN | | SCL | | D8 | SCK | SCK | SCK | SCK | +| D6 | G_TX | SDA | G_TX | | | D7 | G_RX | SCL | G_RX | RXEN | +| | | | | | | | | | | | +| | End | | | | | | | | | | +| NFC1/ | SDA | G_TX | SDA | G_TX | | NFC2/ | SCL | G_RX | SCL | G_RX | +| D30 | | | | | | D31 | | | | | +| | | | | | | | | | | | +| | Internal | | | | | | | | | | +| D16 | SCL1 | SCL1 | SCL1 | SCL1 | | | | | | | +| D17 | SDA1 | SDA1 | SDA1 | SDA1 | | | | | | | + +The default column shows the pin assignments for the Wio-SX1262 for XIAO +(standalone SKU 113010003 or nRF52840 kit SKU 102010710). +The I2C column shows an alternative pin assignment using I2C on D6/D7 in place of the GNSS. +The BTB column shows the pin assignment for the Wio-SX1262 -30-pin board-to-board connector version from the ESP32S3 kit. +The BLE-L column shows the pin assignment for the original DIY xiao_ble, and which is retained for legacy users. +Note that the in addition to the difference between the default and the I2C pinouts in placing the pins on NFC or +D6/D7, the user button is activated on D0. The button conflicts with the official GNSS module, so caution is advised. +*/ + #define PINS_COUNT (33) #define NUM_DIGITAL_PINS (33) #define NUM_ANALOG_INPUTS (8) @@ -65,7 +96,7 @@ static const uint8_t A5 = PIN_A5; #define LED_GREEN (13) #define LED_BLUE (12) -#define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_PIN +#define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_POWER #define PIN_LED2 LED_BLUE #define PIN_LED3 LED_RED @@ -91,15 +122,15 @@ static const uint8_t A5 = PIN_A5; */ #define USE_SX1262 -#ifdef XIAO_BLE_LEGACY_PINOUT +#if defined(XIAO_BLE_LEGACY_PINOUT) // Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 #define SX126X_CS D0 #define SX126X_DIO1 D1 #define SX126X_BUSY D2 #define SX126X_RESET D3 #define SX126X_RXEN D7 - -#elif defined(SEEED_XIAO_WIO_BTB) +#else +#if defined(SEEED_XIAO_NRF_WIO_BTB) // Wio-SX1262 for XIAO with 30-pin board-to-board connector // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Schematic_Diagram_Wio-SX1262_for_XIAO.pdf #define SX126X_CS D3 @@ -109,13 +140,15 @@ static const uint8_t A5 = PIN_A5; #define SX126X_RXEN D4 #else // Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710) +// Same for both default and I2C pinouts // https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Wio-SX1262%20for%20XIAO%20V1.0_SCH.pdf #define SX126X_CS D4 #define SX126X_DIO1 D1 #define SX126X_BUSY D3 #define SX126X_RESET D2 #define SX126X_RXEN D5 -#endif +#endif // defined(SEEED_XIAO_NRF_WIO_BTB) +#endif // defined(XIAO_BLE_LEGACY_PINOUT) // Common pinouts for all SX126x pinouts above #define SX126X_TXEN RADIOLIB_NC @@ -141,18 +174,26 @@ static const uint8_t SCK = PIN_SPI_SCK; * GPS */ // GPS L76K -#ifdef GPS_L76K + +// Default GPS L76K +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#define GPS_L76K #define GPS_TX_PIN D6 // This is data from the MCU #define GPS_RX_PIN D7 // This is data from the GNSS module +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) +#define PIN_GPS_STANDBY D0 // this is where the conflicting pinouts come from +#endif +// I2C and BLE-Legacy put them on the NFC pins +#else +#define GPS_TX_PIN (30) +#define GPS_RX_PIN (31) +#endif + #define HAS_GPS 1 +#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #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) -#define PIN_SERIAL1_TX (-1) -#endif /* * Battery @@ -171,39 +212,60 @@ static const uint8_t SCK = PIN_SPI_SCK; * Wire Interfaces * Keep this section after potentially conflicting pin definitions */ -#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much -#define WIRE_INTERFACES_COUNT 1 +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much +#define WIRE_INTERFACES_COUNT 1 // changed to 1 for now, as LSM6DS3TR has issues. #if defined(XIAO_BLE_LEGACY_PINOUT) // Used for I2C by DIY xiao_ble variant #define PIN_WIRE_SDA D4 #define PIN_WIRE_SCL D5 -#elif !defined(GPS_L76K) -// If D6 and D7 are free, I2C is probably the most versatile assignment +#else +// Put the I2C pins on the NFC pins by default +#if defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#define PIN_WIRE_SDA 30 +#define PIN_WIRE_SCL 31 +#else +// If not on legacy or defauly, we're wanting I2C on the back pins #define PIN_WIRE_SDA D6 #define PIN_WIRE_SCL D7 -#else -// Internal LSM6DS3TR on XIAO nRF52840 Series -#define PIN_WIRE_SDA (17) -#define PIN_WIRE_SCL (16) -#endif +#endif // defined(SEEED_XIAO_NRF_KIT_DEFAULT) || defined(SEEED_XIAO_NRF_WIO_BTB) +#endif // defined(XIAO_BLE_LEGACY_PINOUT) -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; +// // Internal LSM6DS3TR on XIAO nRF52840 Series - put it on wire1 +// // Note: disabled for now, as there are some issues with the LSM. +// #define PIN_WIRE1_SDA (17) +// #define PIN_WIRE1_SCL (16) + +static const uint8_t SDA = PIN_WIRE_SDA; // Not sure if this is needed +static const uint8_t SCL = PIN_WIRE_SCL; // Not sure if this is needed + +// // QSPI Pins +// // --------- +// #define PIN_QSPI_SCK (24) +// #define PIN_QSPI_CS (25) +// #define PIN_QSPI_IO0 (26) +// #define PIN_QSPI_IO1 (27) +// #define PIN_QSPI_IO2 (28) +// #define PIN_QSPI_IO3 (29) + +// // On-board QSPI Flash +// // ------------------- +// #define EXTERNAL_FLASH_DEVICES P25Q16H +// #define EXTERNAL_FLASH_USE_QSPI /* * Buttons * Keep this section after potentially conflicting pin definitions * because D0 has multiple possible conflicts with various XIAO modules: - * - PIN_GPS_STANDBY on the L76K GNSS Module - * - DIO1 on the Wio-SX1262 - 30-pin board-to-board connector version - * - SX1262X CS on XIAO BLE legacy pinout */ - -#if !defined(GPS_L76K) && !defined(SEEED_XIAO_WIO_BTB) && !defined(XIAO_BLE_LEGACY_PINOUT) +#if defined(SEEED_XIAO_NRF_KIT_I2C) #define BUTTON_PIN D0 #endif +#if defined(SEEED_XIAO_NRF_WIO_BTB) +#define BUTTON_PIN D5 +#endif + #ifdef __cplusplus } #endif diff --git a/variants/nrf52840/t-echo-lite/platformio.ini b/variants/nrf52840/t-echo-lite/platformio.ini index c873dea37..1b725815e 100644 --- a/variants/nrf52840/t-echo-lite/platformio.ini +++ b/variants/nrf52840/t-echo-lite/platformio.ini @@ -30,5 +30,5 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip ;upload_protocol = fs diff --git a/variants/nrf52840/t-echo-lite/variant.h b/variants/nrf52840/t-echo-lite/variant.h index 7dc1a3ef5..54c7bdfb5 100644 --- a/variants/nrf52840/t-echo-lite/variant.h +++ b/variants/nrf52840/t-echo-lite/variant.h @@ -51,7 +51,6 @@ extern "C" { #define LED_GREEN PIN_LED1 #define BLE_LED LED_BLUE -#define BLE_LED_INVERTED 1 #define LED_STATE_ON 0 // State when LED is lit // Buttons diff --git a/variants/nrf52840/t-echo-plus/nicheGraphics.h b/variants/nrf52840/t-echo-plus/nicheGraphics.h index 483e16ea4..73067d7a7 100644 --- a/variants/nrf52840/t-echo-plus/nicheGraphics.h +++ b/variants/nrf52840/t-echo-plus/nicheGraphics.h @@ -8,6 +8,7 @@ #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -43,6 +44,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); diff --git a/variants/nrf52840/t-echo-plus/variant.h b/variants/nrf52840/t-echo-plus/variant.h index 4038ce6ef..7ebdf48c0 100644 --- a/variants/nrf52840/t-echo-plus/variant.h +++ b/variants/nrf52840/t-echo-plus/variant.h @@ -132,9 +132,6 @@ static const uint8_t A0 = PIN_A0; #define HAS_DRV2605 1 -// Battery / ADC already defined above -#define HAS_RTC 1 - #define SERIAL_PRINT_PORT 0 #ifdef __cplusplus diff --git a/variants/nrf52840/t-echo/nicheGraphics.h b/variants/nrf52840/t-echo/nicheGraphics.h index c89d816b9..c0b24dea7 100644 --- a/variants/nrf52840/t-echo/nicheGraphics.h +++ b/variants/nrf52840/t-echo/nicheGraphics.h @@ -11,6 +11,7 @@ // Applets #include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" #include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/FavoritesMap/FavoritesMapApplet.h" #include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" #include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" #include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" @@ -79,6 +80,7 @@ void setupNicheGraphics() inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Favorites Map", new InkHUD::FavoritesMapApplet); // - inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 @@ -123,4 +125,4 @@ void setupNicheGraphics() buttons->start(); } -#endif \ No newline at end of file +#endif diff --git a/variants/nrf52840/t-echo/platformio.ini b/variants/nrf52840/t-echo/platformio.ini index 4acd70b02..89ce488ad 100644 --- a/variants/nrf52840/t-echo/platformio.ini +++ b/variants/nrf52840/t-echo/platformio.ini @@ -30,7 +30,7 @@ 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/a05c11c02862624266b61599b0d6ba93e33c6f24.zip + https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip # renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib lewisxhe/SensorLib@0.3.4 ;upload_protocol = fs diff --git a/variants/nrf52840/tracker-t1000-e/variant.cpp b/variants/nrf52840/tracker-t1000-e/variant.cpp index 8096705d0..1d0423b9b 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.cpp +++ b/variants/nrf52840/tracker-t1000-e/variant.cpp @@ -32,10 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 143b7d4ad..e258f63ae 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -47,7 +47,7 @@ extern "C" { #define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc #define PIN_LED1 (0 + 24) // P0.24 -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/nrf52840/wio-t1000-s/variant.cpp b/variants/nrf52840/wio-t1000-s/variant.cpp index 85e0c44f3..3c54f6ce5 100644 --- a/variants/nrf52840/wio-t1000-s/variant.cpp +++ b/variants/nrf52840/wio-t1000-s/variant.cpp @@ -32,10 +32,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(LED_PIN, OUTPUT); - digitalWrite(LED_PIN, LOW); - pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index 3b8103d85..a55c42775 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -47,7 +47,7 @@ extern "C" { #define PIN_3V3_ACC_EN (32 + 7) // P1.7, Power to Acc #define PIN_LED1 (0 + 24) // P0.24 -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define LED_BLUE -1 // Actually green #define LED_STATE_ON 1 // State when LED is lit diff --git a/variants/rp2040/challenger_2040_lora/pins_arduino.h b/variants/rp2040/challenger_2040_lora/pins_arduino.h index 5e7311413..ee86e1a34 100644 --- a/variants/rp2040/challenger_2040_lora/pins_arduino.h +++ b/variants/rp2040/challenger_2040_lora/pins_arduino.h @@ -7,7 +7,7 @@ #define ADC_RESOLUTION (12u) // LEDs -#define PIN_LED (24u) +#define LED_POWER (24u) // Serial #define PIN_SERIAL1_TX (16u) diff --git a/variants/rp2040/challenger_2040_lora/variant.h b/variants/rp2040/challenger_2040_lora/variant.h index 552f90720..f5126cfff 100644 --- a/variants/rp2040/challenger_2040_lora/variant.h +++ b/variants/rp2040/challenger_2040_lora/variant.h @@ -5,8 +5,6 @@ #define EXT_NOTIFY_OUT 0xFFFFFFFF #define BUTTON_PIN 0xFFFFFFFF -#define LED_PIN PIN_LED - #define USE_RF95 // RFM95/SX127x #undef LORA_SCK diff --git a/variants/rp2040/ec_catsniffer/variant.h b/variants/rp2040/ec_catsniffer/variant.h index 400074e59..7df69f134 100644 --- a/variants/rp2040/ec_catsniffer/variant.h +++ b/variants/rp2040/ec_catsniffer/variant.h @@ -9,7 +9,7 @@ #undef GPS_RX_PIN #undef GPS_TX_PIN -#define LED_PIN 27 +#define LED_POWER 27 #define USE_SX1262 diff --git a/variants/rp2040/feather_rp2040_rfm95/variant.h b/variants/rp2040/feather_rp2040_rfm95/variant.h index e9e178202..efaced7b4 100644 --- a/variants/rp2040/feather_rp2040_rfm95/variant.h +++ b/variants/rp2040/feather_rp2040_rfm95/variant.h @@ -20,7 +20,7 @@ #define BUTTON_PIN 7 // #define BUTTON_NEED_PULLUP -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED // #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/nibble_rp2040/variant.h b/variants/rp2040/nibble_rp2040/variant.h index 0f71b98e9..3b1dfcd7b 100644 --- a/variants/rp2040/nibble_rp2040/variant.h +++ b/variants/rp2040/nibble_rp2040/variant.h @@ -2,7 +2,7 @@ #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 -#define LED_PIN 1 +#define LED_POWER 1 #define HAS_CPU_SHUTDOWN 1 diff --git a/variants/rp2040/rak11310/pins_arduino.h b/variants/rp2040/rak11310/pins_arduino.h index 56214a947..59290bbdb 100644 --- a/variants/rp2040/rak11310/pins_arduino.h +++ b/variants/rp2040/rak11310/pins_arduino.h @@ -23,8 +23,7 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED +#define PIN_LED1 (23u) #define LED_NOTIFICATION (24u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/rak11310/variant.h b/variants/rp2040/rak11310/variant.h index 4d2b9ca3a..4dfad060e 100644 --- a/variants/rp2040/rak11310/variant.h +++ b/variants/rp2040/rak11310/variant.h @@ -10,7 +10,7 @@ #define I2C_SDA1 2 #define I2C_SCL1 3 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #define BUTTON_PIN 9 diff --git a/variants/rp2040/rp2040-lora/variant.h b/variants/rp2040/rp2040-lora/variant.h index 92b067457..51a760e0b 100644 --- a/variants/rp2040/rp2040-lora/variant.h +++ b/variants/rp2040/rp2040-lora/variant.h @@ -17,7 +17,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN -1 // Pin 17 used for antenna switching via DIO4 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED // #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipico-slowclock/variant.h b/variants/rp2040/rpipico-slowclock/variant.h index fb97ec0fb..40d20e17a 100644 --- a/variants/rp2040/rpipico-slowclock/variant.h +++ b/variants/rp2040/rpipico-slowclock/variant.h @@ -52,7 +52,7 @@ #define BUTTON_PIN 18 #define EXT_NOTIFY_OUT 22 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipico/variant.h b/variants/rp2040/rpipico/variant.h index 7efaeaf7a..dd849c290 100644 --- a/variants/rp2040/rpipico/variant.h +++ b/variants/rp2040/rpipico/variant.h @@ -15,7 +15,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/rpipicow/variant.h b/variants/rp2040/rpipicow/variant.h index fe94e615d..2de00545e 100644 --- a/variants/rp2040/rpipicow/variant.h +++ b/variants/rp2040/rpipicow/variant.h @@ -19,7 +19,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/rp2040/senselora_rp2040/pins_arduino.h b/variants/rp2040/senselora_rp2040/pins_arduino.h index 61705c8d9..575839cbc 100644 --- a/variants/rp2040/senselora_rp2040/pins_arduino.h +++ b/variants/rp2040/senselora_rp2040/pins_arduino.h @@ -11,8 +11,7 @@ static const uint8_t A2 = PIN_A2; static const uint8_t A3 = PIN_A3; // LEDs -#define PIN_LED (23u) -#define PIN_LED1 PIN_LED +#define PIN_LED1 (23u) #define ADC_RESOLUTION 12 diff --git a/variants/rp2040/senselora_rp2040/variant.h b/variants/rp2040/senselora_rp2040/variant.h index cc90284b7..f79ed66ca 100644 --- a/variants/rp2040/senselora_rp2040/variant.h +++ b/variants/rp2040/senselora_rp2040/variant.h @@ -5,7 +5,7 @@ #define BUTTON_PIN 2 #define BUTTON_NEED_PULLUP -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED1 #define ledOff(pin) pinMode(pin, INPUT) #undef BATTERY_PIN diff --git a/variants/rp2350/rpipico2/variant.h b/variants/rp2350/rpipico2/variant.h index 7efaeaf7a..dd849c290 100644 --- a/variants/rp2350/rpipico2/variant.h +++ b/variants/rp2350/rpipico2/variant.h @@ -15,7 +15,7 @@ #define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 -#define LED_PIN PIN_LED +#define LED_POWER PIN_LED #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) diff --git a/variants/stm32/CDEBYTE_E77-MBL/variant.h b/variants/stm32/CDEBYTE_E77-MBL/variant.h index 2cf355a61..686326137 100644 --- a/variants/stm32/CDEBYTE_E77-MBL/variant.h +++ b/variants/stm32/CDEBYTE_E77-MBL/variant.h @@ -15,8 +15,8 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PB4 // LED1 -// #define LED_PIN PB3 // LED2 +#define LED_POWER PB4 // LED1 +// #define LED_POWER PB3 // LED2 #define LED_STATE_ON 1 #define SERIAL_PRINT_PORT 1 diff --git a/variants/stm32/milesight_gs301/variant.h b/variants/stm32/milesight_gs301/variant.h index e86e93fc4..f68d70458 100644 --- a/variants/stm32/milesight_gs301/variant.h +++ b/variants/stm32/milesight_gs301/variant.h @@ -6,7 +6,7 @@ // I/O #define LED_STATE_ON 1 #define PIN_LED1 PA0 // Green LED -#define LED_PIN PIN_LED1 +#define LED_POWER PIN_LED1 #define PIN_LED2 PA0 // Red LED #define USER_LED PIN_LED2 #define BUTTON_PIN PC13 diff --git a/variants/stm32/rak3172/variant.h b/variants/stm32/rak3172/variant.h index b3f6cbcda..bd6decd4c 100644 --- a/variants/stm32/rak3172/variant.h +++ b/variants/stm32/rak3172/variant.h @@ -13,7 +13,7 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PA0 // Green LED +#define LED_POWER PA0 // Green LED #define LED_STATE_ON 1 #define RAK3172 diff --git a/variants/stm32/russell/variant.h b/variants/stm32/russell/variant.h index 796302d34..8773d5d8d 100644 --- a/variants/stm32/russell/variant.h +++ b/variants/stm32/russell/variant.h @@ -4,7 +4,7 @@ #define USE_STM32WLx // I/O -#define LED_PIN PA0 // Red LED +#define LED_POWER PA0 // Red LED #define LED_STATE_ON 1 #define BUTTON_PIN PH3 // Shared with BOOT0 #define BUTTON_NEED_PULLUP diff --git a/variants/stm32/stm32.ini b/variants/stm32/stm32.ini index bb0a4d3ce..7a3b37642 100644 --- a/variants/stm32/stm32.ini +++ b/variants/stm32/stm32.ini @@ -2,7 +2,7 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.4.0 + platformio/ststm32@19.5.0 platform_packages = # renovate: datasource=github-tags depName=Arduino_Core_STM32 packageName=stm32duino/Arduino_Core_STM32 platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip diff --git a/variants/stm32/wio-e5/platformio.ini b/variants/stm32/wio-e5/platformio.ini index 311cade58..c8dbb2b72 100644 --- a/variants/stm32/wio-e5/platformio.ini +++ b/variants/stm32/wio-e5/platformio.ini @@ -17,5 +17,6 @@ build_flags = -DPIN_SERIAL2_RX=PA3 -DHAS_GPS=1 -DGPS_SERIAL_PORT=Serial2 + -DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1 upload_port = stlink diff --git a/variants/stm32/wio-e5/variant.h b/variants/stm32/wio-e5/variant.h index 2b20eb2a6..da2c623fb 100644 --- a/variants/stm32/wio-e5/variant.h +++ b/variants/stm32/wio-e5/variant.h @@ -14,7 +14,7 @@ Do not expect a working Meshtastic device with this target. #define USE_STM32WLx -#define LED_PIN PB5 +#define LED_POWER PB5 #define LED_STATE_ON 0 #define WIO_E5 diff --git a/version.properties b/version.properties index 62145da14..1bbb87be7 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 19 +build = 20