diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml
index a1e8dd852..c048b7ac2 100644
--- a/.github/actions/build-variant/action.yml
+++ b/.github/actions/build-variant/action.yml
@@ -76,7 +76,7 @@ runs:
done
- name: PlatformIO ${{ inputs.arch }} download cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.platformio/.cache
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
@@ -100,7 +100,7 @@ runs:
id: version
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
overwrite: true
diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml
index d7d26f0e8..de114be1c 100644
--- a/.github/workflows/build_debian_src.yml
+++ b/.github/workflows/build_debian_src.yml
@@ -64,7 +64,7 @@ jobs:
PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
overwrite: true
diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml
index 28e4ee994..19381e211 100644
--- a/.github/workflows/build_firmware.yml
+++ b/.github/workflows/build_firmware.yml
@@ -21,7 +21,7 @@ jobs:
# Use 'arctastic' self-hosted runner pool when building in the main repo
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
outputs:
- artifact-id: ${{ steps.upload.outputs.artifact-id }}
+ artifact-id: ${{ steps.upload-firmware.outputs.artifact-id }}
steps:
- uses: actions/checkout@v6
with:
@@ -56,20 +56,22 @@ jobs:
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
- - name: Echo manifest from release/firmware-*.mt.json to job summary
- if: ${{ always() }}
+ - name: Job summary
env:
PIO_ENV: ${{ inputs.pio_env }}
run: |
- echo "## Manifest: \`$PIO_ENV\`" >> $GITHUB_STEP_SUMMARY
+ echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
+ echo "Manifest
" >> $GITHUB_STEP_SUMMARY
+ echo '' >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
cat release/firmware-*.mt.json >> $GITHUB_STEP_SUMMARY
echo '' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
+ echo " " >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
- id: upload
+ uses: actions/upload-artifact@v6
+ id: upload-firmware
with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
overwrite: true
@@ -82,3 +84,12 @@ jobs:
release/*.zip
release/device-*.sh
release/device-*.bat
+
+ - name: Store manifests as an artifact
+ uses: actions/upload-artifact@v6
+ id: upload-manifest
+ with:
+ name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
+ overwrite: true
+ path: |
+ release/*.mt.json
diff --git a/.github/workflows/build_one_target.yml b/.github/workflows/build_one_target.yml
index 9d9e0114b..9cc0bac78 100644
--- a/.github/workflows/build_one_target.yml
+++ b/.github/workflows/build_one_target.yml
@@ -98,7 +98,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-*-*
@@ -111,7 +111,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -127,7 +127,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -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@v5
+ uses: actions/upload-artifact@v6
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 acd63f28f..8a25829e4 100644
--- a/.github/workflows/main_matrix.yml
+++ b/.github/workflows/main_matrix.yml
@@ -77,16 +77,21 @@ jobs:
fail-fast: false
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
-
- runs-on: ubuntu-latest
+ # Use 'arctastic' self-hosted runner pool when checking in the main repo
+ runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps:
- uses: actions/checkout@v6
- - name: Build base
- id: base
- uses: ./.github/actions/setup-base
+ with:
+ submodules: recursive
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }}
- run: bin/check-all.sh ${{ matrix.check.board }}
+ uses: meshtastic/gh-action-firmware@main
+ with:
+ pio_platform: ${{ matrix.check.platform }}
+ pio_env: ${{ matrix.check.board }}
+ pio_target: check
build:
needs: [setup, version]
@@ -168,7 +173,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -178,7 +183,7 @@ jobs:
run: ls -R
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -195,7 +200,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -214,7 +219,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@v5
+ uses: actions/upload-artifact@v6
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -228,6 +233,40 @@ jobs:
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
+ shame:
+ if: github.repository == 'meshtastic/firmware'
+ continue-on-error: true
+ runs-on: ubuntu-latest
+ needs: [build]
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
+ fetch-depth: 0
+ - name: Download the current manifests
+ uses: actions/download-artifact@v7
+ with:
+ path: ./manifests-new/
+ pattern: manifest-*
+ merge-multiple: true
+ - name: Upload combined manifests for later commit and global stats crunching.
+ uses: actions/upload-artifact@v6
+ id: upload-manifest
+ with:
+ name: manifests-all
+ overwrite: true
+ path: |
+ manifests-new/*.mt.json
+ - name: Find the merge base
+ run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
+ env:
+ base: ${{ github.base_ref }}
+ head: ${{ github.head_ref }}
+ - name: Download the old manifests
+ run: gh run download -R ${{ github.repository }} --commit ${{ env.MERGE_BASE }} --name manifests-all --dir manifest-old/
+ - name: Do scan and post comment
+ run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
+
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
@@ -255,14 +294,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -286,7 +325,7 @@ jobs:
}' > firmware-${{ needs.version.outputs.long }}.json
- name: Save Release manifest artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: manifest-${{ needs.version.outputs.long }}
overwrite: true
@@ -327,7 +366,7 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -344,7 +383,7 @@ jobs:
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -383,14 +422,14 @@ jobs:
python-version: 3.x
- name: Get firmware artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Get manifest artifact
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: manifest-${{ needs.version.outputs.long }}
path: ./publish
diff --git a/.github/workflows/merge_queue.yml b/.github/workflows/merge_queue.yml
index a71afad9d..bd3f6d4eb 100644
--- a/.github/workflows/merge_queue.yml
+++ b/.github/workflows/merge_queue.yml
@@ -147,7 +147,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -160,7 +160,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
@@ -176,7 +176,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -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@v5
+ uses: actions/upload-artifact@v6
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@v6
+ uses: actions/download-artifact@v7
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -292,7 +292,7 @@ jobs:
with:
python-version: 3.x
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -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@v6
+ - uses: actions/download-artifact@v7
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@v6
+ - uses: actions/download-artifact@v7
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
diff --git a/.github/workflows/package_obs.yml b/.github/workflows/package_obs.yml
index 2b202ed95..63f1fe8a0 100644
--- a/.github/workflows/package_obs.yml
+++ b/.github/workflows/package_obs.yml
@@ -58,7 +58,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/package_pio_deps.yml b/.github/workflows/package_pio_deps.yml
index cb10a79f3..82ffe66e9 100644
--- a/.github/workflows/package_pio_deps.yml
+++ b/.github/workflows/package_pio_deps.yml
@@ -56,7 +56,7 @@ jobs:
PLATFORMIO_CORE_DIR: pio/core
- name: Store binaries as an artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
overwrite: true
diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml
index 2e3278041..9a463dbea 100644
--- a/.github/workflows/package_ppa.yml
+++ b/.github/workflows/package_ppa.yml
@@ -60,7 +60,7 @@ jobs:
id: version
- name: Download artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true
diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml
index a3e0b23cf..6306d777f 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@v6
+ uses: actions/download-artifact@v7
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml
index f21b13ee1..badbb31d4 100644
--- a/.github/workflows/release_channels.yml
+++ b/.github/workflows/release_channels.yml
@@ -102,7 +102,7 @@ jobs:
PIP_DISABLE_PIP_VERSION_CHECK: 1
- name: Create Bumps pull request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@v8
with:
base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version
diff --git a/.github/workflows/sec_sast_semgrep_cron.yml b/.github/workflows/sec_sast_semgrep_cron.yml
index d044f9038..d93449d6d 100644
--- a/.github/workflows/sec_sast_semgrep_cron.yml
+++ b/.github/workflows/sec_sast_semgrep_cron.yml
@@ -33,7 +33,7 @@ jobs:
# step 3
- name: save report as pipeline artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: report.sarif
overwrite: true
diff --git a/.github/workflows/test_native.yml b/.github/workflows/test_native.yml
index 26ff306a9..cabe0dd97 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@v5
+ uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}
@@ -94,7 +94,7 @@ jobs:
- name: Save test results
if: always() # run this step even if previous step failed
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
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@v5
+ uses: actions/upload-artifact@v6
if: always() # run this step even if previous step failed
with:
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}
@@ -137,7 +137,7 @@ jobs:
id: version
- name: Download test artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true
@@ -150,7 +150,7 @@ jobs:
reporter: java-junit
- name: Download coverage artifacts
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v7
with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
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@v5
+ uses: actions/upload-artifact@v6
with:
name: code-coverage-report-${{ steps.version.outputs.long }}
path: code-coverage-report
diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml
index af0557fda..35565d1e4 100644
--- a/.github/workflows/update_protobufs.yml
+++ b/.github/workflows/update_protobufs.yml
@@ -16,7 +16,7 @@ jobs:
submodules: true
- name: Update submodule
- if: ${{ github.ref == 'refs/heads/master' }}
+ if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }}
run: |
git submodule update --remote protobufs
@@ -31,7 +31,7 @@ jobs:
./bin/regen-protos.sh
- name: Create pull request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@v8
with:
branch: create-pull-request/update-protobufs
labels: submodules
diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml
index 95e5b0dd2..c20066f7f 100644
--- a/.trunk/trunk.yaml
+++ b/.trunk/trunk.yaml
@@ -9,24 +9,24 @@ plugins:
lint:
enabled:
- checkov@3.2.495
- - renovate@42.30.4
+ - renovate@42.64.1
- prettier@3.7.4
- - trufflehog@3.91.2
+ - trufflehog@3.92.3
- yamllint@1.37.1
- bandit@1.9.2
- - trivy@0.67.2
+ - trivy@0.68.2
- taplo@0.10.0
- - ruff@0.14.7
+ - ruff@0.14.10
- isort@7.0.0
- - markdownlint@0.46.0
- - oxipng@9.1.5
+ - markdownlint@0.47.0
+ - oxipng@10.0.0
- svgo@4.0.0
- actionlint@1.7.9
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- - black@25.11.0
+ - black@25.12.0
- git-diff-check
- gitleaks@8.30.0
- clang-format@16.0.3
diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh
index 8c684aa7e..4e799b30a 100755
--- a/bin/build-esp32.sh
+++ b/bin/build-esp32.sh
@@ -22,7 +22,7 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
+pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
@@ -32,20 +32,10 @@ cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin
echo "Copying ESP32 update bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
-echo "Building Filesystem for ESP32 targets"
-# If you want to build the webui, uncomment the following lines
-# pio run --environment $1 -t buildfs
-# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
-# # Remove webserver files from the filesystem and rebuild
-# ls -l data/static # Diagnostic list of files
-# rm -rf data/static
-pio run --environment $1 -t buildfs --disable-auto-clean
+echo "Copying Filesystem for ESP32 targets"
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin
cp bin/device-install.* $OUTDIR/
cp bin/device-update.* $OUTDIR/
-# Generate the manifest file
-echo "Generating Meshtastic manifest"
-TIMEFORMAT="Generated manifest in %E seconds"
-time pio run --environment $1 -t mtjson --silent --disable-auto-clean
+echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-nrf52.sh b/bin/build-nrf52.sh
index c605fb1e0..e3a421865 100755
--- a/bin/build-nrf52.sh
+++ b/bin/build-nrf52.sh
@@ -22,7 +22,7 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
+pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
@@ -47,8 +47,5 @@ if (echo $1 | grep -q "rak4631"); then
cp $SRCHEX $OUTDIR/
fi
-# Generate the manifest file
-echo "Generating Meshtastic manifest"
-TIMEFORMAT="Generated manifest in %E seconds"
-time pio run --environment $1 -t mtjson --silent --disable-auto-clean
+echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-rp2xx0.sh b/bin/build-rp2xx0.sh
index ae26fdfbf..3ef1c1e34 100755
--- a/bin/build-rp2xx0.sh
+++ b/bin/build-rp2xx0.sh
@@ -22,15 +22,12 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
+pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying uf2 file"
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
-# Generate the manifest file
-echo "Generating Meshtastic manifest"
-TIMEFORMAT="Generated manifest in %E seconds"
-time pio run --environment $1 -t mtjson --silent --disable-auto-clean
+echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/build-stm32wl.sh b/bin/build-stm32wl.sh
index b85da04a6..023f3603c 100755
--- a/bin/build-stm32wl.sh
+++ b/bin/build-stm32wl.sh
@@ -22,15 +22,12 @@ export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION
-pio run --environment $1 # -v
+pio run --environment $1 -t mtjson # -v
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf
echo "Copying STM32 bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
-# Generate the manifest file
-echo "Generating Meshtastic manifest"
-TIMEFORMAT="Generated manifest in %E seconds"
-time pio run --environment $1 -t mtjson --silent --disable-auto-clean
+echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml
index b4cc81792..adf804ba9 100644
--- a/bin/config-dist.yaml
+++ b/bin/config-dist.yaml
@@ -184,6 +184,8 @@ Input:
Logging:
LogLevel: info # debug, info, warn, error
# TraceFile: /var/log/meshtasticd.json
+# JSONFile: /packets.json # File location for JSON output of decoded packets
+# JSONFilter: position # filter for packets to save to JSON file
# AsciiLogs: true # default if not specified is !isatty() on stdout
Webserver:
diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml
index 140ac3e2a..5779167ab 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.18
+
https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17
diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py
index 151cf0a97..3fdbffb70 100644
--- a/bin/platformio-custom.py
+++ b/bin/platformio-custom.py
@@ -159,20 +159,22 @@ def load_boot_logo(source, target, env):
# Load the boot logo on TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
- env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
+ env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo)
-# Rename (mv) littlefs.bin to include the PROGNAME
-# This ensures the littlefs.bin is named consistently with the firmware
-env.AddPostAction('$BUILD_DIR/littlefs.bin', env.VerboseAction(
- f'mv $BUILD_DIR/littlefs.bin $BUILD_DIR/{lfsbin}',
- f'Renaming littlefs.bin to {lfsbin}'
-))
+mtjson_deps = ["buildprog"]
+if platform.name == "espressif32":
+ # Build littlefs image as part of mtjson target
+ # Equivalent to `pio run -t buildfs`
+ target_lfs = env.DataToBin(
+ join("$BUILD_DIR", "${ESP32_FS_IMAGE_NAME}"), "$PROJECT_DATA_DIR"
+ )
+ mtjson_deps.append(target_lfs)
env.AddCustomTarget(
name="mtjson",
- dependencies=None,
+ dependencies=mtjson_deps,
actions=[manifest_gather],
title="Meshtastic Manifest",
description="Generating Meshtastic manifest JSON + Checksums",
- always_build=True,
+ always_build=False,
)
diff --git a/bin/platformio-pre.py b/bin/platformio-pre.py
index 4e51a6544..16278b813 100644
--- a/bin/platformio-pre.py
+++ b/bin/platformio-pre.py
@@ -11,6 +11,9 @@ else:
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
+ env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
# Print the new program name for verification
print(f"PROGNAME: {env.get('PROGNAME')}")
+if platform.name == "espressif32":
+ print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")
diff --git a/bin/shame.py b/bin/shame.py
new file mode 100644
index 000000000..f2253bfdc
--- /dev/null
+++ b/bin/shame.py
@@ -0,0 +1,95 @@
+import sys
+import os
+import json
+from github import Github
+
+def parseFile(path):
+ with open(path, "r") as f:
+ data = json.loads(f)
+ for file in data["files"]:
+ if file["name"].endswith(".bin"):
+ return file["name"], file["bytes"]
+
+if len(sys.argv) != 4:
+ print(f"expected usage: {sys.argv[0]} ")
+ sys.exit(1)
+
+pr_number = int(sys.argv[1])
+
+token = os.getenv("GITHUB_TOKEN")
+if not token:
+ raise EnvironmentError("GITHUB_TOKEN not found in environment.")
+
+repo_name = os.getenv("GITHUB_REPOSITORY") # "owner/repo"
+if not repo_name:
+ raise EnvironmentError("GITHUB_REPOSITORY not found in environment.")
+
+oldFiles = sys.argv[2]
+old = set(os.path.join(oldFiles, f) for f in os.listdir(oldFiles) if os.path.isfile(f))
+newFiles = sys.argv[3]
+new = set(os.path.join(newFiles, f) for f in os.listdir(newFiles) if os.path.isfile(f))
+
+startMarkdown = "# Target Size Changes\n\n"
+markdown = ""
+
+newlyIntroduced = new - old
+if len(newlyIntroduced) > 0:
+ markdown += "## Newly Introduced Targets\n\n"
+ # create a table
+ markdown += "| File | Size |\n"
+ markdown += "| ---- | ---- |\n"
+ for f in newlyIntroduced:
+ name, size = parseFile(f)
+ markdown += f"| `{name}` | {size}b |\n"
+
+# do not log removed targets
+# PRs only run a small subset of builds, so removed targets are not meaningful
+# since they are very likely to just be not ran in PR CI
+
+both = old & new
+degradations = []
+improvements = []
+for f in both:
+ oldName, oldSize = parseFile(f)
+ _, newSize = parseFile(f)
+ if oldSize != newSize:
+ if newSize < oldSize:
+ improvements.append((oldName, oldSize, newSize))
+ else:
+ degradations.append((oldName, oldSize, newSize))
+
+if len(degradations) > 0:
+ markdown += "\n## Degradation\n\n"
+ # create a table
+ markdown += "| File | Difference | Old Size | New Size |\n"
+ markdown += "| ---- | ---------- | -------- | -------- |\n"
+ for oldName, oldSize, newSize in degradations:
+ markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
+
+if len(improvements) > 0:
+ markdown += "\n## Improvement\n\n"
+ # create a table
+ markdown += "| File | Difference | Old Size | New Size |\n"
+ markdown += "| ---- | ---------- | -------- | -------- |\n"
+ for oldName, oldSize, newSize in improvements:
+ markdown += f"| `{oldName}` | **{oldSize - newSize}b** | {oldSize}b | {newSize}b |\n"
+
+if len(markdown) == 0:
+ markdown = "No changes in target sizes detected."
+
+g = Github(token)
+repo = g.get_repo(repo_name)
+pr = repo.get_pull(pr_number)
+
+existing_comment = None
+for comment in pr.get_issue_comments():
+ if comment.body.startswith(startMarkdown):
+ existing_comment = comment
+ break
+
+final_markdown = startMarkdown + markdown
+
+if existing_comment:
+ existing_comment.edit(body=final_markdown)
+else:
+ pr.create_issue_comment(body=final_markdown)
diff --git a/debian/changelog b/debian/changelog
index b9212c1be..ccaffa3cf 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+meshtasticd (2.7.18.0) unstable; urgency=medium
+
+ * Version 2.7.18
+
+ -- GitHub Actions Sat, 20 Dec 2025 15:47:25 +0000
+
meshtasticd (2.7.17.0) unstable; urgency=medium
* Version 2.7.17
diff --git a/extra_scripts/esp32_extra.py b/extra_scripts/esp32_extra.py
index 8841ad1dc..f7698561a 100755
--- a/extra_scripts/esp32_extra.py
+++ b/extra_scripts/esp32_extra.py
@@ -10,6 +10,12 @@ Import("env")
platform = env.PioPlatform()
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
+# IntelHex workaround, remove after fixed upstream
+# https://github.com/platformio/platform-espressif32/issues/1632
+try:
+ import intelhex
+except ImportError:
+ env.Execute("$PYTHONEXE -m pip install intelhex")
import esptool
diff --git a/platformio.ini b/platformio.ini
index 6d1ba40f4..cb44cfc15 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -104,17 +104,13 @@ lib_deps =
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
+
+; Extra TCP/IP networking libs for supported devices
+[networking_extra]
+lib_deps =
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0
-; Minimal networking libs for nrf52 (excludes Syslog to save flash)
-[nrf52_networking_base]
-lib_deps =
- # renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
- thingsboard/TBPubSubClient@2.12.1
- # renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
- arduino-libraries/NTPClient@3.2.1
-
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
@@ -124,7 +120,7 @@ lib_deps =
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
- https://github.com/meshtastic/device-ui/archive/4fb5f24787caa841b58dbf623a52c4c5861d6722.zip
+ https://github.com/meshtastic/device-ui/archive/862ed040c4ab44f0dfbbe492691f144886102588.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -163,8 +159,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5
- # renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221
- https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a
+ # renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
+ https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
diff --git a/protobufs b/protobufs
index 4095e5989..c474fd3f4 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit 4095e598902b4cd893dbcb62842514704d0f64e0
+Subproject commit c474fd3f49864f5f66ea3cd83c26848b8ae7cc64
diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp
index 246cf0022..d88f9fc9f 100644
--- a/src/DisplayFormatters.cpp
+++ b/src/DisplayFormatters.cpp
@@ -31,6 +31,9 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST:
return useShortName ? "LongF" : "LongFast";
break;
+ case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
+ return useShortName ? "LongT" : "LongTurbo";
+ break;
case meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE:
return useShortName ? "LongM" : "LongMod";
break;
diff --git a/src/configuration.h b/src/configuration.h
index b4ab57053..650e1cc71 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -29,8 +29,8 @@ along with this program. If not, see .
#if __has_include("Melopero_RV3028.h")
#include "Melopero_RV3028.h"
#endif
-#if __has_include("pcf8563.h")
-#include "pcf8563.h"
+#if __has_include("SensorRtcHelper.hpp")
+#include "SensorRtcHelper.hpp"
#endif
/* Offer chance for variant-specific defines */
diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp
index 8ac503b83..83a455de7 100644
--- a/src/detect/ScanI2C.cpp
+++ b/src/detect/ScanI2C.cpp
@@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const
ScanI2C::FoundDevice ScanI2C::firstRTC() const
{
- ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE};
- return firstOfOrNONE(3, types);
+ ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_PCF85063, RTC_RX8130CE};
+ return firstOfOrNONE(4, types);
}
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h
index cced980a6..3a79d97c5 100644
--- a/src/detect/ScanI2C.h
+++ b/src/detect/ScanI2C.h
@@ -14,6 +14,7 @@ class ScanI2C
SCREEN_ST7567,
RTC_RV3028,
RTC_PCF8563,
+ RTC_PCF85063,
RTC_RX8130CE,
CARDKB,
TDECKKB,
diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp
index db269ac64..8e91d1787 100644
--- a/src/detect/ScanI2CTwoWire.cpp
+++ b/src/detect/ScanI2CTwoWire.cpp
@@ -202,6 +202,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif
+#ifdef PCF85063_RTC
+ SCAN_SIMPLE_CASE(PCF85063_RTC, RTC_PCF85063, "PCF85063", (uint8_t)addr.address)
+#endif
+
case CARDKB_ADDR:
// Do we have the RAK14006 instead?
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp
index 1122f0a51..25cd3ceff 100644
--- a/src/gps/RTC.cpp
+++ b/src/gps/RTC.cpp
@@ -66,26 +66,26 @@ RTCSetResult readFromRTC()
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
+ } else {
+ LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
-#elif defined(PCF8563_RTC)
+#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
+#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
+#elif defined(PCF85063_RTC)
+ if (rtc_found.address == PCF85063_RTC) {
+#endif
uint32_t now = millis();
- PCF8563_Class rtc;
+ SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
#else
- rtc.begin();
+ rtc.begin(Wire);
#endif
- auto tc = rtc.getDateTime();
- tm t;
- t.tm_year = tc.year - 1900;
- t.tm_mon = tc.month - 1;
- t.tm_mday = tc.day;
- t.tm_hour = tc.hour;
- t.tm_min = tc.minute;
- t.tm_sec = tc.second;
+ RTC_DateTime datetime = rtc.getDateTime();
+ tm t = datetime.toUnixTime();
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
@@ -100,14 +100,16 @@ RTCSetResult readFromRTC()
}
#endif
- LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
- t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
+ LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
+ t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
+ } else {
+ LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
@@ -232,20 +234,28 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
+ } else {
+ LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
-#elif defined(PCF8563_RTC)
+#elif defined(PCF8563_RTC) || defined(PCF85063_RTC)
+#if defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) {
- PCF8563_Class rtc;
+#elif defined(PCF85063_RTC)
+ if (rtc_found.address == PCF85063_RTC) {
+#endif
+ SensorRtcHelper rtc;
#if WIRE_INTERFACES_COUNT == 2
rtc.begin(rtc_found.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire);
#else
- rtc.begin();
+ rtc.begin(Wire);
#endif
tm *t = gmtime(&tv->tv_sec);
- rtc.setDateTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
- LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
- t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
+ rtc.setDateTime(*t);
+ LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1,
+ t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
+ } else {
+ LOG_WARN("RTC not found (found address 0x%02X)", rtc_found.address);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp
index 1b3a148d6..ceb3b83f5 100644
--- a/src/graphics/draw/DebugRenderer.cpp
+++ b/src/graphics/draw/DebugRenderer.cpp
@@ -532,8 +532,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const int labelX = x;
int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK
+#ifndef T_DECK_PRO
barsOffset -= 12;
#endif
+#endif
#if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset;
#else
@@ -574,7 +576,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#endif
// Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT);
- display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
+ display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr);
};
// === Memory values ===
diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp
index f782dabb6..586bdd4a6 100644
--- a/src/graphics/draw/MenuHandler.cpp
+++ b/src/graphics/draw/MenuHandler.cpp
@@ -20,12 +20,41 @@
#include "modules/KeyVerificationModule.h"
#include "modules/TraceRouteModule.h"
+#include
+#include
#include
+#include
extern uint16_t TFT_MESH;
namespace graphics
{
+
+namespace
+{
+
+// Caller must ensure the provided options array outlives the banner callback.
+template
+BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOption (&options)[N],
+ std::array &labels, Callback &&onSelection)
+{
+ for (size_t i = 0; i < N; ++i) {
+ labels[i] = options[i].label;
+ }
+
+ const MenuOption *optionsPtr = options;
+ auto callback = std::function &, int)>(std::forward(onSelection));
+
+ BannerOverlayOptions bannerOptions;
+ bannerOptions.message = message;
+ bannerOptions.optionsArrayPtr = labels.data();
+ bannerOptions.optionsCount = static_cast(N);
+ bannerOptions.bannerCallback = [optionsPtr, callback](int selected) -> void { callback(optionsPtr[selected], selected); };
+ return bannerOptions;
+}
+
+} // namespace
+
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
@@ -197,48 +226,38 @@ void menuHandler::DeviceRolePicker()
void menuHandler::RadioPresetPicker()
{
- static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
- "MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
- enum optionsNumbers {
- Back = 0,
- radiopreset_LongSlow = 1,
- radiopreset_LongModerate = 2,
- radiopreset_LongFast = 3,
- radiopreset_MediumSlow = 4,
- radiopreset_MediumFast = 5,
- radiopreset_ShortSlow = 6,
- radiopreset_ShortFast = 7,
- radiopreset_ShortTurbo = 8
- };
- BannerOverlayOptions bannerOptions;
- bannerOptions.message = "Radio Preset";
- bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 9;
- bannerOptions.bannerCallback = [](int selected) -> void {
- if (selected == Back) {
- menuHandler::menuQueue = menuHandler::lora_Menu;
- screen->runNow();
- return;
- } else if (selected == radiopreset_LongSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
- } else if (selected == radiopreset_LongModerate) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
- } else if (selected == radiopreset_LongFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
- } else if (selected == radiopreset_MediumSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
- } else if (selected == radiopreset_MediumFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
- } else if (selected == radiopreset_ShortSlow) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
- } else if (selected == radiopreset_ShortFast) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
- } else if (selected == radiopreset_ShortTurbo) {
- config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
- }
- service->reloadConfig(SEGMENT_CONFIG);
- rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ static const RadioPresetOption presetOptions[] = {
+ {"Back", OptionsAction::Back},
+ {"LongTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO},
+ {"LongModerate", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE},
+ {"LongFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST},
+ {"MediumSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW},
+ {"MediumFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST},
+ {"ShortSlow", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW},
+ {"ShortFast", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST},
+ {"ShortTurbo", OptionsAction::Select, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO},
};
+
+ constexpr size_t presetCount = sizeof(presetOptions) / sizeof(presetOptions[0]);
+ static std::array presetLabels{};
+
+ auto bannerOptions =
+ createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void {
+ if (option.action == OptionsAction::Back) {
+ menuHandler::menuQueue = menuHandler::lora_Menu;
+ screen->runNow();
+ return;
+ }
+
+ if (!option.hasValue) {
+ return;
+ }
+
+ config.lora.modem_preset = option.value;
+ service->reloadConfig(SEGMENT_CONFIG);
+ rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
+ });
+
screen->showOverlayBanner(bannerOptions);
}
@@ -1041,12 +1060,13 @@ void menuHandler::switchToMUIMenu()
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{
- static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal",
- "Pink", "White"};
+ static const char *optionsArray[] = {
+ "Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Blue", "Teal", "Cyan", "Ice", "Pink",
+ "White", "Gray"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Screen Color";
bannerOptions.optionsArrayPtr = optionsArray;
- bannerOptions.optionsCount = 10;
+ bannerOptions.optionsCount = 14;
bannerOptions.bannerCallback = [display](int selected) -> void {
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
@@ -1082,20 +1102,40 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
TFT_MESH_g = 153;
TFT_MESH_b = 255;
} else if (selected == 7) {
- LOG_INFO("Setting color to Teal");
- TFT_MESH_r = 64;
- TFT_MESH_g = 224;
- TFT_MESH_b = 208;
+ LOG_INFO("Setting color to Blue");
+ TFT_MESH_r = 0;
+ TFT_MESH_g = 0;
+ TFT_MESH_b = 255;
} else if (selected == 8) {
+ LOG_INFO("Setting color to Teal");
+ TFT_MESH_r = 16;
+ TFT_MESH_g = 102;
+ TFT_MESH_b = 102;
+ } else if (selected == 9) {
+ LOG_INFO("Setting color to Cyan");
+ TFT_MESH_r = 0;
+ TFT_MESH_g = 255;
+ TFT_MESH_b = 255;
+ } else if (selected == 10) {
+ LOG_INFO("Setting color to Ice");
+ TFT_MESH_r = 173;
+ TFT_MESH_g = 216;
+ TFT_MESH_b = 230;
+ } else if (selected == 11) {
LOG_INFO("Setting color to Pink");
TFT_MESH_r = 255;
TFT_MESH_g = 105;
TFT_MESH_b = 180;
- } else if (selected == 9) {
+ } else if (selected == 12) {
LOG_INFO("Setting color to White");
TFT_MESH_r = 255;
TFT_MESH_g = 255;
TFT_MESH_b = 255;
+ } else if (selected == 13) {
+ LOG_INFO("Setting color to Gray");
+ TFT_MESH_r = 128;
+ TFT_MESH_g = 128;
+ TFT_MESH_b = 128;
} else {
menuQueue = system_base_menu;
screen->runNow();
diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h
index a611b7c9d..df7c2739b 100644
--- a/src/graphics/draw/MenuHandler.h
+++ b/src/graphics/draw/MenuHandler.h
@@ -99,5 +99,24 @@ class menuHandler
static void BluetoothToggleMenu();
};
+/* Generic Menu Options designations */
+enum class OptionsAction { Back, Select };
+
+template struct MenuOption {
+ const char *label;
+ OptionsAction action;
+ bool hasValue;
+ T value;
+
+ MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn)
+ : label(labelIn), action(actionIn), hasValue(true), value(valueIn)
+ {
+ }
+
+ MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
+};
+
+using RadioPresetOption = MenuOption;
+
} // namespace graphics
#endif
\ No newline at end of file
diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp
index bed2b7b7c..aa54ef2f1 100644
--- a/src/graphics/emotes.cpp
+++ b/src/graphics/emotes.cpp
@@ -13,41 +13,81 @@ const Emote emotes[] = {
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // đ Thumbs Down
// --- Smileys (Multiple Unicode Aliases) ---
- {"\U0001F60A", Smiling_Eyes, Smiling_Eyes_width, Smiling_Eyes_height}, // đ Smiling Eyes
- {"\U0001F600", Grinning, Grinning_width, Grinning_height}, // đ Grinning Face
- {"\U0001F642", Slightly_Smiling, Slightly_Smiling_width, Slightly_Smiling_height}, // đ Slightly Smiling Face
- {"\U0001F609", Winking_Face, Winking_Face_width, Winking_Face_height}, // đ Winking Face
- {"\U0001F601", Grinning_Smiling_Eyes, Grinning_Smiling_Eyes_width, Grinning_Smiling_Eyes_height}, // đ Grinning Smiling Eyes
- {"\U0001F60D", Heart_eyes, Heart_eyes_width, Heart_eyes_height}, // đ Heart Eyes
+ {"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // đ Smiling Eyes
+ {"\U0001F600", grinning, grinning_width, grinning_height}, // đ Grinning Face
+ {"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // đ Slightly Smiling Face
+ {"\U0001F609", winking_face, winking_face_width, winking_face_height}, // đ Winking Face
+ {"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // đ Grinning Smiling Eyes
+ {"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // đ Heart Eyes
{"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // đĨ° Smiling Face with Hearts
// --- Question/Alert ---
- {"\u2753", question, question_width, question_height}, // â Question Mark
- {"\u203C\uFE0F", bang, bang_width, bang_height}, // âŧī¸ Double Exclamation Mark
+ {"\u2753", question, question_width, question_height}, // â Question Mark
+ {"\u203C\uFE0F", bang, bang_width, bang_height}, // âŧī¸ Double Exclamation Mark
+ {"\u26A0\uFE0F", caution, caution_width, caution_height}, // â ī¸ Warning Sign
// --- Laughing Faces ---
{"\U0001F602", haha, haha_width, haha_height}, // đ Face with Tears of Joy
- {"\U0001F923", ROFL, ROFL_width, ROFL_height}, // đ¤Ŗ Rolling on the Floor Laughing
- {"\U0001F606", Smiling_Closed_Eyes, Smiling_Closed_Eyes_width, Smiling_Closed_Eyes_height}, // đ Smiling Closed Eyes
+ {"\U0001F923", rofl, rofl_width, rofl_height}, // đ¤Ŗ Rolling on the Floor Laughing
+ {"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // đ Smiling Closed Eyes
{"\U0001F605", haha, haha_width, haha_height}, // đ
Smiling with Sweat
- {"\U0001F604", Grinning_SmilingEyes2, Grinning_SmilingEyes2_width,
- Grinning_SmilingEyes2_height}, // đ Grinning Face with Smiling Eyes
- {"\U0001F62D", Loudly_Crying_Face, Loudly_Crying_Face_width, Loudly_Crying_Face_height}, // đ Loudly Crying Face
+ {"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width,
+ grinning_smiling_eyes_2_height}, // đ Grinning Face with Smiling Eyes
+ {"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // đ Loudly Crying Face
+ {"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 𤎠Face Vomiting
+ {"\U0001F60E", cool, cool_width, cool_height}, // đ Smiling Face with Sunglasses
+ {"\U0001F440", eyes, eyes_width, eyes_height}, // đ Eyes
+ {"\U0001F441\uFE0F", eye, eye_width, eye_height}, // đī¸ Eye
// --- Gestures and People ---
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // đ Waving Hand
{"\u270C\uFE0F", peace_sign, peace_sign_width, peace_sign_height}, // âī¸ Victory Hand
{"\U0001F596", vulcan_salute, vulcan_salute_width, vulcan_salute_height}, // đ Vulcan Salute
- {"\U0001F64F", Praying, Praying_width, Praying_height}, // đ Praying Hands
+ {"\U0001F64F", praying, praying_width, praying_height}, // đ Praying Hands
+ {"\U0001F4AA", strong, strong_width, strong_height}, // đĒ Flexed Biceps
+ {"\U0001F937", shrug, shrug_width, shrug_height}, // 𤡠Person Shrugging
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // đ¤ Cowboy Hat Face
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // đ§ Headphones
+ // --- Symbols ---
+ {"\u2714\uFE0F", check_mark, check_mark_width, check_mark_height}, // âī¸ Check Mark
+ {"\u2705", check_mark, check_mark_width, check_mark_height}, // â
Check Mark Button
+ {"\u2611\uFE0F", check_mark, check_mark_width, check_mark_height}, // âī¸ Check Box with Check
+ {"\U0001F3E0", house, house_width, house_height}, // đ House
+
// --- Weather ---
- {"\u2600", sun, sun_width, sun_height}, // â Sun (without variation selector)
- {"\u2600\uFE0F", sun, sun_width, sun_height}, // âī¸ Sun (with variation selector)
- {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // đ§ī¸ Cloud with Rain
- {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // âī¸ Cloud
- {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // đĢī¸ Fog
+ {"\u2600", sun, sun_width, sun_height}, // â Sun (without variation selector)
+ {"\u2600\uFE0F", sun, sun_width, sun_height}, // âī¸ Sun (with variation selector)
+ {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // đ§ī¸ Cloud with Rain
+ {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // âī¸ Cloud
+ {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // đĢī¸ Fog
+ {"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // âī¸ Snowflake
+ {"\U0001F4A7", drop, drop_width, drop_height}, // đ§ Droplet
+ {"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // đĄī¸ Thermometer
+ {"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width,
+ sun_behind_raincloud_height}, // đĻī¸ Sun Behind Rain Cloud
+ {"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // â
Sun Behind Cloud
+ {"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // â
ī¸ Sun Behind Cloud
+ {"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // đ¨ī¸ Cloud with Snow
+ {"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width,
+ cloud_with_lightning_height}, // đŠī¸ Cloud with Lightning
+ {"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
+ cloud_with_lightning_rain_height}, // â Cloud with Lightning and Rain
+ {"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
+ cloud_with_lightning_rain_height}, // âī¸ Cloud with Lightning and Rain
+ {"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // đŦī¸ Wind Face
+
+ // --- Moon Phases ---
+ {"\U0001F311", new_moon, new_moon_width, new_moon_height}, // đ New Moon
+ {"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // đ Waxing Crescent Moon
+ {"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // đ First Quarter Moon
+ {"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // đ Waxing Gibbous Moon
+ {"\U0001F315", full_moon, full_moon_width, full_moon_height}, // đ Full Moon
+ {"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // đ Waning Gibbous Moon
+ {"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // đ Last Quarter Moon
+ {"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // đ Waning Crescent Moon
+ {"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width,
+ first_quarter_moon_face_height}, // đ First Quarter Moon Face
// --- Misc Faces ---
{"\U0001F608", devil, devil_width, devil_height}, // đ Smiling Face with Horns
@@ -67,13 +107,49 @@ const Emote emotes[] = {
{"\U0001F498", heart, heart_width, heart_height}, // đ Heart with Arrow
// --- Objects ---
- {"\U0001F4A9", poo, poo_width, poo_height}, // đŠ Pile of Poo
- {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // đ Bell
- {"\U0001F36A", cookie, cookie_width, cookie_height}, // đĒ Cookie
- {"\U0001F525", Fire, Fire_width, Fire_height}, // đĨ Fire
- {"\u2728", Sparkles, Sparkles_width, Sparkles_height}, // ⨠Sparkles
- {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // đŗī¸ Hole
- {"\U0001F3B3", bowling, bowling_width, bowling_height} // đŗ Bowling
+ {"\U0001F4A9", poo, poo_width, poo_height}, // đŠ Pile of Poo
+ {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}, // đ Bell
+ {"\U0001F4CB", clipboard, clipboard_width, clipboard_height}, // đ Clipboard
+ {"\U0001F36A", cookie, cookie_width, cookie_height}, // đĒ Cookie
+ {"\U0001F370", shortcake, shortcake_width, shortcake_height}, // đ° Shortcake
+ {"\U0001F351", peach, peach_width, peach_height}, // đ Peach
+ {"\U0001F983", turkey, turkey_width, turkey_height}, // đĻ Turkey
+ {"\U0001F357", turkey_leg, turkey_leg_width, turkey_leg_height}, // đ Poultry Leg
+ {"\U0001F525", fire, fire_width, fire_height}, // đĨ Fire
+ {"\u2728", sparkles, sparkles_width, sparkles_height}, // ⨠Sparkles
+ {"\U0001F573\uFE0F", hole, hole_width, hole_height}, // đŗī¸ Hole
+ {"\U0001F3B3", bowling, bowling_width, bowling_height}, // đŗ Bowling
+
+ // --- Arrows ---
+ {"\u2193", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // â Downwards Arrow
+ {"\u2193\uFE0E", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // âī¸ Downwards Arrow (text)
+ {"\u2193\uFE0F", downwards_arrow, downwards_arrow_width, downwards_arrow_height}, // âī¸ Downwards Arrow (emoji)
+ {"\u2199", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // â South West Arrow
+ {"\u2199\uFE0E", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // âī¸ South West Arrow (text)
+ {"\u2199\uFE0F", south_west_arrow, south_west_arrow_width, south_west_arrow_height}, // âī¸ South West Arrow (emoji)
+ {"\u2190", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // â Leftwards Arrow
+ {"\u2190\uFE0E", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // âī¸ Leftwards Arrow (text)
+ {"\u2190\uFE0F", leftwards_arrow, leftwards_arrow_width, leftwards_arrow_height}, // âī¸ Leftwards Arrow (emoji)
+ {"\u2196", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // â North West Arrow
+ {"\u2196\uFE0E", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // âī¸ North West Arrow (text)
+ {"\u2196\uFE0F", north_west_arrow, north_west_arrow_width, north_west_arrow_height}, // âī¸ North West Arrow (emoji)
+ {"\u2191", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // â Upwards Arrow
+ {"\u2191\uFE0E", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // âī¸ Upwards Arrow (text)
+ {"\u2191\uFE0F", upwards_arrow, upwards_arrow_width, upwards_arrow_height}, // âī¸ Upwards Arrow (emoji)
+ {"\u2197", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // â North East Arrow
+ {"\u2197\uFE0E", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // âī¸ North East Arrow (text)
+ {"\u2197\uFE0F", north_east_arrow, north_east_arrow_width, north_east_arrow_height}, // âī¸ North East Arrow (emoji)
+ {"\u2192", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // â Rightwards Arrow
+ {"\u2192\uFE0E", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // âī¸ Rightwards Arrow (text)
+ {"\u2192\uFE0F", rightwards_arrow, rightwards_arrow_width, rightwards_arrow_height}, // âī¸ Rightwards Arrow (emoji)
+ {"\u2198", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // â South East Arrow
+ {"\u2198\uFE0E", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // âī¸ South East Arrow (text)
+ {"\u2198\uFE0F", south_east_arrow, south_east_arrow_width, south_east_arrow_height}, // âī¸ South East Arrow (emoji)
+
+ // --- Halloween ---
+ {"\U0001F383", jack_o_lantern, jack_o_lantern_width, jack_o_lantern_height}, // đ Jack-O-Lantern
+ {"\U0001F47B", ghost, ghost_width, ghost_height}, // đģ Ghost
+ {"\U0001F480", skull, skull_width, skull_height} // đ Skull
#endif
};
@@ -88,23 +164,23 @@ const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0
0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02,
0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00};
-const unsigned char Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
+const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43,
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
+const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Slightly_Smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
+const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Winking_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
+const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Grinning_Smiling_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
+const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
@@ -112,7 +188,7 @@ const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18,
0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23,
0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00};
-const unsigned char Heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
+const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48,
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
@@ -128,19 +204,19 @@ const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04,
0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char ROFL[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
+const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44,
0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Smiling_Closed_Eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
+const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Grinning_SmilingEyes2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
- 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
- 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
+const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
+ 0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
+ 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Loudly_Crying_Face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
+const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A,
0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00};
@@ -192,7 +268,7 @@ const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04
0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40,
0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
+const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70,
0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C};
@@ -200,11 +276,11 @@ const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25,
0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
-const unsigned char Praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
+const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21,
0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC};
-const unsigned char Sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
+const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E,
0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00};
@@ -227,7 +303,179 @@ const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x0
const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A,
0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22,
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05,
+ 0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D,
+ 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F};
+
+const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF,
+ 0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C,
+ 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31};
+
+const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7,
+ 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E,
+ 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D};
+
+const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22,
+ 0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43,
+ 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F};
+
+const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA,
+ 0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
+ 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0,
+ 0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7,
+ 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00};
+
+const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60,
+ 0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E,
+ 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00};
+
+const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA,
+ 0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51,
+ 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F};
+
+const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60,
+ 0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05,
+ 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0,
+ 0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F,
+ 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00};
+
+const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8,
+ 0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47,
+ 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38};
+
+const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE,
+ 0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12,
+ 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC,
+ 0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
+ 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02,
+ 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
+ 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01,
+ 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01};
+
+const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
+ 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21,
+ 0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01};
+
+const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9,
+ 0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85,
+ 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00};
+
+const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02,
+ 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40,
+ 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02,
+ 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78,
+ 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02,
+ 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F,
+ 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2,
+ 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F,
+ 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE,
+ 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
+ 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE,
+ 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43,
+ 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE,
+ 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40,
+ 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E,
+ 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40,
+ 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
+
+const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00,
+ 0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80,
+ 0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07};
+
+const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02,
+ 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40,
+ 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03};
+
+const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2,
+ 0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50,
+ 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07};
+
+const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88,
+ 0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80,
+ 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C};
+
+const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00,
+ 0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00,
+ 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00};
+
+const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8,
+ 0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F,
+ 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00};
+
+const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE,
+ 0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F,
+ 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00,
+ 0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00,
+ 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
+ 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F,
+ 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01};
+
+const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C,
+ 0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00,
+ 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC,
+ 0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03,
+ 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00,
+ 0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C,
+ 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00};
+
+const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A,
+ 0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82,
+ 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F};
+
+const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00,
+ 0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01,
+ 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00};
+
+const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04,
+ 0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55,
+ 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F};
+
+const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90,
+ 0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48,
+ 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F};
+
+const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F,
+ 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
+ 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00};
+
+const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4,
+ 0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F,
+ 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#endif
} // namespace graphics
-#endif
\ No newline at end of file
+#endif
diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h
index b1b2d16da..0637712cc 100644
--- a/src/graphics/emotes.h
+++ b/src/graphics/emotes.h
@@ -22,33 +22,33 @@ extern const int numEmotes;
extern const unsigned char thumbup[] PROGMEM;
extern const unsigned char thumbdown[] PROGMEM;
-#define Smiling_Eyes_height 16
-#define Smiling_Eyes_width 16
-extern const unsigned char Smiling_Eyes[] PROGMEM;
+#define smiling_eyes_height 16
+#define smiling_eyes_width 16
+extern const unsigned char smiling_eyes[] PROGMEM;
-#define Grinning_height 16
-#define Grinning_width 16
-extern const unsigned char Grinning[] PROGMEM;
+#define grinning_height 16
+#define grinning_width 16
+extern const unsigned char grinning[] PROGMEM;
-#define Slightly_Smiling_height 16
-#define Slightly_Smiling_width 16
-extern const unsigned char Slightly_Smiling[] PROGMEM;
+#define slightly_smiling_height 16
+#define slightly_smiling_width 16
+extern const unsigned char slightly_smiling[] PROGMEM;
-#define Winking_Face_height 16
-#define Winking_Face_width 16
-extern const unsigned char Winking_Face[] PROGMEM;
+#define winking_face_height 16
+#define winking_face_width 16
+extern const unsigned char winking_face[] PROGMEM;
-#define Grinning_Smiling_Eyes_height 16
-#define Grinning_Smiling_Eyes_width 16
-extern const unsigned char Grinning_Smiling_Eyes[] PROGMEM;
+#define grinning_smiling_eyes_height 16
+#define grinning_smiling_eyes_width 16
+extern const unsigned char grinning_smiling_eyes[] PROGMEM;
#define heart_smile_height 16
#define heart_smile_width 16
extern const unsigned char heart_smile[] PROGMEM;
-#define Heart_eyes_height 16
-#define Heart_eyes_width 16
-extern const unsigned char Heart_eyes[] PROGMEM;
+#define heart_eyes_height 16
+#define heart_eyes_width 16
+extern const unsigned char heart_eyes[] PROGMEM;
#define question_height 16
#define question_width 16
@@ -62,21 +62,21 @@ extern const unsigned char bang[] PROGMEM;
#define haha_width 16
extern const unsigned char haha[] PROGMEM;
-#define ROFL_height 16
-#define ROFL_width 16
-extern const unsigned char ROFL[] PROGMEM;
+#define rofl_height 16
+#define rofl_width 16
+extern const unsigned char rofl[] PROGMEM;
-#define Smiling_Closed_Eyes_height 16
-#define Smiling_Closed_Eyes_width 16
-extern const unsigned char Smiling_Closed_Eyes[] PROGMEM;
+#define smiling_closed_eyes_height 16
+#define smiling_closed_eyes_width 16
+extern const unsigned char smiling_closed_eyes[] PROGMEM;
-#define Grinning_SmilingEyes2_height 16
-#define Grinning_SmilingEyes2_width 16
-extern const unsigned char Grinning_SmilingEyes2[] PROGMEM;
+#define grinning_smiling_eyes_2_height 16
+#define grinning_smiling_eyes_2_width 16
+extern const unsigned char grinning_smiling_eyes_2[] PROGMEM;
-#define Loudly_Crying_Face_height 16
-#define Loudly_Crying_Face_width 16
-extern const unsigned char Loudly_Crying_Face[] PROGMEM;
+#define loudly_crying_face_height 16
+#define loudly_crying_face_width 16
+extern const unsigned char loudly_crying_face[] PROGMEM;
#define wave_icon_height 16
#define wave_icon_width 16
@@ -126,21 +126,21 @@ extern const unsigned char bell_icon[] PROGMEM;
#define cookie_height 16
extern const unsigned char cookie[] PROGMEM;
-#define Fire_width 16
-#define Fire_height 16
-extern const unsigned char Fire[] PROGMEM;
+#define fire_width 16
+#define fire_height 16
+extern const unsigned char fire[] PROGMEM;
#define peace_sign_width 16
#define peace_sign_height 16
extern const unsigned char peace_sign[] PROGMEM;
-#define Praying_width 16
-#define Praying_height 16
-extern const unsigned char Praying[] PROGMEM;
+#define praying_width 16
+#define praying_height 16
+extern const unsigned char praying[] PROGMEM;
-#define Sparkles_width 16
-#define Sparkles_height 16
-extern const unsigned char Sparkles[] PROGMEM;
+#define sparkles_width 16
+#define sparkles_height 16
+extern const unsigned char sparkles[] PROGMEM;
#define clown_width 16
#define clown_height 16
@@ -161,6 +161,178 @@ extern const unsigned char bowling[] PROGMEM;
#define vulcan_salute_width 16
#define vulcan_salute_height 16
extern const unsigned char vulcan_salute[] PROGMEM;
+
+#define jack_o_lantern_width 16
+#define jack_o_lantern_height 16
+extern const unsigned char jack_o_lantern[] PROGMEM;
+
+#define ghost_width 16
+#define ghost_height 16
+extern const unsigned char ghost[] PROGMEM;
+
+#define skull_width 16
+#define skull_height 16
+extern const unsigned char skull[] PROGMEM;
+
+#define vomiting_width 16
+#define vomiting_height 16
+extern const unsigned char vomiting[] PROGMEM;
+
+#define cool_width 16
+#define cool_height 16
+extern const unsigned char cool[] PROGMEM;
+
+#define shortcake_width 16
+#define shortcake_height 16
+extern const unsigned char shortcake[] PROGMEM;
+
+#define caution_width 16
+#define caution_height 16
+extern const unsigned char caution[] PROGMEM;
+
+#define clipboard_width 16
+#define clipboard_height 16
+extern const unsigned char clipboard[] PROGMEM;
+
+#define snowflake_width 16
+#define snowflake_height 16
+extern const unsigned char snowflake[] PROGMEM;
+
+#define drop_width 16
+#define drop_height 16
+extern const unsigned char drop[] PROGMEM;
+
+#define thermometer_width 16
+#define thermometer_height 16
+extern const unsigned char thermometer[] PROGMEM;
+
+#define sun_behind_raincloud_width 16
+#define sun_behind_raincloud_height 16
+extern const unsigned char sun_behind_raincloud[] PROGMEM;
+
+#define sun_behind_cloud_width 16
+#define sun_behind_cloud_height 16
+extern const unsigned char sun_behind_cloud[] PROGMEM;
+
+#define cloud_with_snow_width 16
+#define cloud_with_snow_height 16
+extern const unsigned char cloud_with_snow[] PROGMEM;
+
+#define cloud_with_lightning_width 16
+#define cloud_with_lightning_height 16
+extern const unsigned char cloud_with_lightning[] PROGMEM;
+
+#define cloud_with_lightning_rain_width 16
+#define cloud_with_lightning_rain_height 16
+extern const unsigned char cloud_with_lightning_rain[] PROGMEM;
+
+#define wind_face_width 16
+#define wind_face_height 16
+extern const unsigned char wind_face[] PROGMEM;
+
+#define new_moon_width 16
+#define new_moon_height 16
+extern const unsigned char new_moon[] PROGMEM;
+
+#define waxing_crescent_moon_width 16
+#define waxing_crescent_moon_height 16
+extern const unsigned char waxing_crescent_moon[] PROGMEM;
+
+#define first_quarter_moon_width 16
+#define first_quarter_moon_height 16
+extern const unsigned char first_quarter_moon[] PROGMEM;
+
+#define waxing_gibbous_moon_width 16
+#define waxing_gibbous_moon_height 16
+extern const unsigned char waxing_gibbous_moon[] PROGMEM;
+
+#define full_moon_width 16
+#define full_moon_height 16
+extern const unsigned char full_moon[] PROGMEM;
+
+#define waning_gibbous_moon_width 16
+#define waning_gibbous_moon_height 16
+extern const unsigned char waning_gibbous_moon[] PROGMEM;
+
+#define last_quarter_moon_width 16
+#define last_quarter_moon_height 16
+extern const unsigned char last_quarter_moon[] PROGMEM;
+
+#define waning_crescent_moon_width 16
+#define waning_crescent_moon_height 16
+extern const unsigned char waning_crescent_moon[] PROGMEM;
+
+#define first_quarter_moon_face_width 16
+#define first_quarter_moon_face_height 16
+extern const unsigned char first_quarter_moon_face[] PROGMEM;
+
+#define peach_width 16
+#define peach_height 16
+extern const unsigned char peach[] PROGMEM;
+
+#define turkey_width 16
+#define turkey_height 16
+extern const unsigned char turkey[] PROGMEM;
+
+#define turkey_leg_width 16
+#define turkey_leg_height 16
+extern const unsigned char turkey_leg[] PROGMEM;
+
+#define south_west_arrow_width 16
+#define south_west_arrow_height 16
+extern const unsigned char south_west_arrow[] PROGMEM;
+
+#define south_east_arrow_width 16
+#define south_east_arrow_height 16
+extern const unsigned char south_east_arrow[] PROGMEM;
+
+#define north_west_arrow_width 16
+#define north_west_arrow_height 16
+extern const unsigned char north_west_arrow[] PROGMEM;
+
+#define north_east_arrow_width 16
+#define north_east_arrow_height 16
+extern const unsigned char north_east_arrow[] PROGMEM;
+
+#define downwards_arrow_width 16
+#define downwards_arrow_height 16
+extern const unsigned char downwards_arrow[] PROGMEM;
+
+#define leftwards_arrow_width 16
+#define leftwards_arrow_height 16
+extern const unsigned char leftwards_arrow[] PROGMEM;
+
+#define upwards_arrow_width 16
+#define upwards_arrow_height 16
+extern const unsigned char upwards_arrow[] PROGMEM;
+
+#define rightwards_arrow_width 16
+#define rightwards_arrow_height 16
+extern const unsigned char rightwards_arrow[] PROGMEM;
+
+#define strong_width 16
+#define strong_height 16
+extern const unsigned char strong[] PROGMEM;
+
+#define check_mark_width 16
+#define check_mark_height 16
+extern const unsigned char check_mark[] PROGMEM;
+
+#define house_width 16
+#define house_height 16
+extern const unsigned char house[] PROGMEM;
+
+#define shrug_width 16
+#define shrug_height 16
+extern const unsigned char shrug[] PROGMEM;
+
+#define eyes_width 16
+#define eyes_height 16
+extern const unsigned char eyes[] PROGMEM;
+
+#define eye_width 16
+#define eye_height 16
+extern const unsigned char eye[] PROGMEM;
#endif // EXCLUDE_EMOJI
-} // namespace graphics
\ No newline at end of file
+} // namespace graphics
diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h
index 802186e6e..b35ca5cc0 100644
--- a/src/graphics/niche/InkHUD/Applet.h
+++ b/src/graphics/niche/InkHUD/Applet.h
@@ -88,8 +88,14 @@ class Applet : public GFX
virtual void onForeground() {}
virtual void onBackground() {}
virtual void onShutdown() {}
- virtual void onButtonShortPress() {} // (System Applets only)
- virtual void onButtonLongPress() {} // (System Applets only)
+ virtual void onButtonShortPress() {}
+ virtual void onButtonLongPress() {}
+ virtual void onExitShort() {}
+ virtual void onExitLong() {}
+ virtual void onNavUp() {}
+ virtual void onNavDown() {}
+ virtual void onNavLeft() {}
+ virtual void onNavRight() {}
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp
new file mode 100644
index 000000000..67ef87f41
--- /dev/null
+++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp
@@ -0,0 +1,205 @@
+#ifdef MESHTASTIC_INCLUDE_INKHUD
+
+#include "./AlignStickApplet.h"
+
+using namespace NicheGraphics;
+
+InkHUD::AlignStickApplet::AlignStickApplet()
+{
+ if (!settings->joystick.aligned)
+ bringToForeground();
+}
+
+void InkHUD::AlignStickApplet::onRender()
+{
+ setFont(fontMedium);
+ printAt(0, 0, "Align Joystick:");
+ setFont(fontSmall);
+ std::string instructions = "Move joystick in the direction indicated";
+ printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions);
+
+ // Size of the region in which the joystick graphic should fit
+ uint16_t joyXLimit = X(0.8);
+ uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1;
+ if (getTextWidth(instructions) > width())
+ contentH += fontSmall.lineHeight();
+ uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2;
+ uint16_t joyYLimit = freeY * 0.8;
+
+ // Use the shorter of the two
+ uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit;
+
+ // Center the joystick graphic
+ uint16_t centerX = X(0.5);
+ uint16_t centerY = contentH + freeY * 0.5;
+
+ // Draw joystick graphic
+ drawStick(centerX, centerY, joyWidth);
+
+ setFont(fontSmall);
+ printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM);
+}
+
+// Draw a scalable joystick graphic
+void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width)
+{
+ if (width < 9) // too small to draw
+ return;
+
+ else if (width < 40) { // only draw up arrow
+ uint16_t chamfer = width < 20 ? 1 : 2;
+
+ // Draw filled up arrow
+ drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK);
+
+ } else { // large enough to draw the full thing
+ uint16_t chamfer = width < 80 ? 1 : 2;
+ uint16_t stroke = 3; // pixels
+ uint16_t arrowW = width * 0.22;
+ uint16_t hollowW = arrowW - stroke * 2;
+
+ // Draw center circle
+ fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK);
+ fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE);
+
+ // Draw filled up arrow
+ drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK);
+
+ // Draw down arrow
+ drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK);
+ drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE);
+
+ // Draw left arrow
+ drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK);
+ drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE);
+
+ // Draw right arrow
+ drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK);
+ drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE);
+ }
+}
+
+// Draw a scalable joystick direction arrow
+// a right-triangle with blunted tips
+/*
+ _ <--point
+ ^ / \
+ | / \
+ size / \
+ | / \
+ v |_________|
+
+*/
+void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size,
+ uint16_t chamfer, Color color)
+{
+ uint16_t chamferW = chamfer * 2 + 1;
+ uint16_t triangleW = size - chamferW;
+
+ // Draw arrow
+ switch (direction) {
+ case Direction::UP:
+ fillRect(pointX - chamfer, pointY, chamferW, triangleW, color);
+ fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color);
+ fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer,
+ pointY + triangleW, color);
+ fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer,
+ pointY + triangleW, color);
+ break;
+ case Direction::DOWN:
+ fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color);
+ fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color);
+ fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer,
+ pointY - triangleW, color);
+ fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer,
+ pointY - triangleW, color);
+ break;
+ case Direction::LEFT:
+ fillRect(pointX, pointY - chamfer, triangleW, chamferW, color);
+ fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
+ fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW,
+ pointY - chamfer, color);
+ fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW,
+ pointY + chamfer, color);
+ break;
+ case Direction::RIGHT:
+ fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color);
+ fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
+ fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW,
+ pointY - chamfer, color);
+ fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW,
+ pointY + chamfer, color);
+ break;
+ }
+}
+
+void InkHUD::AlignStickApplet::onForeground()
+{
+ // Prevent most other applets from requesting update, and skip their rendering entirely
+ // Another system applet with a higher precedence can potentially ignore this
+ SystemApplet::lockRendering = true;
+ SystemApplet::lockRequests = true;
+
+ handleInput = true; // Intercept the button input for our applet
+}
+
+void InkHUD::AlignStickApplet::onBackground()
+{
+ // Allow normal update behavior to resume
+ SystemApplet::lockRendering = false;
+ SystemApplet::lockRequests = false;
+ SystemApplet::handleInput = false;
+
+ // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
+ // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onButtonLongPress()
+{
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onExitLong()
+{
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onNavUp()
+{
+ settings->joystick.aligned = true;
+
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onNavDown()
+{
+ inkhud->rotateJoystick(2); // 180 deg
+ settings->joystick.aligned = true;
+
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onNavLeft()
+{
+ inkhud->rotateJoystick(3); // 270 deg
+ settings->joystick.aligned = true;
+
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::AlignStickApplet::onNavRight()
+{
+ inkhud->rotateJoystick(1); // 90 deg
+ settings->joystick.aligned = true;
+
+ sendToBackground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+#endif
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h
new file mode 100644
index 000000000..8dba33165
--- /dev/null
+++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h
@@ -0,0 +1,50 @@
+#ifdef MESHTASTIC_INCLUDE_INKHUD
+
+/*
+
+System Applet for manually aligning the joystick with the screen
+
+should be run at startup if the joystick is enabled
+and not aligned to the screen
+
+*/
+
+#pragma once
+
+#include "configuration.h"
+
+#include "graphics/niche/InkHUD/SystemApplet.h"
+
+namespace NicheGraphics::InkHUD
+{
+
+class AlignStickApplet : public SystemApplet
+{
+ public:
+ AlignStickApplet();
+
+ void onRender() override;
+ void onForeground() override;
+ void onBackground() override;
+ void onButtonLongPress() override;
+ void onExitLong() override;
+ void onNavUp() override;
+ void onNavDown() override;
+ void onNavLeft() override;
+ void onNavRight() override;
+
+ protected:
+ enum Direction {
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT,
+ };
+
+ void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width);
+ void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color);
+};
+
+} // namespace NicheGraphics::InkHUD
+
+#endif
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
index c84ee09e0..debe2b719 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
@@ -30,6 +30,7 @@ enum MenuAction {
TOGGLE_AUTOSHOW_APPLET,
SET_RECENTS,
ROTATE,
+ ALIGN_JOYSTICK,
LAYOUT,
TOGGLE_BATTERY_ICON,
TOGGLE_NOTIFICATIONS,
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
index 09f76ed46..7e7093857 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
@@ -178,6 +178,10 @@ void InkHUD::MenuApplet::execute(MenuItem item)
inkhud->rotate();
break;
+ case ALIGN_JOYSTICK:
+ inkhud->openAlignStick();
+ break;
+
case LAYOUT:
// Todo: smarter incrementing of tile count
settings->userTiles.count++;
@@ -287,14 +291,17 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
// items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO
items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
+ previousPage = MenuPage::EXIT;
break;
case SEND:
populateSendPage();
+ previousPage = MenuPage::ROOT;
break;
case CANNEDMESSAGE_RECIPIENT:
populateRecipientPage();
+ previousPage = MenuPage::OPTIONS;
break;
case OPTIONS:
@@ -321,6 +328,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
if (settings->userTiles.maxCount > 1)
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
+ if (settings->joystick.enabled)
+ items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT));
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
&settings->optionalFeatures.notifications));
items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS,
@@ -332,20 +341,24 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
items.push_back(
MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock));
items.push_back(MenuItem("Exit", MenuPage::EXIT));
+ previousPage = MenuPage::ROOT;
break;
case APPLETS:
populateAppletPage();
items.push_back(MenuItem("Exit", MenuPage::EXIT));
+ previousPage = MenuPage::OPTIONS;
break;
case AUTOSHOW:
populateAutoshowPage();
items.push_back(MenuItem("Exit", MenuPage::EXIT));
+ previousPage = MenuPage::OPTIONS;
break;
case RECENTS:
populateRecentsPage();
+ previousPage = MenuPage::OPTIONS;
break;
case EXIT:
@@ -479,12 +492,21 @@ void InkHUD::MenuApplet::onButtonShortPress()
// Push the auto-close timer back
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
- // Move menu cursor to next entry, then update
- if (cursorShown)
- cursor = (cursor + 1) % items.size();
- else
- cursorShown = true;
- requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ if (!settings->joystick.enabled) {
+ // Move menu cursor to next entry, then update
+ if (cursorShown)
+ cursor = (cursor + 1) % items.size();
+ else
+ cursorShown = true;
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ } else {
+ if (cursorShown)
+ execute(items.at(cursor));
+ else
+ showPage(MenuPage::EXIT);
+ if (!wantsToRender())
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+ }
}
void InkHUD::MenuApplet::onButtonLongPress()
@@ -504,6 +526,62 @@ void InkHUD::MenuApplet::onButtonLongPress()
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
}
+void InkHUD::MenuApplet::onExitShort()
+{
+ // Exit the menu
+ showPage(MenuPage::EXIT);
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onNavUp()
+{
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+
+ // Move menu cursor to previous entry, then update
+ if (cursor == 0)
+ cursor = items.size() - 1;
+ else
+ cursor--;
+
+ if (!cursorShown)
+ cursorShown = true;
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onNavDown()
+{
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+
+ // Move menu cursor to next entry, then update
+ if (cursorShown)
+ cursor = (cursor + 1) % items.size();
+ else
+ cursorShown = true;
+
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onNavLeft()
+{
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+
+ // Go to the previous menu page
+ showPage(previousPage);
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
+void InkHUD::MenuApplet::onNavRight()
+{
+ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
+
+ if (cursorShown)
+ execute(items.at(cursor));
+ if (!wantsToRender())
+ requestUpdate(Drivers::EInk::UpdateTypes::FAST);
+}
+
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
void InkHUD::MenuApplet::populateAppletPage()
{
@@ -796,4 +874,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
cm.recipientItems.clear();
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
index 8f9280e6f..4f9f92227 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
@@ -27,6 +27,11 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
+ void onExitShort() override;
+ void onNavUp() override;
+ void onNavDown() override;
+ void onNavLeft() override;
+ void onNavRight() override;
void onRender() override;
void show(Tile *t); // Open the menu, onto a user tile
@@ -52,6 +57,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
MenuPage currentPage = MenuPage::ROOT;
+ MenuPage previousPage = MenuPage::EXIT;
uint8_t cursor = 0; // Which menu item is currently highlighted
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
@@ -97,4 +103,4 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
} // namespace NicheGraphics::InkHUD
-#endif
\ No newline at end of file
+#endif
diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp
index ae0836d19..2ea9c7fe0 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp
@@ -153,6 +153,42 @@ void InkHUD::NotificationApplet::onButtonLongPress()
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
}
+void InkHUD::NotificationApplet::onExitShort()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::NotificationApplet::onExitLong()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::NotificationApplet::onNavUp()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::NotificationApplet::onNavDown()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::NotificationApplet::onNavLeft()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
+void InkHUD::NotificationApplet::onNavRight()
+{
+ dismiss();
+ inkhud->forceUpdate(EInk::UpdateTypes::FULL);
+}
+
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
// Called internally when we first get a "notifiable event", and then again before render,
// in case autoshow swapped which applet was displayed
diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h
index 66df784b4..16ea13407 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h
@@ -31,6 +31,12 @@ class NotificationApplet : public SystemApplet
void onBackground() override;
void onButtonShortPress() override;
void onButtonLongPress() override;
+ void onExitShort() override;
+ void onExitLong() override;
+ void onNavUp() override;
+ void onNavDown() override;
+ void onNavLeft() override;
+ void onNavRight() override;
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp
index ade44ab65..a9d579873 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp
@@ -112,12 +112,21 @@ void InkHUD::TipsApplet::onRender()
setFont(fontSmall);
int16_t cursorY = fontMedium.lineHeight() * 1.5;
- printAt(0, cursorY, "User Button");
- cursorY += fontSmall.lineHeight() * 1.2;
- printAt(0, cursorY, "- short press: next");
- cursorY += fontSmall.lineHeight() * 1.2;
- printAt(0, cursorY, "- long press: select / open menu");
- cursorY += fontSmall.lineHeight() * 1.5;
+ if (!settings->joystick.enabled) {
+ printAt(0, cursorY, "User Button");
+ cursorY += fontSmall.lineHeight() * 1.2;
+ printAt(0, cursorY, "- short press: next");
+ cursorY += fontSmall.lineHeight() * 1.2;
+ printAt(0, cursorY, "- long press: select / open menu");
+ } else {
+ printAt(0, cursorY, "Joystick");
+ cursorY += fontSmall.lineHeight() * 1.2;
+ printAt(0, cursorY, "- open menu / select");
+ cursorY += fontSmall.lineHeight() * 1.5;
+ printAt(0, cursorY, "Exit Button");
+ cursorY += fontSmall.lineHeight() * 1.2;
+ printAt(0, cursorY, "- switch tile / close menu");
+ }
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
} break;
@@ -127,8 +136,13 @@ void InkHUD::TipsApplet::onRender()
printAt(0, 0, "Tip: Rotation");
setFont(fontSmall);
- printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
- "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
+ if (!settings->joystick.enabled) {
+ printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
+ "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
+ } else {
+ printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
+ "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
+ }
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
@@ -232,4 +246,10 @@ void InkHUD::TipsApplet::onButtonShortPress()
requestUpdate();
}
+// Functions the same as the user button in this instance
+void InkHUD::TipsApplet::onExitShort()
+{
+ onButtonShortPress();
+}
+
#endif
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h
index db88585e9..159e6f58f 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h
@@ -36,6 +36,7 @@ class TipsApplet : public SystemApplet
void onForeground() override;
void onBackground() override;
void onButtonShortPress() override;
+ void onExitShort() override;
protected:
void renderWelcome(); // Very first screen of tutorial
diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp
index cdda1638d..5382d2391 100644
--- a/src/graphics/niche/InkHUD/Events.cpp
+++ b/src/graphics/niche/InkHUD/Events.cpp
@@ -55,10 +55,15 @@ void InkHUD::Events::onButtonShort()
}
// If no system applet is handling input, default behavior instead is to cycle applets
- if (consumer)
+ // or open menu if joystick is enabled
+ if (consumer) {
consumer->onButtonShortPress();
- else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
- inkhud->nextApplet();
+ } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
+ if (!settings->joystick.enabled)
+ inkhud->nextApplet();
+ else
+ inkhud->openMenu();
+ }
}
void InkHUD::Events::onButtonLong()
@@ -83,6 +88,156 @@ void InkHUD::Events::onButtonLong()
inkhud->openMenu();
}
+void InkHUD::Events::onExitShort()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Short tone
+ playChirp();
+ // Cancel any beeping, buzzing, blinking
+ // Some button handling suppressed if we are dismissing an external notification (see below)
+ bool dismissedExt = dismissExternalNotification();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ // If no system applet is handling input, default behavior instead is change tiles
+ if (consumer)
+ consumer->onExitShort();
+ else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
+ inkhud->nextTile();
+ }
+}
+
+void InkHUD::Events::onExitLong()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Slightly longer than playChirp
+ playBoop();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ if (consumer)
+ consumer->onExitLong();
+ }
+}
+
+void InkHUD::Events::onNavUp()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Short tone
+ playChirp();
+ // Cancel any beeping, buzzing, blinking
+ // Some button handling suppressed if we are dismissing an external notification (see below)
+ bool dismissedExt = dismissExternalNotification();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ if (consumer)
+ consumer->onNavUp();
+ }
+}
+
+void InkHUD::Events::onNavDown()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Short tone
+ playChirp();
+ // Cancel any beeping, buzzing, blinking
+ // Some button handling suppressed if we are dismissing an external notification (see below)
+ bool dismissedExt = dismissExternalNotification();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ if (consumer)
+ consumer->onNavDown();
+ }
+}
+
+void InkHUD::Events::onNavLeft()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Short tone
+ playChirp();
+ // Cancel any beeping, buzzing, blinking
+ // Some button handling suppressed if we are dismissing an external notification (see below)
+ bool dismissedExt = dismissExternalNotification();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ // If no system applet is handling input, default behavior instead is to cycle applets
+ if (consumer)
+ consumer->onNavLeft();
+ else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
+ inkhud->prevApplet();
+ }
+}
+
+void InkHUD::Events::onNavRight()
+{
+ if (settings->joystick.enabled) {
+ // Audio feedback (via buzzer)
+ // Short tone
+ playChirp();
+ // Cancel any beeping, buzzing, blinking
+ // Some button handling suppressed if we are dismissing an external notification (see below)
+ bool dismissedExt = dismissExternalNotification();
+
+ // Check which system applet wants to handle the button press (if any)
+ SystemApplet *consumer = nullptr;
+ for (SystemApplet *sa : inkhud->systemApplets) {
+ if (sa->handleInput) {
+ consumer = sa;
+ break;
+ }
+ }
+
+ // If no system applet is handling input, default behavior instead is to cycle applets
+ if (consumer)
+ consumer->onNavRight();
+ else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
+ inkhud->nextApplet();
+ }
+}
+
// Callback for deepSleepObserver
// Returns 0 to signal that we agree to sleep now
int InkHUD::Events::beforeDeepSleep(void *unused)
diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h
index df68f368c..664ca19f0 100644
--- a/src/graphics/niche/InkHUD/Events.h
+++ b/src/graphics/niche/InkHUD/Events.h
@@ -29,6 +29,12 @@ class Events
void onButtonShort(); // User button: short press
void onButtonLong(); // User button: long press
+ void onExitShort(); // Exit button: short press
+ void onExitLong(); // Exit button: long press
+ void onNavUp(); // Navigate up
+ void onNavDown(); // Navigate down
+ void onNavLeft(); // Navigate left
+ void onNavRight(); // Navigate right
int beforeDeepSleep(void *unused); // Prepare for shutdown
int beforeReboot(void *unused); // Prepare for reboot
diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp
index 90b6718e0..9f05ae5bb 100644
--- a/src/graphics/niche/InkHUD/InkHUD.cpp
+++ b/src/graphics/niche/InkHUD/InkHUD.cpp
@@ -80,6 +80,94 @@ void InkHUD::InkHUD::longpress()
events->onButtonLong();
}
+// Call this when your exit button gets a short press
+void InkHUD::InkHUD::exitShort()
+{
+ events->onExitShort();
+}
+
+// Call this when your exit button gets a long press
+void InkHUD::InkHUD::exitLong()
+{
+ events->onExitLong();
+}
+
+// Call this when your joystick gets an up input
+void InkHUD::InkHUD::navUp()
+{
+ switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
+ case 1: // 90 deg
+ events->onNavLeft();
+ break;
+ case 2: // 180 deg
+ events->onNavDown();
+ break;
+ case 3: // 270 deg
+ events->onNavRight();
+ break;
+ default: // 0 deg
+ events->onNavUp();
+ break;
+ }
+}
+
+// Call this when your joystick gets a down input
+void InkHUD::InkHUD::navDown()
+{
+ switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
+ case 1: // 90 deg
+ events->onNavRight();
+ break;
+ case 2: // 180 deg
+ events->onNavUp();
+ break;
+ case 3: // 270 deg
+ events->onNavLeft();
+ break;
+ default: // 0 deg
+ events->onNavDown();
+ break;
+ }
+}
+
+// Call this when your joystick gets a left input
+void InkHUD::InkHUD::navLeft()
+{
+ switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
+ case 1: // 90 deg
+ events->onNavDown();
+ break;
+ case 2: // 180 deg
+ events->onNavRight();
+ break;
+ case 3: // 270 deg
+ events->onNavUp();
+ break;
+ default: // 0 deg
+ events->onNavLeft();
+ break;
+ }
+}
+
+// Call this when your joystick gets a right input
+void InkHUD::InkHUD::navRight()
+{
+ switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
+ case 1: // 90 deg
+ events->onNavUp();
+ break;
+ case 2: // 180 deg
+ events->onNavLeft();
+ break;
+ case 3: // 270 deg
+ events->onNavDown();
+ break;
+ default: // 0 deg
+ events->onNavRight();
+ break;
+ }
+}
+
// Cycle the next user applet to the foreground
// Only activated applets are cycled
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
@@ -88,6 +176,14 @@ void InkHUD::InkHUD::nextApplet()
windowManager->nextApplet();
}
+// Cycle the previous user applet to the foreground
+// Only activated applets are cycled
+// If user has a multi-applet layout, the applets will cycle on the "focused tile"
+void InkHUD::InkHUD::prevApplet()
+{
+ windowManager->prevApplet();
+}
+
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::InkHUD::openMenu()
@@ -95,6 +191,12 @@ void InkHUD::InkHUD::openMenu()
windowManager->openMenu();
}
+// Bring AlignStick applet to the foreground
+void InkHUD::InkHUD::openAlignStick()
+{
+ windowManager->openAlignStick();
+}
+
// In layouts where multiple applets are shown at once, change which tile is focused
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
void InkHUD::InkHUD::nextTile()
@@ -102,12 +204,26 @@ void InkHUD::InkHUD::nextTile()
windowManager->nextTile();
}
+// In layouts where multiple applets are shown at once, change which tile is focused
+// The focused tile in the one which cycles applets on button short press, and displays menu on long press
+void InkHUD::InkHUD::prevTile()
+{
+ windowManager->prevTile();
+}
+
// Rotate the display image by 90 degrees
void InkHUD::InkHUD::rotate()
{
windowManager->rotate();
}
+// rotate the joystick in 90 degree increments
+void InkHUD::InkHUD::rotateJoystick(uint8_t angle)
+{
+ persistence->settings.joystick.alignment += angle;
+ persistence->settings.joystick.alignment %= 4;
+}
+
// Show / hide the battery indicator in top-right
void InkHUD::InkHUD::toggleBatteryIcon()
{
diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h
index 13839ea22..7325d8262 100644
--- a/src/graphics/niche/InkHUD/InkHUD.h
+++ b/src/graphics/niche/InkHUD/InkHUD.h
@@ -55,15 +55,25 @@ class InkHUD
void shortpress();
void longpress();
+ void exitShort();
+ void exitLong();
+ void navUp();
+ void navDown();
+ void navLeft();
+ void navRight();
// Trigger UI changes
// - called by various InkHUD components
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
void nextApplet();
+ void prevApplet();
void openMenu();
+ void openAlignStick();
void nextTile();
+ void prevTile();
void rotate();
+ void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
void toggleBatteryIcon();
// Updating the display
diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h
index b85274c87..5054b7234 100644
--- a/src/graphics/niche/InkHUD/Persistence.h
+++ b/src/graphics/niche/InkHUD/Persistence.h
@@ -29,7 +29,7 @@ class Persistence
// Used to invalidate old settings, if needed
// Version 0 is reserved for testing, and will always load defaults
- static constexpr uint32_t SETTINGS_VERSION = 2;
+ static constexpr uint32_t SETTINGS_VERSION = 3;
struct Settings {
struct Meta {
@@ -96,6 +96,19 @@ class Persistence
bool safeShutdownSeen = false;
} tips;
+ // Joystick settings for enabling and aligning to the screen
+ struct Joystick {
+ // Modifies the UI for joystick use
+ bool enabled = false;
+
+ // gets set to true when AlignStick applet is completed
+ bool aligned = false;
+
+ // Rotation of the joystick
+ // Multiples of 90 degrees clockwise
+ uint8_t alignment = 0;
+ } joystick;
+
// Rotation of the display
// Multiples of 90 degrees clockwise
// Most commonly: rotation is 0 when flex connector is oriented below display
diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini
index 80984f399..b985f9f77 100644
--- a/src/graphics/niche/InkHUD/PlatformioConfig.ini
+++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini
@@ -8,4 +8,5 @@ build_flags =
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
- https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
\ No newline at end of file
+ # TODO renovate
+ https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX
diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp
index c883e9a29..0548de1eb 100644
--- a/src/graphics/niche/InkHUD/WindowManager.cpp
+++ b/src/graphics/niche/InkHUD/WindowManager.cpp
@@ -2,6 +2,7 @@
#include "./WindowManager.h"
+#include "./Applets/System/AlignStick/AlignStickApplet.h"
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
#include "./Applets/System/Logo/LogoApplet.h"
#include "./Applets/System/Menu/MenuApplet.h"
@@ -98,6 +99,38 @@ void InkHUD::WindowManager::nextTile()
userTiles.at(settings->userTiles.focused)->requestHighlight();
}
+// Focus on a different tile but decrement index
+void InkHUD::WindowManager::prevTile()
+{
+ // Close the menu applet if open
+ // We don't *really* want to do this, but it simplifies handling *a lot*
+ MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
+ bool menuWasOpen = false;
+ if (menu->isForeground()) {
+ menu->sendToBackground();
+ menuWasOpen = true;
+ }
+
+ // Swap to next tile
+ if (settings->userTiles.focused == 0)
+ settings->userTiles.focused = settings->userTiles.count - 1;
+ else
+ settings->userTiles.focused--;
+
+ // Make sure that we don't get stuck on the placeholder tile
+ refocusTile();
+
+ if (menuWasOpen)
+ menu->show(userTiles.at(settings->userTiles.focused));
+
+ // Ask the tile to draw an indicator showing which tile is now focused
+ // Requests a render
+ // We only draw this indicator if the device uses an aux button to switch tiles.
+ // Assume aux button is used to switch tiles if the "next tile" menu item is hidden
+ if (!settings->optionalMenuItems.nextTile)
+ userTiles.at(settings->userTiles.focused)->requestHighlight();
+}
+
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::WindowManager::openMenu()
@@ -106,6 +139,15 @@ void InkHUD::WindowManager::openMenu()
menu->show(userTiles.at(settings->userTiles.focused));
}
+// Bring the AlignStick applet to the foreground
+void InkHUD::WindowManager::openAlignStick()
+{
+ if (settings->joystick.enabled) {
+ AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick");
+ alignStick->bringToForeground();
+ }
+}
+
// On the currently focussed tile: cycle to the next available user applet
// Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::nextApplet()
@@ -155,6 +197,59 @@ void InkHUD::WindowManager::nextApplet()
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
+// On the currently focussed tile: cycle to the previous available user applet
+// Applets available for this must be activated, and not already displayed on another tile
+void InkHUD::WindowManager::prevApplet()
+{
+ Tile *t = userTiles.at(settings->userTiles.focused);
+
+ // Abort if zero applets available
+ // nullptr means WindowManager::refocusTile determined that there were no available applets
+ if (!t->getAssignedApplet())
+ return;
+
+ // Find the index of the applet currently shown on the tile
+ uint8_t appletIndex = -1;
+ for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
+ if (inkhud->userApplets.at(i) == t->getAssignedApplet()) {
+ appletIndex = i;
+ break;
+ }
+ }
+
+ // Confirm that we did find the applet
+ assert(appletIndex != (uint8_t)-1);
+
+ // Iterate forward through the WindowManager::applets, looking for the previous valid applet
+ Applet *prevValidApplet = nullptr;
+ for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) {
+ uint8_t newAppletIndex = 0;
+ if (i > appletIndex)
+ newAppletIndex = inkhud->userApplets.size() + appletIndex - i;
+ else
+ newAppletIndex = (appletIndex - i);
+ Applet *a = inkhud->userApplets.at(newAppletIndex);
+
+ // Looking for an applet which is active (enabled by user), but currently in background
+ if (a->isActive() && !a->isForeground()) {
+ prevValidApplet = a;
+ settings->userTiles.displayedUserApplet[settings->userTiles.focused] =
+ newAppletIndex; // Remember this setting between boots!
+ break;
+ }
+ }
+
+ // Confirm that we found another applet
+ if (!prevValidApplet)
+ return;
+
+ // Hide old applet, show new applet
+ t->getAssignedApplet()->sendToBackground();
+ t->assignApplet(prevValidApplet);
+ prevValidApplet->bringToForeground();
+ inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
+}
+
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
@@ -338,6 +433,8 @@ void InkHUD::WindowManager::createSystemApplets()
addSystemApplet("Logo", new LogoApplet, new Tile);
addSystemApplet("Pairing", new PairingApplet, new Tile);
addSystemApplet("Tips", new TipsApplet, new Tile);
+ if (settings->joystick.enabled)
+ addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
addSystemApplet("Menu", new MenuApplet, nullptr);
@@ -360,6 +457,8 @@ void InkHUD::WindowManager::placeSystemTiles()
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
+ if (settings->joystick.enabled)
+ inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h
index 4d1aedf1b..5def48f8c 100644
--- a/src/graphics/niche/InkHUD/WindowManager.h
+++ b/src/graphics/niche/InkHUD/WindowManager.h
@@ -28,8 +28,11 @@ class WindowManager
// - call these to make stuff change
void nextTile();
+ void prevTile();
void openMenu();
+ void openAlignStick();
void nextApplet();
+ void prevApplet();
void rotate();
void toggleBatteryIcon();
diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp
new file mode 100644
index 000000000..287fb943f
--- /dev/null
+++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp
@@ -0,0 +1,523 @@
+#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+
+#include "./TwoButtonExtended.h"
+
+#include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin
+#include "PowerFSM.h"
+#include "sleep.h"
+
+using namespace NicheGraphics::Inputs;
+
+TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended")
+{
+ // Don't start polling buttons for release immediately
+ // Assume they are in a "released" state at boot
+ OSThread::disable();
+
+#ifdef ARCH_ESP32
+ // Register callbacks for before and after lightsleep
+ lsObserver.observe(¬ifyLightSleep);
+ lsEndObserver.observe(¬ifyLightSleepEnd);
+#endif
+
+ // Explicitly initialize these, just to keep cppcheck quiet..
+ buttons[0] = Button();
+ buttons[1] = Button();
+ joystick[Direction::UP] = SimpleButton();
+ joystick[Direction::DOWN] = SimpleButton();
+ joystick[Direction::LEFT] = SimpleButton();
+ joystick[Direction::RIGHT] = SimpleButton();
+}
+
+// Get access to (or create) the singleton instance of this class
+// Accessible inside the ISRs, even though we maybe shouldn't
+TwoButtonExtended *TwoButtonExtended::getInstance()
+{
+ // Instantiate the class the first time this method is called
+ static TwoButtonExtended *const singletonInstance = new TwoButtonExtended;
+
+ return singletonInstance;
+}
+
+// Begin receiving button input
+// We probably need to do this after sleep, as well as at boot
+void TwoButtonExtended::start()
+{
+ if (buttons[0].pin != 0xFF)
+ attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
+
+ if (buttons[1].pin != 0xFF)
+ attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
+
+ if (joystick[Direction::UP].pin != 0xFF)
+ attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp,
+ joystickActiveLogic == LOW ? FALLING : RISING);
+
+ if (joystick[Direction::DOWN].pin != 0xFF)
+ attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown,
+ joystickActiveLogic == LOW ? FALLING : RISING);
+
+ if (joystick[Direction::LEFT].pin != 0xFF)
+ attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft,
+ joystickActiveLogic == LOW ? FALLING : RISING);
+
+ if (joystick[Direction::RIGHT].pin != 0xFF)
+ attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight,
+ joystickActiveLogic == LOW ? FALLING : RISING);
+}
+
+// Stop receiving button input, and run custom sleep code
+// Called before device sleeps. This might be power-off, or just ESP32 light sleep
+// Some devices will want to attach interrupts here, for the user button to wake from sleep
+void TwoButtonExtended::stop()
+{
+ if (buttons[0].pin != 0xFF)
+ detachInterrupt(buttons[0].pin);
+
+ if (buttons[1].pin != 0xFF)
+ detachInterrupt(buttons[1].pin);
+
+ if (joystick[Direction::UP].pin != 0xFF)
+ detachInterrupt(joystick[Direction::UP].pin);
+
+ if (joystick[Direction::DOWN].pin != 0xFF)
+ detachInterrupt(joystick[Direction::DOWN].pin);
+
+ if (joystick[Direction::LEFT].pin != 0xFF)
+ detachInterrupt(joystick[Direction::LEFT].pin);
+
+ if (joystick[Direction::RIGHT].pin != 0xFF)
+ detachInterrupt(joystick[Direction::RIGHT].pin);
+}
+
+// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings
+// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere.
+// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method.
+uint8_t TwoButtonExtended::getUserButtonPin()
+{
+ uint8_t pin = 0xFF; // Unset
+
+ // Use default pin for variant, if no better source
+#ifdef BUTTON_PIN
+ pin = BUTTON_PIN;
+#endif
+
+ // From userPrefs.jsonc, if set
+#ifdef USERPREFS_BUTTON_PIN
+ pin = USERPREFS_BUTTON_PIN;
+#endif
+
+ // From user's override in device settings, if set
+ if (config.device.button_gpio)
+ pin = config.device.button_gpio;
+
+ return pin;
+}
+
+// Configures the wiring and logic of either button
+// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
+void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
+{
+ // Prevent the same GPIO being assigned to multiple buttons
+ // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
+ for (uint8_t i = 0; i < whichButton; i++) {
+ if (buttons[i].pin == pin) {
+ LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
+ return;
+ }
+ }
+
+ assert(whichButton < 2);
+ buttons[whichButton].pin = pin;
+ buttons[whichButton].activeLogic = LOW;
+
+ pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
+}
+
+// Configures the wiring and logic of the joystick buttons
+// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
+void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup)
+{
+ if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin ||
+ joystick[Direction::RIGHT].pin == rPin) {
+ LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment");
+ return;
+ }
+
+ joystick[Direction::UP].pin = uPin;
+ joystick[Direction::DOWN].pin = dPin;
+ joystick[Direction::LEFT].pin = lPin;
+ joystick[Direction::RIGHT].pin = rPin;
+ joystickActiveLogic = LOW;
+
+ pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT);
+ pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT);
+ pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT);
+ pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
+}
+
+void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
+{
+ assert(whichButton < 2);
+ buttons[whichButton].debounceLength = debounceMs;
+ buttons[whichButton].longpressLength = longpressMs;
+}
+
+void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs)
+{
+ joystickDebounceLength = debounceMs;
+}
+
+// Set what should happen when a button becomes pressed
+// Use this to implement a "while held" behavior
+void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown)
+{
+ assert(whichButton < 2);
+ buttons[whichButton].onDown = onDown;
+}
+
+// Set what should happen when a button becomes unpressed
+// Use this to implement a "While held" behavior
+void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp)
+{
+ assert(whichButton < 2);
+ buttons[whichButton].onUp = onUp;
+}
+
+// Set what should happen when a "short press" event has occurred
+void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress)
+{
+ assert(whichButton < 2);
+ buttons[whichButton].onPress = onPress;
+}
+
+// Set what should happen when a "long press" event has fired
+// Note: this will occur while the button is still held
+void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
+{
+ assert(whichButton < 2);
+ buttons[whichButton].onLongPress = onLongPress;
+}
+
+// Set what should happen when a joystick button becomes pressed
+// Use this to implement a "while held" behavior
+void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown)
+{
+ joystick[Direction::UP].onDown = uDown;
+ joystick[Direction::DOWN].onDown = dDown;
+ joystick[Direction::LEFT].onDown = lDown;
+ joystick[Direction::RIGHT].onDown = rDown;
+}
+
+// Set what should happen when a joystick button becomes unpressed
+// Use this to implement a "while held" behavior
+void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp)
+{
+ joystick[Direction::UP].onUp = uUp;
+ joystick[Direction::DOWN].onUp = dUp;
+ joystick[Direction::LEFT].onUp = lUp;
+ joystick[Direction::RIGHT].onUp = rUp;
+}
+
+// Set what should happen when a "press" event has fired
+// Note: this will occur while the joystick button is still held
+void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress)
+{
+ joystick[Direction::UP].onPress = uPress;
+ joystick[Direction::DOWN].onPress = dPress;
+ joystick[Direction::LEFT].onPress = lPress;
+ joystick[Direction::RIGHT].onPress = rPress;
+}
+
+// Handle the start of a press to the primary button
+// Wakes our button thread
+void TwoButtonExtended::isrPrimary()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->buttons[0].state == State::REST) {
+ b->buttons[0].state = State::IRQ;
+ b->buttons[0].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+// Handle the start of a press to the secondary button
+// Wakes our button thread
+void TwoButtonExtended::isrSecondary()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->buttons[1].state == State::REST) {
+ b->buttons[1].state = State::IRQ;
+ b->buttons[1].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+// Handle the start of a press to the joystick buttons
+// Also wakes our button thread
+void TwoButtonExtended::isrJoystickUp()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->joystick[Direction::UP].state == State::REST) {
+ b->joystick[Direction::UP].state = State::IRQ;
+ b->joystick[Direction::UP].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+void TwoButtonExtended::isrJoystickDown()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->joystick[Direction::DOWN].state == State::REST) {
+ b->joystick[Direction::DOWN].state = State::IRQ;
+ b->joystick[Direction::DOWN].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+void TwoButtonExtended::isrJoystickLeft()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->joystick[Direction::LEFT].state == State::REST) {
+ b->joystick[Direction::LEFT].state = State::IRQ;
+ b->joystick[Direction::LEFT].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+void TwoButtonExtended::isrJoystickRight()
+{
+ static volatile bool isrRunning = false;
+
+ if (!isrRunning) {
+ isrRunning = true;
+ TwoButtonExtended *b = TwoButtonExtended::getInstance();
+ if (b->joystick[Direction::RIGHT].state == State::REST) {
+ b->joystick[Direction::RIGHT].state = State::IRQ;
+ b->joystick[Direction::RIGHT].irqAtMillis = millis();
+ b->startThread();
+ }
+ isrRunning = false;
+ }
+}
+
+// Concise method to start our button thread
+// Follows an ISR, listening for button release
+void TwoButtonExtended::startThread()
+{
+ if (!OSThread::enabled) {
+ OSThread::setInterval(10);
+ OSThread::enabled = true;
+ }
+}
+
+// Concise method to stop our button thread
+// Called when we no longer need to poll for button release
+void TwoButtonExtended::stopThread()
+{
+ if (OSThread::enabled) {
+ OSThread::disable();
+ }
+
+ // Reset both buttons manually
+ // Just in case an IRQ fires during the process of resetting the system
+ // Can occur with super rapid presses?
+ buttons[0].state = REST;
+ buttons[1].state = REST;
+ joystick[Direction::UP].state = REST;
+ joystick[Direction::DOWN].state = REST;
+ joystick[Direction::LEFT].state = REST;
+ joystick[Direction::RIGHT].state = REST;
+}
+
+// Our button thread
+// Started by an IRQ, on either button
+// Polls for button releases
+// Stops when both buttons released
+int32_t TwoButtonExtended::runOnce()
+{
+ constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
+ constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton);
+
+ // Allow either button to request that our thread should continue polling
+ bool awaitingRelease = false;
+
+ // Check both primary and secondary buttons
+ for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
+ switch (buttons[i].state) {
+ // No action: button has not been pressed
+ case REST:
+ break;
+
+ // New press detected by interrupt
+ case IRQ:
+ powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
+ buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
+ buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
+ awaitingRelease = true; // Mark that polling-for-release should continue
+ break;
+
+ // An existing press continues
+ // Not held long enough to register as longpress
+ case POLLING_UNFIRED: {
+ uint32_t length = millis() - buttons[i].irqAtMillis;
+
+ // If button released since last thread tick,
+ if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
+ buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
+ buttons[i].state = State::REST; // Mark that the button has reset
+ if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
+ buttons[i].onPress(); // Run callback: press
+ }
+ // If button not yet released
+ else {
+ awaitingRelease = true; // Mark that polling-for-release should continue
+ if (length >= buttons[i].longpressLength) {
+ // Run callback: long press (once)
+ // Then continue waiting for release, to rearm
+ buttons[i].state = State::POLLING_FIRED;
+ buttons[i].onLongPress();
+ }
+ }
+ break;
+ }
+
+ // Button still held, but duration long enough that longpress event already fired
+ // Just waiting for release
+ case POLLING_FIRED:
+ // Release detected
+ if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
+ buttons[i].state = State::REST;
+ buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
+ }
+ // Not yet released, keep polling
+ else
+ awaitingRelease = true;
+ break;
+ }
+ }
+
+ // Check all the joystick directions
+ for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) {
+ switch (joystick[i].state) {
+ // No action: button has not been pressed
+ case REST:
+ break;
+
+ // New press detected by interrupt
+ case IRQ:
+ powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
+ joystick[i].onDown(); // Run callback: press has begun (possible hold behavior)
+ joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
+ awaitingRelease = true; // Mark that polling-for-release should continue
+ break;
+
+ // An existing press continues
+ // Not held long enough to register as press
+ case POLLING_UNFIRED: {
+ uint32_t length = millis() - joystick[i].irqAtMillis;
+
+ // If button released since last thread tick,
+ if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
+ joystick[i].onUp(); // Run callback: press has ended (possible release of a hold)
+ joystick[i].state = State::REST; // Mark that the button has reset
+ }
+ // If button not yet released
+ else {
+ awaitingRelease = true; // Mark that polling-for-release should continue
+ if (length >= joystickDebounceLength) {
+ // Run callback: long press (once)
+ // Then continue waiting for release, to rearm
+ joystick[i].state = State::POLLING_FIRED;
+ joystick[i].onPress();
+ }
+ }
+ break;
+ }
+
+ // Button still held after press
+ // Just waiting for release
+ case POLLING_FIRED:
+ // Release detected
+ if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
+ joystick[i].state = State::REST;
+ joystick[i].onUp(); // Callback: release of hold
+ }
+ // Not yet released, keep polling
+ else
+ awaitingRelease = true;
+ break;
+ }
+ }
+
+ // If all buttons are now released
+ // we don't need to waste cpu resources polling
+ // IRQ will restart this thread when we next need it
+ if (!awaitingRelease)
+ stopThread();
+
+ // Run this method again, or don't..
+ // Use whatever behavior was previously set by stopThread() or startThread()
+ return OSThread::interval;
+}
+
+#ifdef ARCH_ESP32
+
+// Detach our class' interrupts before lightsleep
+// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
+int TwoButtonExtended::beforeLightSleep(void *unused)
+{
+ stop();
+ return 0; // Indicates success
+}
+
+// Reconfigure our interrupts
+// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
+int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause)
+{
+ start();
+
+ // Manually trigger the button-down ISR
+ // - during light sleep, our ISR is disabled
+ // - if light sleep ends by button press, pretend our own ISR caught it
+ // - need to manually confirm by reading pin ourselves, to avoid occasional false positives
+ // (false positive only when using internal pullup resistors?)
+ if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
+ isrPrimary();
+
+ return 0; // Indicates success
+}
+
+#endif
+
+#endif
diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h
new file mode 100644
index 000000000..23fd78a2a
--- /dev/null
+++ b/src/graphics/niche/Inputs/TwoButtonExtended.h
@@ -0,0 +1,136 @@
+#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+
+/*
+
+Re-usable NicheGraphics input source
+
+Short and Long press for up to two buttons
+Interrupt driven
+
+*/
+
+/*
+
+This expansion adds support for four more buttons
+These buttons are single-action only, no long press
+Interrupt driven
+
+*/
+
+#pragma once
+
+#include "configuration.h"
+
+#include "assert.h"
+#include "functional"
+
+#ifdef ARCH_ESP32
+#include "esp_sleep.h" // For light-sleep handling
+#endif
+
+#include "Observer.h"
+
+namespace NicheGraphics::Inputs
+{
+
+class TwoButtonExtended : protected concurrency::OSThread
+{
+ public:
+ typedef std::function Callback;
+
+ static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
+
+ static TwoButtonExtended *getInstance(); // Create or get the singleton instance
+ void start(); // Start handling button input
+ void stop(); // Stop handling button input (disconnect ISRs for sleep)
+ void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
+ void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false);
+ void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
+ void setJoystickDebounce(uint32_t debounceMs);
+ void setHandlerDown(uint8_t whichButton, Callback onDown);
+ void setHandlerUp(uint8_t whichButton, Callback onUp);
+ void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
+ void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
+ void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
+ void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
+ void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
+
+ // Disconnect and reconnect interrupts for light sleep
+#ifdef ARCH_ESP32
+ int beforeLightSleep(void *unused);
+ int afterLightSleep(esp_sleep_wakeup_cause_t cause);
+#endif
+
+ private:
+ // Internal state of a specific button
+ enum State {
+ REST, // Up, no activity
+ IRQ, // Down detected, not yet handled
+ POLLING_UNFIRED, // Down handled, polling for release
+ POLLING_FIRED, // Longpress fired, button still held
+ };
+
+ // Joystick Directions
+ enum Direction { UP = 0, DOWN, LEFT, RIGHT };
+
+ // Data used for direction (single-action) buttons
+ class SimpleButton
+ {
+ public:
+ // Per-button config
+ uint8_t pin = 0xFF; // 0xFF: unset
+ volatile State state = State::REST; // Internal state
+ volatile uint32_t irqAtMillis; // millis() when button went down
+
+ // Per-button event callbacks
+ static void noop(){};
+ std::function onDown = noop;
+ std::function onUp = noop;
+ std::function onPress = noop;
+ };
+
+ // Data used for double-action buttons
+ class Button : public SimpleButton
+ {
+ public:
+ // Per-button extended config
+ bool activeLogic = LOW; // Active LOW by default.
+ uint32_t debounceLength = 50; // Minimum length for shortpress in ms
+ uint32_t longpressLength = 500; // Time until longpress in ms
+
+ // Per-button event callbacks
+ std::function onLongPress = noop;
+ };
+
+#ifdef ARCH_ESP32
+ // Get notified when lightsleep begins and ends
+ CallbackObserver lsObserver =
+ CallbackObserver(this, &TwoButtonExtended::beforeLightSleep);
+ CallbackObserver lsEndObserver =
+ CallbackObserver(this, &TwoButtonExtended::afterLightSleep);
+#endif
+
+ int32_t runOnce() override; // Timer method. Polls for button release
+
+ void startThread(); // Start polling for release
+ void stopThread(); // Stop polling for release
+
+ static void isrPrimary(); // User Button ISR
+ static void isrSecondary(); // optional aux button or joystick center
+ static void isrJoystickUp();
+ static void isrJoystickDown();
+ static void isrJoystickLeft();
+ static void isrJoystickRight();
+
+ TwoButtonExtended(); // Constructor made private: force use of Button::instance()
+
+ // Info about both buttons
+ Button buttons[2];
+ bool joystickActiveLogic = LOW; // Active LOW by default
+ uint32_t joystickDebounceLength = 50; // time until press in ms
+ SimpleButton joystick[4];
+};
+
+}; // namespace NicheGraphics::Inputs
+
+#endif
diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h
index 022101f7d..c55d7fa53 100644
--- a/src/input/InputBroker.h
+++ b/src/input/InputBroker.h
@@ -53,6 +53,7 @@ typedef struct _InputEvent {
class InputPollable
{
public:
+ virtual ~InputPollable() = default;
virtual void pollOnce() = 0;
};
diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp
index 7b43fa256..cc1222595 100644
--- a/src/input/RotaryEncoderImpl.cpp
+++ b/src/input/RotaryEncoderImpl.cpp
@@ -3,6 +3,9 @@
#include "RotaryEncoderImpl.h"
#include "InputBroker.h"
#include "RotaryEncoder.h"
+#ifdef ARCH_ESP32
+#include "sleep.h"
+#endif
#define ORIGIN_NAME "RotaryEncoder"
@@ -11,6 +14,20 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl()
{
rotary = nullptr;
+#ifdef ARCH_ESP32
+ isFirstInit = true;
+#endif
+}
+
+RotaryEncoderImpl::~RotaryEncoderImpl()
+{
+ LOG_DEBUG("RotaryEncoderImpl destructor");
+ detachRotaryEncoderInterrupts();
+
+ if (rotary != nullptr) {
+ delete rotary;
+ rotary = nullptr;
+ }
}
bool RotaryEncoderImpl::init()
@@ -25,15 +42,22 @@ bool RotaryEncoderImpl::init()
eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw);
eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press);
- rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
- moduleConfig.canned_message.inputbroker_pin_press);
- rotary->resetButton();
+ if (rotary == nullptr) {
+ rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
+ moduleConfig.canned_message.inputbroker_pin_press);
+ }
- interruptInstance = this;
- auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
- attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
+ attachRotaryEncoderInterrupts();
+
+#ifdef ARCH_ESP32
+ // Register callbacks for before and after lightsleep
+ // Used to detach and reattach interrupts
+ if (isFirstInit) {
+ lsObserver.observe(¬ifyLightSleep);
+ lsEndObserver.observe(¬ifyLightSleepEnd);
+ isFirstInit = false;
+ }
+#endif
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
@@ -71,6 +95,50 @@ void RotaryEncoderImpl::pollOnce()
}
}
+void RotaryEncoderImpl::detachRotaryEncoderInterrupts()
+{
+ LOG_DEBUG("RotaryEncoderImpl detach button interrupts");
+ if (interruptInstance == this) {
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_a);
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_b);
+ detachInterrupt(moduleConfig.canned_message.inputbroker_pin_press);
+ interruptInstance = nullptr;
+ } else {
+ LOG_WARN("RotaryEncoderImpl: interrupts already detached");
+ }
+}
+
+void RotaryEncoderImpl::attachRotaryEncoderInterrupts()
+{
+ LOG_DEBUG("RotaryEncoderImpl attach button interrupts");
+ if (rotary != nullptr && interruptInstance == nullptr) {
+ rotary->resetButton();
+
+ interruptInstance = this;
+ auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
+ attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
+ } else {
+ LOG_WARN("RotaryEncoderImpl: interrupts already attached");
+ }
+}
+
+#ifdef ARCH_ESP32
+
+int RotaryEncoderImpl::beforeLightSleep(void *unused)
+{
+ detachRotaryEncoderInterrupts();
+ return 0; // Indicates success;
+}
+
+int RotaryEncoderImpl::afterLightSleep(esp_sleep_wakeup_cause_t cause)
+{
+ attachRotaryEncoderInterrupts();
+ return 0; // Indicates success;
+}
+#endif
+
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
#endif
\ No newline at end of file
diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h
index 6f8e9fe5f..ec8a064bd 100644
--- a/src/input/RotaryEncoderImpl.h
+++ b/src/input/RotaryEncoderImpl.h
@@ -8,12 +8,18 @@
class RotaryEncoder;
-class RotaryEncoderImpl : public InputPollable
+class RotaryEncoderImpl final : public InputPollable
{
public:
RotaryEncoderImpl();
- bool init(void);
+ ~RotaryEncoderImpl() override;
+ bool init();
virtual void pollOnce() override;
+ // Disconnect and reconnect interrupts for light sleep
+#ifdef ARCH_ESP32
+ int beforeLightSleep(void *unused);
+ int afterLightSleep(esp_sleep_wakeup_cause_t cause);
+#endif
protected:
static RotaryEncoderImpl *interruptInstance;
@@ -23,6 +29,21 @@ class RotaryEncoderImpl : public InputPollable
input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary;
+
+ private:
+#ifdef ARCH_ESP32
+ bool isFirstInit;
+#endif
+ void detachRotaryEncoderInterrupts();
+ void attachRotaryEncoderInterrupts();
+
+#ifdef ARCH_ESP32
+ // Get notified when lightsleep begins and ends
+ CallbackObserver lsObserver =
+ CallbackObserver(this, &RotaryEncoderImpl::beforeLightSleep);
+ CallbackObserver lsEndObserver =
+ CallbackObserver(this, &RotaryEncoderImpl::afterLightSleep);
+#endif
};
extern RotaryEncoderImpl *rotaryEncoderImpl;
diff --git a/src/main.cpp b/src/main.cpp
index f8d89e1ba..e29736712 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -428,10 +428,17 @@ void setup()
#endif
#if ARCH_PORTDUINO
+ RTCQuality ourQuality = RTCQualityDevice;
+
+ std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c");
+ if (timeCommandResult[0] == '1') {
+ ourQuality = RTCQualityNTP;
+ }
+
struct timeval tv;
tv.tv_sec = time(NULL);
tv.tv_usec = 0;
- perhapsSetRTC(RTCQualityDevice, &tv);
+ perhapsSetRTC(ourQuality, &tv);
#endif
powerMonInit();
@@ -439,6 +446,13 @@ void setup()
LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n");
+#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
+#ifndef SENSECAP_INDICATOR
+ // use PSRAM for malloc calls > 256 bytes
+ heap_caps_malloc_extmem_enable(256);
+#endif
+#endif
+
#if defined(DEBUG_MUTE) && defined(DEBUG_PORT)
DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n");
DEBUG_PORT.printf("Version %s for %s from %s\r\n", optstr(APP_VERSION), optstr(APP_ENV), optstr(APP_REPO));
diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h
index b53f552fa..a3cc7791c 100644
--- a/src/mesh/Channels.h
+++ b/src/mesh/Channels.h
@@ -96,6 +96,8 @@ class Channels
bool setDefaultPresetCryptoForHash(ChannelHash channelHash);
+ int16_t getHash(ChannelIndex i) { return hashes[i]; }
+
private:
/** Given a channel index, change to use the crypto key specified by that index
*
@@ -113,8 +115,6 @@ class Channels
*/
int16_t generateHash(ChannelIndex channelNum);
- int16_t getHash(ChannelIndex i) { return hashes[i]; }
-
/**
* Validate a channel, fixing any errors as needed
*/
diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp
index 032be241b..b7459abe0 100644
--- a/src/mesh/FloodingRouter.cpp
+++ b/src/mesh/FloodingRouter.cpp
@@ -124,6 +124,10 @@ void FloodingRouter::perhapsCancelDupe(const meshtastic_MeshPacket *p)
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE && iface) {
iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
}
+ if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE && iface && nodeDB &&
+ nodeDB->isFromOrToFavoritedNode(*p)) {
+ iface->clampToLateRebroadcastWindow(getFrom(p), p->id);
+ }
}
bool FloodingRouter::isRebroadcaster()
diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h
index eda3f8881..e7178bcfe 100644
--- a/src/mesh/MeshModule.h
+++ b/src/mesh/MeshModule.h
@@ -225,4 +225,4 @@ class MeshModule
/** set the destination and packet parameters of packet p intended as a reply to a particular "to" packet
* This ensures that if the request packet was sent reliably, the reply is sent that way as well.
*/
-void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);
\ No newline at end of file
+void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to);
diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp
index 1b2af082d..297404747 100644
--- a/src/mesh/MeshService.cpp
+++ b/src/mesh/MeshService.cpp
@@ -276,6 +276,10 @@ bool MeshService::trySendPosition(NodeNum dest, bool wantReplies)
if (nodeDB->hasValidPosition(node)) {
#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS
if (positionModule) {
+ if (!config.position.fixed_position && !nodeDB->hasLocalPositionSinceBoot()) {
+ LOG_DEBUG("Skip position ping; no fresh position since boot");
+ return false;
+ }
LOG_INFO("Send position ping to 0x%x, wantReplies=%d, channel=%d", dest, wantReplies, node->channel);
positionModule->sendOurPosition(dest, wantReplies, node->channel);
return true;
diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp
index d3000c500..2d4bad854 100644
--- a/src/mesh/NodeDB.cpp
+++ b/src/mesh/NodeDB.cpp
@@ -805,11 +805,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 500;
moduleConfig.external_notification.nag_timeout = 2;
#endif
-#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312)
- // Default to RAK led pin 2 (blue)
+#if defined(RAK4630) || defined(RAK11310) || defined(RAK3312) || defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
+ // Default to PIN_LED2 for external notification output (LED color depends on device variant)
moduleConfig.external_notification.enabled = true;
moduleConfig.external_notification.output = PIN_LED2;
+#if defined(MUZI_BASE) || defined(ELECROW_ThinkNode_M3)
+ moduleConfig.external_notification.active = false;
+#else
moduleConfig.external_notification.active = true;
+#endif
moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
@@ -1039,6 +1043,7 @@ void NodeDB::clearLocalPosition()
node->position.altitude = 0;
node->position.time = 0;
setLocalPosition(meshtastic_Position_init_default);
+ localPositionUpdatedSinceBoot = false;
}
void NodeDB::cleanupMeshDB()
diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h
index 306acc0a5..6fd8deb87 100644
--- a/src/mesh/NodeDB.h
+++ b/src/mesh/NodeDB.h
@@ -279,9 +279,13 @@ class NodeDB
LOG_DEBUG("Set local position: lat=%i lon=%i time=%u timestamp=%u", position.latitude_i, position.longitude_i,
position.time, position.timestamp);
localPosition = position;
+ if (position.latitude_i != 0 || position.longitude_i != 0) {
+ localPositionUpdatedSinceBoot = true;
+ }
}
bool hasValidPosition(const meshtastic_NodeInfoLite *n);
+ bool hasLocalPositionSinceBoot() const { return localPositionUpdatedSinceBoot; }
#if !defined(MESHTASTIC_EXCLUDE_PKI)
bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest);
@@ -301,6 +305,7 @@ class NodeDB
private:
bool duplicateWarned = false;
+ bool localPositionUpdatedSinceBoot = false;
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
uint32_t lastSort = 0; // When last sorted the nodeDB
diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp
index 3c0da4494..f7daf1122 100644
--- a/src/mesh/RadioInterface.cpp
+++ b/src/mesh/RadioInterface.cpp
@@ -296,11 +296,6 @@ bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p)
return true;
}
- // If we are a CLIENT_BASE and the packet is from or to a favorited node, we should rebroadcast early
- if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
- return nodeDB->isFromOrToFavoritedNode(*p);
- }
-
return false;
}
@@ -503,6 +498,11 @@ void RadioInterface::applyModemConfig()
cr = 5;
sf = 10;
break;
+ case meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO:
+ bw = (myRegion->wideLora) ? 1625.0 : 500;
+ cr = 8;
+ sf = 11;
+ break;
default: // Config_LoRaConfig_ModemPreset_LONG_FAST is default. Gracefully use this is preset is something illegal.
bw = (myRegion->wideLora) ? 812.5 : 250;
cr = 5;
@@ -539,13 +539,26 @@ void RadioInterface::applyModemConfig()
}
if ((myRegion->freqEnd - myRegion->freqStart) < bw / 1000) {
- static const char *err_string = "Regional frequency range is smaller than bandwidth. Fall back to default preset";
- LOG_ERROR(err_string);
+ const float regionSpanKHz = (myRegion->freqEnd - myRegion->freqStart) * 1000.0f;
+ const float requestedBwKHz = bw;
+ const bool isWideRequest = requestedBwKHz >= 499.5f; // treat as 500 kHz preset
+ const char *presetName =
+ DisplayFormatters::getModemPresetDisplayName(loraConfig.modem_preset, false, loraConfig.use_preset);
+
+ char err_string[160];
+ if (isWideRequest) {
+ snprintf(err_string, sizeof(err_string), "%s region too narrow for 500kHz preset (%s). Falling back to LongFast.",
+ myRegion->name, presetName);
+ } else {
+ snprintf(err_string, sizeof(err_string), "%s region span %.0fkHz < requested %.0fkHz. Falling back to LongFast.",
+ myRegion->name, regionSpanKHz, requestedBwKHz);
+ }
+ LOG_ERROR("%s", err_string);
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
cn->level = meshtastic_LogRecord_Level_ERROR;
- sprintf(cn->message, err_string);
+ snprintf(cn->message, sizeof(cn->message), "%s", err_string);
service->sendClientNotification(cn);
// Set to default modem preset
diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp
index 00066a7a3..7619fc106 100644
--- a/src/mesh/ReliableRouter.cpp
+++ b/src/mesh/ReliableRouter.cpp
@@ -150,7 +150,9 @@ void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
PacketId nakId = (c && c->error_reason != meshtastic_Routing_Error_NONE) ? p->decoded.request_id : 0;
// We intentionally don't check wasSeenRecently, because it is harmless to delete non existent retransmission records
- if (ackId || nakId) {
+ if ((ackId || nakId) &&
+ // Implicit ACKs from MQTT should not stop retransmissions
+ !(isFromUs(p) && p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT)) {
LOG_DEBUG("Received a %s for 0x%x, stopping retransmissions", ackId ? "ACK" : "NAK", ackId);
if (ackId) {
stopRetransmission(p->to, ackId);
diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp
index 05f47d7f4..47ed0c85a 100644
--- a/src/mesh/Router.cpp
+++ b/src/mesh/Router.cpp
@@ -37,8 +37,8 @@
static MemoryDynamic dynamicPool;
Allocator &packetPool = dynamicPool;
-#elif defined(ARCH_STM32WL)
-// On STM32 there isn't enough heap left over for the rest of the firmware if we allocate this statically.
+#elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM)
+// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically.
// For now, make it dynamic again.
#define MAX_PACKETS \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
@@ -526,6 +526,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
#elif ARCH_PORTDUINO
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
+ } else if (portduino_config.JSONFilename != "") {
+ if (portduino_config.JSONFilter == (_meshtastic_PortNum)0 || portduino_config.JSONFilter == p->decoded.portnum) {
+ JSONFile << MeshPacketSerializer::JsonSerialize(p, false) << std::endl;
+ }
}
#endif
return DecodeState::DECODE_SUCCESS;
@@ -688,7 +692,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// Store a copy of encrypted packet for MQTT
DEBUG_HEAP_BEFORE;
- meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
+ p_encrypted = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
@@ -754,6 +758,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
}
packetPool.release(p_encrypted); // Release the encrypted packet
+ p_encrypted = nullptr;
}
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
diff --git a/src/mesh/Router.h b/src/mesh/Router.h
index 10a3771a7..dbe6f4f39 100644
--- a/src/mesh/Router.h
+++ b/src/mesh/Router.h
@@ -91,6 +91,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory
before us */
uint32_t rxDupe = 0, txRelayCanceled = 0;
+ // pointer to the encrypted packet
+ meshtastic_MeshPacket *p_encrypted = nullptr;
+
protected:
friend class RoutingModule;
diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp
index 4c4d0e3d1..ccece50d3 100644
--- a/src/mesh/generated/meshtastic/admin.pb.cpp
+++ b/src/mesh/generated/meshtastic/admin.pb.cpp
@@ -33,3 +33,5 @@ PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO)
+
+
diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h
index a542cf29c..b3edf7d43 100644
--- a/src/mesh/generated/meshtastic/admin.pb.h
+++ b/src/mesh/generated/meshtastic/admin.pb.h
@@ -16,6 +16,16 @@
#endif
/* Enum definitions */
+/* Firmware update mode for OTA updates */
+typedef enum _meshtastic_OTAMode {
+ /* Do not reboot into OTA mode */
+ meshtastic_OTAMode_NO_REBOOT_OTA = 0,
+ /* Reboot into OTA mode for BLE firmware update */
+ meshtastic_OTAMode_OTA_BLE = 1,
+ /* Reboot into OTA mode for WiFi firmware update */
+ meshtastic_OTAMode_OTA_WIFI = 2
+} meshtastic_OTAMode;
+
/* TODO: REPLACE */
typedef enum _meshtastic_AdminMessage_ConfigType {
/* TODO: REPLACE */
@@ -258,10 +268,13 @@ typedef struct _meshtastic_AdminMessage {
meshtastic_SharedContact add_contact;
/* Initiate or respond to a key verification request */
meshtastic_KeyVerificationAdmin key_verification;
+ /* Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now) */
+ meshtastic_OTAMode reboot_ota_mode;
/* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */
int32_t factory_reset_device;
/* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
- Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. */
+ Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
+ Deprecated in favor of reboot_ota_mode in 2.7.17 */
int32_t reboot_ota_seconds;
/* This message is only supported for the simulator Portduino build.
If received the simulator will exit successfully. */
@@ -288,6 +301,10 @@ extern "C" {
#endif
/* Helper constants for enums */
+#define _meshtastic_OTAMode_MIN meshtastic_OTAMode_NO_REBOOT_OTA
+#define _meshtastic_OTAMode_MAX meshtastic_OTAMode_OTA_WIFI
+#define _meshtastic_OTAMode_ARRAYSIZE ((meshtastic_OTAMode)(meshtastic_OTAMode_OTA_WIFI+1))
+
#define _meshtastic_AdminMessage_ConfigType_MIN meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG
#define _meshtastic_AdminMessage_ConfigType_MAX meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG
#define _meshtastic_AdminMessage_ConfigType_ARRAYSIZE ((meshtastic_AdminMessage_ConfigType)(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG+1))
@@ -309,6 +326,7 @@ extern "C" {
#define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
#define meshtastic_AdminMessage_payload_variant_restore_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
#define meshtastic_AdminMessage_payload_variant_remove_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation
+#define meshtastic_AdminMessage_payload_variant_reboot_ota_mode_ENUMTYPE meshtastic_OTAMode
@@ -396,6 +414,7 @@ extern "C" {
#define meshtastic_AdminMessage_commit_edit_settings_tag 65
#define meshtastic_AdminMessage_add_contact_tag 66
#define meshtastic_AdminMessage_key_verification_tag 67
+#define meshtastic_AdminMessage_reboot_ota_mode_tag 68
#define meshtastic_AdminMessage_factory_reset_device_tag 94
#define meshtastic_AdminMessage_reboot_ota_seconds_tag 95
#define meshtastic_AdminMessage_exit_simulator_tag 96
@@ -454,6 +473,7 @@ X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_ed
X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \
X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \
+X(a, STATIC, ONEOF, UENUM, (payload_variant,reboot_ota_mode,reboot_ota_mode), 68) \
X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \
X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \
X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index 327568316..d4ef5bee4 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -293,7 +293,8 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Long Range - Fast */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST = 0,
- /* Long Range - Slow */
+ /* Long Range - Slow
+ Deprecated in 2.7: Unpopular slow preset. */
meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW = 1,
/* Very Long Range - Slow
Deprecated in 2.5: Works only with txco and is unusably slow */
@@ -311,7 +312,10 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
/* Short Range - Turbo
This is the fastest preset and the only one with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth. */
- meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8
+ meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
+ /* Long Range - Turbo
+ This preset performs similarly to LongFast, but with 500Khz bandwidth. */
+ meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
} meshtastic_Config_LoRaConfig_ModemPreset;
typedef enum _meshtastic_Config_BluetoothConfig_PairingMode {
@@ -689,8 +693,8 @@ extern "C" {
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
-#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO
-#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO+1))
+#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
+#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
#define _meshtastic_Config_BluetoothConfig_PairingMode_MIN meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN
#define _meshtastic_Config_BluetoothConfig_PairingMode_MAX meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN
diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp
index 9966e52f8..d8eee1203 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.cpp
+++ b/src/mesh/generated/meshtastic/mesh.pb.cpp
@@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2)
PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
+PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
+
+
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
@@ -121,6 +124,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
+
+
diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h
index 0c48a7891..6f2c755be 100644
--- a/src/mesh/generated/meshtastic/mesh.pb.h
+++ b/src/mesh/generated/meshtastic/mesh.pb.h
@@ -478,6 +478,22 @@ typedef enum _meshtastic_Routing_Error {
meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED = 38
} meshtastic_Routing_Error;
+/* Enum of message types */
+typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
+ /* Send an announcement of the canonical tip of a chain */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE = 0,
+ /* Query whether a specific link is on the chain */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_CHAIN_QUERY = 1,
+ /* Request the next link in the chain */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_REQUEST = 3,
+ /* Provide a link to add to the chain */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE = 4,
+ /* If we must fragment, send the first half */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_FIRSTHALF = 5,
+ /* If we must fragment, send the second half */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
+} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
+
/* The priority of this message for sending.
Higher priorities are sent first (when managing the transmit queue).
This field is never sent over the air, it is only used internally inside of a local device node.
@@ -782,6 +798,32 @@ typedef struct _meshtastic_KeyVerification {
meshtastic_KeyVerification_hash2_t hash2;
} meshtastic_KeyVerification;
+typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_message_hash_t;
+typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_commit_hash_t;
+typedef PB_BYTES_ARRAY_T(32) meshtastic_StoreForwardPlusPlus_root_hash_t;
+typedef PB_BYTES_ARRAY_T(240) meshtastic_StoreForwardPlusPlus_message_t;
+/* The actual over-the-mesh message doing store and forward++ */
+typedef struct _meshtastic_StoreForwardPlusPlus {
+ /* Which message type is this */
+ meshtastic_StoreForwardPlusPlus_SFPP_message_type sfpp_message_type;
+ /* The hash of the specific message */
+ meshtastic_StoreForwardPlusPlus_message_hash_t message_hash;
+ /* The hash of a link on a chain */
+ meshtastic_StoreForwardPlusPlus_commit_hash_t commit_hash;
+ /* the root hash of a chain */
+ meshtastic_StoreForwardPlusPlus_root_hash_t root_hash;
+ /* The encrypted bytes from a message */
+ meshtastic_StoreForwardPlusPlus_message_t message;
+ /* Message ID of the contained message */
+ uint32_t encapsulated_id;
+ /* Destination of the contained message */
+ uint32_t encapsulated_to;
+ /* Sender of the contained message */
+ uint32_t encapsulated_from;
+ /* The receive time of the message in question */
+ uint32_t encapsulated_rxtime;
+} meshtastic_StoreForwardPlusPlus;
+
/* Waypoint message, used to share arbitrary locations across the mesh */
typedef struct _meshtastic_Waypoint {
/* Id of the waypoint */
@@ -1310,6 +1352,10 @@ extern "C" {
#define _meshtastic_Routing_Error_MAX meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED
#define _meshtastic_Routing_Error_ARRAYSIZE ((meshtastic_Routing_Error)(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED+1))
+#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN meshtastic_StoreForwardPlusPlus_SFPP_message_type_CANON_ANNOUNCE
+#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
+#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
+
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
@@ -1338,6 +1384,8 @@ extern "C" {
#define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum
+#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
+
#define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority
@@ -1380,6 +1428,7 @@ extern "C" {
#define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}}
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
+#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1411,6 +1460,7 @@ extern "C" {
#define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}}
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
+#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0}
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
#define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0, _meshtastic_MeshPacket_TransportMechanism_MIN}
@@ -1489,6 +1539,15 @@ extern "C" {
#define meshtastic_KeyVerification_nonce_tag 1
#define meshtastic_KeyVerification_hash1_tag 2
#define meshtastic_KeyVerification_hash2_tag 3
+#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_tag 1
+#define meshtastic_StoreForwardPlusPlus_message_hash_tag 2
+#define meshtastic_StoreForwardPlusPlus_commit_hash_tag 3
+#define meshtastic_StoreForwardPlusPlus_root_hash_tag 4
+#define meshtastic_StoreForwardPlusPlus_message_tag 5
+#define meshtastic_StoreForwardPlusPlus_encapsulated_id_tag 6
+#define meshtastic_StoreForwardPlusPlus_encapsulated_to_tag 7
+#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
+#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
#define meshtastic_Waypoint_id_tag 1
#define meshtastic_Waypoint_latitude_i_tag 2
#define meshtastic_Waypoint_longitude_i_tag 3
@@ -1705,6 +1764,19 @@ X(a, STATIC, SINGULAR, BYTES, hash2, 3)
#define meshtastic_KeyVerification_CALLBACK NULL
#define meshtastic_KeyVerification_DEFAULT NULL
+#define meshtastic_StoreForwardPlusPlus_FIELDLIST(X, a) \
+X(a, STATIC, SINGULAR, UENUM, sfpp_message_type, 1) \
+X(a, STATIC, SINGULAR, BYTES, message_hash, 2) \
+X(a, STATIC, SINGULAR, BYTES, commit_hash, 3) \
+X(a, STATIC, SINGULAR, BYTES, root_hash, 4) \
+X(a, STATIC, SINGULAR, BYTES, message, 5) \
+X(a, STATIC, SINGULAR, UINT32, encapsulated_id, 6) \
+X(a, STATIC, SINGULAR, UINT32, encapsulated_to, 7) \
+X(a, STATIC, SINGULAR, UINT32, encapsulated_from, 8) \
+X(a, STATIC, SINGULAR, UINT32, encapsulated_rxtime, 9)
+#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
+#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
+
#define meshtastic_Waypoint_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, id, 1) \
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
@@ -1980,6 +2052,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg;
extern const pb_msgdesc_t meshtastic_Routing_msg;
extern const pb_msgdesc_t meshtastic_Data_msg;
extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
+extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
extern const pb_msgdesc_t meshtastic_Waypoint_msg;
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
extern const pb_msgdesc_t meshtastic_MeshPacket_msg;
@@ -2013,6 +2086,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_Routing_fields &meshtastic_Routing_msg
#define meshtastic_Data_fields &meshtastic_Data_msg
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
+#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
#define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg
@@ -2069,6 +2143,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
#define meshtastic_QueueStatus_size 23
#define meshtastic_RouteDiscovery_size 256
#define meshtastic_Routing_size 259
+#define meshtastic_StoreForwardPlusPlus_size 371
#define meshtastic_ToRadio_size 504
#define meshtastic_User_size 115
#define meshtastic_Waypoint_size 165
diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h
index 67adc60cc..6b89c6a37 100644
--- a/src/mesh/generated/meshtastic/portnums.pb.h
+++ b/src/mesh/generated/meshtastic/portnums.pb.h
@@ -86,6 +86,11 @@ typedef enum _meshtastic_PortNum {
/* Paxcounter lib included in the firmware
ENCODING: protobuf */
meshtastic_PortNum_PAXCOUNTER_APP = 34,
+ /* Store and Forward++ module included in the firmware
+ ENCODING: protobuf
+ This module is specifically for Native Linux nodes, and provides a Git-style
+ chain of messages. */
+ meshtastic_PortNum_STORE_FORWARD_PLUSPLUS_APP = 35,
/* Provides a hardware serial interface to send and receive from the Meshtastic network.
Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index aa510a86d..5f0c27fff 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -417,6 +417,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
}
case meshtastic_AdminMessage_enter_dfu_mode_request_tag: {
LOG_INFO("Client requesting to enter DFU mode");
+#if HAS_SCREEN
+ IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
+#endif
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
enterDfuMode();
#endif
diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp
index f918d630f..63392f7e4 100644
--- a/src/modules/Modules.cpp
+++ b/src/modules/Modules.cpp
@@ -217,7 +217,7 @@ void setupModules()
}
#endif // HAS_BUTTON
#if ARCH_PORTDUINO
- if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+ if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && portduino_config.i2cdev != "") {
seesawRotary = new SeesawRotary("SeesawRotary");
if (!seesawRotary->init()) {
delete seesawRotary;
diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp
index aaab019d6..7db8b66cc 100644
--- a/src/modules/NodeInfoModule.cpp
+++ b/src/modules/NodeInfoModule.cpp
@@ -7,17 +7,41 @@
#include "configuration.h"
#include "main.h"
#include
+#include
+
+#ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS
+#define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60)
+#endif
NodeInfoModule *nodeInfoModule;
+static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS;
+
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
{
+ suppressReplyForCurrentRequest = false;
+
if (mp.from == nodeDB->getNodeNum()) {
LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
return false;
}
auto p = *pptr;
+
+ if (mp.decoded.want_response) {
+ const NodeNum sender = getFrom(&mp);
+ const uint32_t now = mp.rx_time ? mp.rx_time : getTime();
+ auto it = lastNodeInfoSeen.find(sender);
+ if (it != lastNodeInfoSeen.end()) {
+ uint32_t sinceLast = now >= it->second ? now - it->second : 0;
+ if (sinceLast < NodeInfoReplySuppressSeconds) {
+ suppressReplyForCurrentRequest = true;
+ }
+ }
+ lastNodeInfoSeen[sender] = now;
+ pruneLastNodeInfoCache();
+ }
+
if (p.is_licensed != owner.is_licensed) {
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
return true;
@@ -42,6 +66,8 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
service->sendToPhone(packetCopy);
}
+ pruneLastNodeInfoCache();
+
// LOG_DEBUG("did handleReceived");
return false; // Let others look at this message also if they want
}
@@ -68,9 +94,11 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
if (p) { // Check whether we didn't ignore it
p->to = dest;
- p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
+ bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
wantReplies;
+
+ p->decoded.want_response = requestWantResponse;
if (_shorterTimeout)
p->priority = meshtastic_MeshPacket_Priority_DEFAULT;
else
@@ -89,6 +117,13 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
meshtastic_MeshPacket *NodeInfoModule::allocReply()
{
+ if (suppressReplyForCurrentRequest) {
+ LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago");
+ ignoreRequest = true;
+ suppressReplyForCurrentRequest = false;
+ return NULL;
+ }
+
if (!airTime->isTxAllowedChannelUtil(false)) {
ignoreRequest = true; // Mark it as ignored for MeshModule
LOG_DEBUG("Skip send NodeInfo > 40%% ch. util");
@@ -125,6 +160,29 @@ meshtastic_MeshPacket *NodeInfoModule::allocReply()
}
}
+void NodeInfoModule::pruneLastNodeInfoCache()
+{
+ if (!nodeDB || !nodeDB->meshNodes)
+ return;
+
+ const size_t maxEntries = nodeDB->meshNodes->size();
+
+ for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) {
+ if (!nodeDB->getMeshNode(it->first)) {
+ it = lastNodeInfoSeen.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) {
+ auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(),
+ [](const std::pair &lhs,
+ const std::pair &rhs) { return lhs.second < rhs.second; });
+ lastNodeInfoSeen.erase(oldestIt);
+ }
+}
+
NodeInfoModule::NodeInfoModule()
: ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo")
{
diff --git a/src/modules/NodeInfoModule.h b/src/modules/NodeInfoModule.h
index 572b81700..d16fbeac2 100644
--- a/src/modules/NodeInfoModule.h
+++ b/src/modules/NodeInfoModule.h
@@ -1,5 +1,6 @@
#pragma once
#include "ProtobufModule.h"
+#include