Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Göttgens
48af1cd0e1 ThinkNode G3, ETH support WIP 2025-11-27 09:52:27 +01:00
202 changed files with 5554 additions and 9593 deletions

View File

@@ -51,7 +51,7 @@ for f in .clusterfuzzlite/*_fuzzer.cpp; do
fuzzer=$(basename "$f" .cpp) fuzzer=$(basename "$f" .cpp)
cp -f "$f" src/fuzzer.cpp cp -f "$f" src/fuzzer.cpp
pio run -vvv --environment "$PIO_ENV" pio run -vvv --environment "$PIO_ENV"
program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/meshtasticd" program="$PLATFORMIO_WORKSPACE_DIR/build/$PIO_ENV/program"
cp "$program" "$OUT/$fuzzer" cp "$program" "$OUT/$fuzzer"
# Copy shared libraries used by the fuzzer. # Copy shared libraries used by the fuzzer.

View File

@@ -2,5 +2,4 @@
self-hosted-runner: self-hosted-runner:
# Labels of self-hosted runner in array of strings. # Labels of self-hosted runner in array of strings.
labels: labels:
- arctastic
- test-runner - test-runner

View File

@@ -76,7 +76,7 @@ runs:
done done
- name: PlatformIO ${{ inputs.arch }} download cache - name: PlatformIO ${{ inputs.arch }} download cache
uses: actions/cache@v5 uses: actions/cache@v4
with: with:
path: ~/.platformio/.cache path: ~/.platformio/.cache
key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }} key: pio-cache-${{ inputs.arch }}-${{ hashFiles('.github/actions/**', '**.ini') }}
@@ -102,7 +102,7 @@ runs:
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}.zip
overwrite: true overwrite: true
path: | path: |
${{ inputs.artifact-paths }} ${{ inputs.artifact-paths }}

View File

@@ -18,8 +18,7 @@ permissions: read-all
jobs: jobs:
pio-build: pio-build:
name: build-${{ inputs.platform }} name: build-${{ inputs.platform }}
# Use 'arctastic' self-hosted runner pool when building in the main repo runs-on: ubuntu-24.04
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }}
outputs: outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }} artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps: steps:
@@ -56,31 +55,15 @@ jobs:
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }} ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }} ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
- name: Job summary
env:
PIO_ENV: ${{ inputs.pio_env }}
run: |
echo "## $PIO_ENV" >> $GITHUB_STEP_SUMMARY
echo "<details><summary><strong>Manifest</strong></summary>" >> $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 "</details>" >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
id: upload id: upload
with: with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
overwrite: true overwrite: true
path: | path: |
release/*.mt.json
release/*.bin release/*.bin
release/*.elf release/*.elf
release/*.uf2 release/*.uf2
release/*.hex release/*.hex
release/*.zip release/*-ota.zip
release/device-*.sh
release/device-*.bat

View File

@@ -119,7 +119,7 @@ jobs:
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*.zip ./firmware-*-ota.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -139,8 +139,8 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh || true chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh || true chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output

View File

@@ -77,21 +77,16 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
check: ${{ fromJson(needs.setup.outputs.check) }} check: ${{ fromJson(needs.setup.outputs.check) }}
# Use 'arctastic' self-hosted runner pool when checking in the main repo
runs-on: ${{ github.repository_owner == 'meshtastic' && 'arctastic' || 'ubuntu-latest' }} runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: - name: Build base
submodules: recursive id: base
ref: ${{github.event.pull_request.head.ref}} uses: ./.github/actions/setup-base
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }} - name: Check ${{ matrix.check.board }}
uses: meshtastic/gh-action-firmware@main run: bin/check-all.sh ${{ matrix.check.board }}
with:
pio_platform: ${{ matrix.check.platform }}
pio_env: ${{ matrix.check.board }}
pio_target: check
build: build:
needs: [setup, version] needs: [setup, version]
@@ -182,17 +177,19 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
path: | path: |
./firmware-*.mt.json
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*.zip ./firmware-*-ota.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -212,8 +209,8 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh || true chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh || true chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
@@ -221,7 +218,7 @@ jobs:
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./*.elf path: ./*.elf
retention-days: 30 retention-days: 30
@@ -239,7 +236,6 @@ jobs:
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
needs: needs:
- setup
- version - version
- gather-artifacts - gather-artifacts
- build-debian-src - build-debian-src
@@ -248,6 +244,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
id: create_release id: create_release
@@ -283,25 +284,10 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -lR run: ls -lR
- name: Generate Release manifest - name: Add Linux sources to GtiHub Release
run: |
jq -n --arg ver "${{ needs.version.outputs.long }}" --argjson targets ${{ toJson(needs.setup.outputs.all) }} '{
"version": $ver,
"targets": $targets
}' > firmware-${{ needs.version.outputs.long }}.json
- name: Save Release manifest artifact
uses: actions/upload-artifact@v5
with:
name: manifest-${{ needs.version.outputs.long }}
overwrite: true
path: firmware-${{ needs.version.outputs.long }}.json
- name: Add sources to GitHub Release
# Only run when targeting master branch with workflow_dispatch # Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
run: | run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{ needs.version.outputs.long }}.json
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
env: env:
@@ -343,15 +329,15 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh || true chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh || true chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
path: ./elfs path: ./elfs
@@ -387,19 +373,12 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- name: Get firmware artifacts - uses: actions/download-artifact@v6
uses: actions/download-artifact@v6
with: with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./publish path: ./publish
- name: Get manifest artifact
uses: actions/download-artifact@v6
with:
pattern: manifest-${{ needs.version.outputs.long }}
path: ./publish
- name: Publish firmware to meshtastic.github.io - name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4 uses: peaceiris/actions-gh-pages@v4
env: env:

View File

@@ -168,7 +168,7 @@ jobs:
./firmware-*.bin ./firmware-*.bin
./firmware-*.uf2 ./firmware-*.uf2
./firmware-*.hex ./firmware-*.hex
./firmware-*.zip ./firmware-*-ota.zip
./device-*.sh ./device-*.sh
./device-*.bat ./device-*.bat
./littlefs-*.bin ./littlefs-*.bin
@@ -188,8 +188,8 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh || true chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh || true chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
@@ -197,7 +197,7 @@ jobs:
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./*.elf path: ./*.elf
retention-days: 30 retention-days: 30
@@ -223,6 +223,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
id: create_release id: create_release
@@ -303,15 +308,15 @@ jobs:
- name: Device scripts permissions - name: Device scripts permissions
run: | run: |
chmod +x ./output/device-install.sh || true chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh || true chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v6 - uses: actions/download-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
path: ./elfs path: ./elfs

View File

@@ -17,7 +17,7 @@ jobs:
with: with:
script: | script: |
const labels = context.payload.pull_request.labels.map(label => label.name); const labels = context.payload.pull_request.labels.map(label => label.name);
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk', 'cleanup']; const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk'];
const hasRequiredLabel = labels.some(label => requiredLabels.includes(label)); const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
if (!hasRequiredLabel) { if (!hasRequiredLabel) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`); core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);

View File

@@ -52,7 +52,7 @@ jobs:
if: needs.native-tests.result != 'skipped' if: needs.native-tests.result != 'skipped'
uses: actions/download-artifact@v6 uses: actions/download-artifact@v6
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
- name: Parse test results and create detailed summary - name: Parse test results and create detailed summary

View File

@@ -102,7 +102,7 @@ jobs:
PIP_DISABLE_PIP_VERSION_CHECK: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1
- name: Create Bumps pull request - name: Create Bumps pull request
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v7
with: with:
base: ${{ github.event.repository.default_branch }} base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version branch: create-pull-request/bump-version

View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Stale PR+Issues - name: Stale PR+Issues
uses: actions/stale@v10.1.1 uses: actions/stale@v10.1.0
with: with:
days-before-stale: 45 days-before-stale: 45
stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days.

View File

@@ -40,7 +40,7 @@ jobs:
- name: Integration test - name: Integration test
run: | run: |
.pio/build/coverage/meshtasticd -s & .pio/build/coverage/program -s &
PID=$! PID=$!
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
echo "Simulator started, launching python test..." echo "Simulator started, launching python test..."
@@ -62,7 +62,7 @@ jobs:
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
with: with:
name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }} name: lcov-coverage-info-native-simulator-test-${{ steps.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./coverage_*.info path: ./coverage_*.info
@@ -96,7 +96,7 @@ jobs:
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./testreport.xml path: ./testreport.xml
@@ -111,7 +111,7 @@ jobs:
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
with: with:
name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }} name: lcov-coverage-info-native-platformio-tests-${{ steps.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./coverage_*.info path: ./coverage_*.info
@@ -139,11 +139,11 @@ jobs:
- name: Download test artifacts - name: Download test artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v6
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
- name: Test Report - name: Test Report
uses: dorny/test-reporter@v2.3.0 uses: dorny/test-reporter@v2.2.0
with: with:
name: PlatformIO Tests name: PlatformIO Tests
path: testreport.xml path: testreport.xml
@@ -152,7 +152,7 @@ jobs:
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v6
with: with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
path: code-coverage-report path: code-coverage-report
merge-multiple: true merge-multiple: true
@@ -165,5 +165,5 @@ jobs:
- name: Save Code Coverage Report - name: Save Code Coverage Report
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: code-coverage-report-${{ steps.version.outputs.long }} name: code-coverage-report-${{ steps.version.outputs.long }}.zip
path: code-coverage-report path: code-coverage-report

View File

@@ -22,7 +22,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
# - uses: actions/setup-python@v6 # - uses: actions/setup-python@v5
# with: # with:
# python-version: '3.10' # python-version: '3.10'

View File

@@ -31,7 +31,7 @@ jobs:
./bin/regen-protos.sh ./bin/regen-protos.sh
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v7
with: with:
branch: create-pull-request/update-protobufs branch: create-pull-request/update-protobufs
labels: submodules labels: submodules

View File

@@ -4,31 +4,31 @@ cli:
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.4 ref: v1.7.3
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.495 - checkov@3.2.492
- renovate@42.48.0 - renovate@42.5.4
- prettier@3.7.4 - prettier@3.6.2
- trufflehog@3.92.3 - trufflehog@3.90.13
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.9.2 - bandit@1.8.6
- trivy@0.68.1 - trivy@0.67.2
- taplo@0.10.0 - taplo@0.10.0
- ruff@0.14.9 - ruff@0.14.4
- isort@7.0.0 - isort@7.0.0
- markdownlint@0.47.0 - markdownlint@0.45.0
- oxipng@10.0.0 - oxipng@9.1.5
- svgo@4.0.0 - svgo@4.0.0
- actionlint@1.7.9 - actionlint@1.7.8
- flake8@7.3.0 - flake8@7.3.0
- hadolint@2.14.0 - hadolint@2.14.0
- shfmt@3.6.0 - shfmt@3.6.0
- shellcheck@0.11.0 - shellcheck@0.11.0
- black@25.12.0 - black@25.11.0
- git-diff-check - git-diff-check
- gitleaks@8.30.0 - gitleaks@8.29.0
- clang-format@16.0.3 - clang-format@16.0.3
ignore: ignore:
- linters: [ALL] - linters: [ALL]

View File

@@ -28,7 +28,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \
# ##### PRODUCTION BUILD ############# # ##### PRODUCTION BUILD #############
FROM alpine:3.23 FROM alpine:3.22
LABEL org.opencontainers.image.title="Meshtastic" \ LABEL org.opencontainers.image.title="Meshtastic" \
org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \
org.opencontainers.image.url="https://meshtastic.org" \ org.opencontainers.image.url="https://meshtastic.org" \

View File

@@ -2,15 +2,9 @@
[esp32_base] [esp32_base]
extends = arduino_base extends = arduino_base
custom_esp32_kind = esp32 custom_esp32_kind = esp32
custom_mtjson_part =
platform = platform =
# renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32
platformio/espressif32@6.12.0 platformio/espressif32@6.11.0
extra_scripts =
${env.extra_scripts}
pre:extra_scripts/esp32_pre.py
extra_scripts/esp32_extra.py
build_src_filter = build_src_filter =
${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> ${arduino_base.build_src_filter} -<platform/nrf52/> -<platform/stm32wl> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
@@ -63,7 +57,7 @@ lib_deps =
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
https://github.com/lewisxhe/XPowersLib/archive/v0.3.2.zip https://github.com/lewisxhe/XPowersLib/archive/v0.3.1.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -28,7 +28,7 @@ lib_deps =
${environmental_extra.lib_deps} ${environmental_extra.lib_deps}
${radiolib_base.lib_deps} ${radiolib_base.lib_deps}
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib # renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
lewisxhe/XPowersLib@0.3.2 lewisxhe/XPowersLib@0.3.1
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

@@ -11,30 +11,16 @@ platform_packages =
; Don't renovate toolchain-gccarmnoneeabi ; Don't renovate toolchain-gccarmnoneeabi
platformio/toolchain-gccarmnoneeabi@~1.90301.0 platformio/toolchain-gccarmnoneeabi@~1.90301.0
extra_scripts = build_type = debug
${env.extra_scripts}
extra_scripts/nrf52_extra.py
build_type = release
build_flags = build_flags =
-include variants/nrf52840/cpp_overrides/lfs_util.h -include arch/nrf52/cpp_overrides/lfs_util.h
${arduino_base.build_flags} ${arduino_base.build_flags}
-DSERIAL_BUFFER_SIZE=1024
-Wno-unused-variable -Wno-unused-variable
-Isrc/platform/nrf52 -Isrc/platform/nrf52
-DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818
-DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_AUDIO=1
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
-Os
build_unflags =
-Ofast
-Og
-ggdb3
-ggdb2
-g3
-g2
-g
-g1
-g0
build_src_filter = build_src_filter =
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> -<serialization/> ${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> -<serialization/>

View File

@@ -1,9 +1,7 @@
[nrf52832_base] [nrf52832_base]
extends = nrf52_base extends = nrf52_base
build_flags = build_flags = ${nrf52_base.build_flags}
${nrf52_base.build_flags}
-DSERIAL_BUFFER_SIZE=1024
lib_deps = lib_deps =
${nrf52_base.lib_deps} ${nrf52_base.lib_deps}

View File

@@ -1,9 +1,7 @@
[nrf52840_base] [nrf52840_base]
extends = nrf52_base extends = nrf52_base
build_flags = build_flags = ${nrf52_base.build_flags}
${nrf52_base.build_flags}
-DSERIAL_BUFFER_SIZE=4096
lib_deps = lib_deps =
${nrf52_base.lib_deps} ${nrf52_base.lib_deps}

View File

@@ -2,13 +2,13 @@
extends = arduino_base extends = arduino_base
platform = platform =
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
platformio/ststm32@19.4.0 platformio/ststm32@19.3.0
platform_packages = platform_packages =
# TODO renovate # TODO renovate
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
extra_scripts = extra_scripts =
${env.extra_scripts} ${env.extra_scripts}
extra_scripts/stm32_extra.py post:extra_scripts/extra_stm32.py
build_type = release build_type = release

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env python3
"""Summarise linker map output to highlight heavy object files and libraries.
Usage:
python bin/analyze_map.py --map .pio/build/rak4631/output.map --top 20
The script parses GNU ld map files and aggregates section sizes per object file
and per archive/library, then prints sortable tables that make it easy to spot
modules worth trimming or hiding behind feature flags.
"""
from __future__ import annotations
import argparse
import collections
import os
import re
import sys
from typing import DefaultDict, Dict, Tuple
SECTION_LINE_RE = re.compile(r"^\s+(?P<section>\S+)\s+0x[0-9A-Fa-f]+\s+0x(?P<size>[0-9A-Fa-f]+)\s+(?P<object>.+)$")
ARCHIVE_MEMBER_RE = re.compile(r"^(?P<archive>.+)\((?P<object>[^)]+)\)$")
def human_size(num_bytes: int) -> str:
"""Return a friendly size string with one decimal place."""
if num_bytes < 1024:
return f"{num_bytes:,} B"
num = float(num_bytes)
for unit in ("KB", "MB", "GB"):
num /= 1024.0
if num < 1024.0:
return f"{num:.1f} {unit}"
return f"{num:.1f} TB"
def shorten_path(path: str, root: str) -> str:
"""Prefer repository-relative paths for readability."""
path = path.strip()
if not path:
return path
# Normalise Windows archives (backslashes) to POSIX style for consistency.
path = path.replace("\\", "/")
# Attempt to strip the root when an absolute path lives inside the repo.
if os.path.isabs(path):
try:
rel = os.path.relpath(path, root)
if not rel.startswith(".."):
return rel
except ValueError:
# relpath can fail on mixed drives on Windows; fall back to basename.
pass
return path
def describe_object(raw_object: str, root: str) -> Tuple[str, str]:
"""Return a human friendly object label and the library it belongs to."""
raw_object = raw_object.strip()
lib_label = "[app]"
match = ARCHIVE_MEMBER_RE.match(raw_object)
if match:
archive = shorten_path(match.group("archive"), root)
obj = match.group("object")
lib_label = os.path.basename(archive) or archive
label = f"{archive}:{obj}"
else:
label = shorten_path(raw_object, root)
# If the object lives under libs, hint at the containing directory.
parent = os.path.basename(os.path.dirname(label))
if parent:
lib_label = parent
return label, lib_label
def parse_map(map_path: str, repo_root: str) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, int]]]:
per_object: DefaultDict[str, int] = collections.defaultdict(int)
per_library: DefaultDict[str, int] = collections.defaultdict(int)
per_object_sections: DefaultDict[str, DefaultDict[str, int]] = collections.defaultdict(lambda: collections.defaultdict(int))
try:
with open(map_path, "r", encoding="utf-8", errors="ignore") as handle:
for line in handle:
match = SECTION_LINE_RE.match(line)
if not match:
continue
section = match.group("section")
if section.startswith("*") or section in {"LOAD", "ORIGIN"}:
continue
size = int(match.group("size"), 16)
if size == 0:
continue
obj_token = match.group("object").strip()
if not obj_token or obj_token.startswith("*") or "load address" in obj_token:
continue
label, lib_label = describe_object(obj_token, repo_root)
per_object[label] += size
per_library[lib_label] += size
per_object_sections[label][section] += size
except FileNotFoundError:
raise SystemExit(f"error: map file '{map_path}' not found. Run a build first.")
return per_object, per_library, per_object_sections
def format_section_breakdown(section_sizes: Dict[str, int], total: int, limit: int = 3) -> str:
items = sorted(section_sizes.items(), key=lambda kv: kv[1], reverse=True)
parts = []
for section, size in items[:limit]:
pct = (size / total) * 100 if total else 0
parts.append(f"{section} {pct:.1f}%")
if len(items) > limit:
remainder = total - sum(size for _, size in items[:limit])
pct = (remainder / total) * 100 if total else 0
parts.append(f"other {pct:.1f}%")
return ", ".join(parts)
def print_report(map_path: str, top_n: int, per_object: Dict[str, int], per_library: Dict[str, int], per_object_sections: Dict[str, Dict[str, int]]):
total_bytes = sum(per_object.values())
if total_bytes == 0:
print("No section data found in map file.")
return
print(f"Map file: {map_path}")
print(f"Accounted size: {human_size(total_bytes)} across {len(per_object)} object files\n")
sorted_objects = sorted(per_object.items(), key=lambda kv: kv[1], reverse=True)
print(f"Top {min(top_n, len(sorted_objects))} object files by linked size:")
for idx, (obj, size) in enumerate(sorted_objects[:top_n], 1):
pct = (size / total_bytes) * 100
breakdown = format_section_breakdown(per_object_sections[obj], size)
print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size)")
print(f" {obj}")
if breakdown:
print(f" sections: {breakdown}")
print()
sorted_libs = sorted(per_library.items(), key=lambda kv: kv[1], reverse=True)
print(f"Top {min(top_n, len(sorted_libs))} libraries or source roots:")
for idx, (lib, size) in enumerate(sorted_libs[:top_n], 1):
pct = (size / total_bytes) * 100
print(f"{idx:2}. {human_size(size):>9} ({size:,} B, {pct:5.2f}% of linked size) {lib}")
def main() -> None:
parser = argparse.ArgumentParser(description="Highlight heavy object files from a GNU ld map file.")
parser.add_argument("--map", default=".pio/build/rak4631/output.map", help="Path to the map file (default: %(default)s)")
parser.add_argument("--top", type=int, default=20, help="Number of entries to display per table (default: %(default)s)")
args = parser.parse_args()
map_path = os.path.abspath(args.map)
repo_root = os.path.abspath(os.getcwd())
per_object, per_library, per_object_sections = parse_map(map_path, repo_root)
print_report(os.path.relpath(map_path, repo_root), args.top, per_object, per_library, per_object_sections)
if __name__ == "__main__":
main()

View File

@@ -5,8 +5,7 @@ set -e
VERSION=`bin/buildinfo.py long` VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short` SHORT_VERSION=`bin/buildinfo.py short`
BUILDDIR=.pio/build/$1 OUTDIR=release/
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -15,27 +14,33 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f $BUILDDIR/firmware* rm -f .pio/build/$1/firmware.*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 -t mtjson # -v pio run --environment $1 # -v
SRCELF=.pio/build/$1/firmware.elf
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf cp $SRCELF $OUTDIR/$basename.elf
echo "Copying ESP32 bin file" echo "Copying ESP32 bin file"
cp $BUILDDIR/$basename.factory.bin $OUTDIR/$basename.factory.bin SRCBIN=.pio/build/$1/firmware.factory.bin
cp $SRCBIN $OUTDIR/$basename.bin
echo "Copying ESP32 update bin file" echo "Copying ESP32 update bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin SRCBIN=.pio/build/$1/firmware.bin
cp $SRCBIN $OUTDIR/$basename-update.bin
echo "Copying Filesystem for ESP32 targets" echo "Building Filesystem for ESP32 targets"
cp $BUILDDIR/littlefs-$1-$VERSION.bin $OUTDIR/littlefs-$1-$VERSION.bin # If you want to build the webui, uncomment the following lines
cp bin/device-install.* $OUTDIR/ # pio run --environment $1 -t buildfs
cp bin/device-update.* $OUTDIR/ # cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin
# # Remove webserver files from the filesystem and rebuild
echo "Copying manifest" # ls -l data/static # Diagnostic list of files
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json # rm -rf data/static
pio run --environment $1 -t buildfs
cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin
cp bin/device-install.* $OUTDIR
cp bin/device-update.* $OUTDIR

View File

@@ -17,19 +17,15 @@ VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
PIO_ENV=${1:-native} PIO_ENV=${1:-native}
BUILDDIR=.pio/build/$PIO_ENV OUTDIR=release/
OUTDIR=release
rm -f $OUTDIR/meshtasticd* rm -f $OUTDIR/firmware*
mkdir -p $OUTDIR/ mkdir -p $OUTDIR/
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
basename=meshtasticd-$1-$VERSION
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
pio pkg install --environment "$PIO_ENV" || platformioFailed pio pkg install --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed
cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)" cp bin/native-install.* $OUTDIR
cp bin/native-install.* $OUTDIR/

View File

@@ -5,8 +5,7 @@ set -e
VERSION=$(bin/buildinfo.py long) VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
BUILDDIR=.pio/build/$1 OUTDIR=release/
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -15,37 +14,40 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f $BUILDDIR/firmware* rm -f .pio/build/$1/firmware.*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 -t mtjson # -v pio run --environment $1 # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf echo "Generating NRF52 dfu file"
DFUPKG=.pio/build/$1/firmware.zip
cp $DFUPKG $OUTDIR/$basename-ota.zip
echo "Copying NRF52 dfu (OTA) file" echo "Generating NRF52 uf2 file"
cp $BUILDDIR/$basename.zip $OUTDIR/$basename.zip SRCHEX=.pio/build/$1/firmware.hex
echo "Copying NRF52 UF2 file" # if WM1110 target, merge hex with softdevice 7.3.0
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2
cp bin/*.uf2 $OUTDIR/
SRCHEX=$BUILDDIR/$basename.hex
# if WM1110 target, copy the merged.hex
if (echo $1 | grep -q "wio-sdk-wm1110"); then if (echo $1 | grep -q "wio-sdk-wm1110"); then
echo "Copying .merged.hex file" echo "Merging with softdevice"
SRCHEX=$BUILDDIR/$basename.merged.hex bin/mergehex -m bin/s140_nrf52_7.3.0_softdevice.hex $SRCHEX -o .pio/build/$1/$basename.hex
cp $SRCHEX $OUTDIR/ SRCHEX=.pio/build/$1/$basename.hex
bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
cp $SRCHEX $OUTDIR
cp bin/*.uf2 $OUTDIR
else
bin/uf2conv.py $SRCHEX -c -o $OUTDIR/$basename.uf2 -f 0xADA52840
cp bin/device-install.* $OUTDIR
cp bin/device-update.* $OUTDIR
cp bin/*.uf2 $OUTDIR
fi fi
if (echo $1 | grep -q "rak4631"); then if (echo $1 | grep -q "rak4631"); then
echo "Copying .hex file" echo "Copying hex file"
cp $SRCHEX $OUTDIR/ cp .pio/build/$1/firmware.hex $OUTDIR/$basename.hex
fi fi
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -5,8 +5,7 @@ set -e
VERSION=`bin/buildinfo.py long` VERSION=`bin/buildinfo.py long`
SHORT_VERSION=`bin/buildinfo.py short` SHORT_VERSION=`bin/buildinfo.py short`
BUILDDIR=.pio/build/$1 OUTDIR=release/
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -15,19 +14,20 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f $BUILDDIR/firmware* rm -f .pio/build/$1/firmware.*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 -t mtjson # -v pio run --environment $1 # -v
SRCELF=.pio/build/$1/firmware.elf
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf cp $SRCELF $OUTDIR/$basename.elf
echo "Copying uf2 file" echo "Copying uf2 file"
cp $BUILDDIR/$basename.uf2 $OUTDIR/$basename.uf2 SRCBIN=.pio/build/$1/firmware.uf2
cp $SRCBIN $OUTDIR/$basename.uf2
echo "Copying manifest" cp bin/device-install.* $OUTDIR
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json cp bin/device-update.* $OUTDIR

View File

@@ -5,8 +5,7 @@ set -e
VERSION=$(bin/buildinfo.py long) VERSION=$(bin/buildinfo.py long)
SHORT_VERSION=$(bin/buildinfo.py short) SHORT_VERSION=$(bin/buildinfo.py short)
BUILDDIR=.pio/build/$1 OUTDIR=release/
OUTDIR=release
rm -f $OUTDIR/firmware* rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
@@ -15,19 +14,16 @@ rm -r $OUTDIR/* || true
platformio pkg install -e $1 platformio pkg install -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f $BUILDDIR/firmware* rm -f .pio/build/$1/firmware.*
# The shell vars the build tool expects to find # The shell vars the build tool expects to find
export APP_VERSION=$VERSION export APP_VERSION=$VERSION
basename=firmware-$1-$VERSION basename=firmware-$1-$VERSION
pio run --environment $1 -t mtjson # -v pio run --environment $1 # -v
SRCELF=.pio/build/$1/firmware.elf
cp $SRCELF $OUTDIR/$basename.elf
cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf SRCBIN=.pio/build/$1/firmware.bin
cp $SRCBIN $OUTDIR/$basename.bin
echo "Copying STM32 bin file"
cp $BUILDDIR/$basename.bin $OUTDIR/$basename.bin
echo "Copying manifest"
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json

View File

@@ -184,8 +184,6 @@ Input:
Logging: Logging:
LogLevel: info # debug, info, warn, error LogLevel: info # debug, info, warn, error
# TraceFile: /var/log/meshtasticd.json # 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 # AsciiLogs: true # default if not specified is !isatty() on stdout
Webserver: Webserver:

View File

@@ -5,14 +5,22 @@ TITLE Meshtastic device-install
SET "SCRIPT_NAME=%~nx0" SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0" SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "TFT_BUILD=0"
SET "BIGDB8=0"
SET "MUIDB8=0"
SET "BIGDB16=0"
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0" SET "LOGCOUNTER=0"
SET "BPS_RESET=0" SET "BPS_RESET=0"
@REM Default offsets.
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202 @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
SET "OTA_OFFSET=0x260000" SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv heltec-v4"
SET "SPIFFS_OFFSET=0x300000" SET "C3=esp32c3"
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator"
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv heltec-v4"
GOTO getopts GOTO getopts
:help :help
@@ -21,7 +29,7 @@ ECHO.
ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset]
ECHO. ECHO.
ECHO Options: ECHO Options:
ECHO -f filename The firmware .factory.bin file to flash. Custom to your device type and region. (required) ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required)
ECHO The file must be located in this current directory. ECHO The file must be located in this current directory.
ECHO -p PORT Set the environment variable for ESPTOOL_PORT. ECHO -p PORT Set the environment variable for ESPTOOL_PORT.
ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO If not set, ESPTOOL iterates all ports (Dangerous).
@@ -32,12 +40,12 @@ ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice. ECHO Some hardware requires this twice.
ECHO. ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.factory.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11
ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.factory.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11
GOTO eof GOTO eof
:version :version
ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO %SCRIPT_NAME% [Version 2.6.2]
ECHO Meshtastic ECHO Meshtastic
GOTO eof GOTO eof
@@ -70,8 +78,8 @@ IF "__!FILENAME!__"=="____" (
CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported." CALL :LOG_MESSAGE ERROR "Filename containing spaces are not supported."
GOTO help GOTO help
) )
IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( IF "__!FILENAME:firmware-=!__"=="__!FILENAME!__" (
CALL :LOG_MESSAGE ERROR "Filename must be a firmware-*.factory.bin file." CALL :LOG_MESSAGE ERROR "Filename must be a firmware-* file."
GOTO help GOTO help
) )
@REM Remove ".\" or "./" file prefix if present. @REM Remove ".\" or "./" file prefix if present.
@@ -85,26 +93,12 @@ IF NOT EXIST !FILENAME! (
GOTO eof GOTO eof
) )
CALL :LOG_MESSAGE DEBUG "Checking for metadata..." IF NOT "!FILENAME:update=!"=="!FILENAME!" (
@REM Derive metadata filename from firmware filename. CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
SET "METAFILE=!FILENAME:.factory.bin=!.mt.json" CALL :LOG_MESSAGE INFO "Use script device-update.bat to flash update !FILENAME!."
IF EXIST !METAFILE! (
@REM Print parsed json with powershell
CALL :LOG_MESSAGE INFO "Firmware metadata: !METAFILE!"
powershell -NoProfile -Command "(Get-Content '!METAFILE!' | ConvertFrom-Json | Out-String).Trim()"
@REM Save metadata values to variables for later use.
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).mcu"`) DO SET "MCU=%%A"
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'ota_1' } | Select-Object -ExpandProperty offset"`
) DO SET "OTA_OFFSET=%%A"
FOR /f "usebackq" %%A IN (`powershell -NoProfile -Command ^
"(Get-Content '!METAFILE!' | ConvertFrom-Json).part | Where-Object { $_.subtype -eq 'spiffs' } | Select-Object -ExpandProperty offset"`
) DO SET "SPIFFS_OFFSET=%%A"
) ELSE (
CALL :LOG_MESSAGE ERROR "No metadata file found: !METAFILE!"
GOTO eof GOTO eof
) ELSE (
CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
) )
:skip-filename :skip-filename
@@ -114,7 +108,7 @@ IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=!PYTHON! -m esptool" SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
WHERE esptool >nul 2>&1 WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 ( IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found. @REM WHERE exits with code 0 if esptool is found.
@@ -152,26 +146,100 @@ IF %BPS_RESET% EQU 1 (
GOTO eof GOTO eof
) )
@REM Extract PROGNAME from %FILENAME% for later use. @REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
SET "PROGNAME=!FILENAME:.factory.bin=!" @REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3
CALL :LOG_MESSAGE DEBUG "Computed PROGNAME: !PROGNAME!" IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" (
CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!"
IF "__!MCU!__" == "__esp32s3__" ( SET "TFT_BUILD=1"
@REM We are working with ESP32-S3
SET "OTA_FILENAME=bleota-s3.bin"
) ELSE IF "__!MCU!__" == "__esp32c3__" (
@REM We are working with ESP32-C3
SET "OTA_FILENAME=bleota-c3.bin"
) ELSE ( ) ELSE (
@REM Everything else CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!"
SET "OTA_FILENAME=bleota.bin"
) )
FOR %%a IN (%BIGDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_8MB%.
SET "BIGDB8=1"
GOTO end_loop_bigdb_8mb
)
)
:end_loop_bigdb_8mb
FOR %%a IN (%MUIDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %MUIDB_8MB%.
SET "MUIDB8=1"
GOTO end_loop_muidb_8mb
)
)
:end_loop_muidb_8mb
FOR %%a IN (%BIGDB_16MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_16MB%.
SET "BIGDB16=1"
GOTO end_loop_bigdb_16mb
)
)
:end_loop_bigdb_16mb
IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
@REM Extract BASENAME from %FILENAME% for later use.
SET "BASENAME=!FILENAME:firmware-=!"
CALL :LOG_MESSAGE DEBUG "Computed firmware basename: !BASENAME!"
@REM Account for S3 and C3 board's different OTA partition.
FOR %%a IN (%S3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %S3%.
SET "OTA_FILENAME=bleota-s3.bin"
GOTO :end_loop_s3
)
)
FOR %%a IN (%C3%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %C3%.
SET "OTA_FILENAME=bleota-c3.bin"
GOTO :end_loop_c3
)
)
@REM Everything else
SET "OTA_FILENAME=bleota.bin"
:end_loop_s3
:end_loop_c3
CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!"
@REM Set SPIFFS filename with "littlefs-" prefix. @REM Set SPIFFS filename with "littlefs-" prefix.
SET "SPIFFS_FILENAME=littlefs-!PROGNAME:firmware-=!.bin" SET "SPIFFS_FILENAME=littlefs-%BASENAME%"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!"
@REM Default offsets.
@REM https://github.com/meshtastic/web-flasher/blob/main/stores/firmwareStore.ts#L202
SET "OTA_OFFSET=0x260000"
SET "SPIFFS_OFFSET=0x300000"
@REM Offsets for BigDB 8mb.
IF %BIGDB8% EQU 1 (
SET "OTA_OFFSET=0x340000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for MUIDB 8mb.
IF %MUIDB8% EQU 1 (
SET "OTA_OFFSET=0x5D0000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for BigDB 16mb.
IF %BIGDB16% EQU 1 (
SET "OTA_OFFSET=0x650000"
SET "SPIFFS_OFFSET=0xc90000"
)
CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set OTA_OFFSET to: !OTA_OFFSET!"
CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_OFFSET to: !SPIFFS_OFFSET!"

View File

@@ -2,15 +2,69 @@
PYTHON=${PYTHON:-$(which python3 python | head -n 1)} PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false BPS_RESET=false
TFT_BUILD=false
MCU="" MCU=""
# Constants # Constants
RESET_BAUD=1200 RESET_BAUD=1200
FIRMWARE_OFFSET=0x00 FIRMWARE_OFFSET=0x00
# Default littlefs* offset.
OFFSET=0x300000 # Variant groups
# Default OTA Offset BIGDB_8MB=(
OTA_OFFSET=0x260000 "crowpanel-esp32s3"
"heltec_capsule_sensor_v3"
"heltec-v3"
"heltec-vision-master-e213"
"heltec-vision-master-e290"
"heltec-vision-master-t190"
"heltec-wireless-paper"
"heltec-wireless-tracker"
"heltec-wsl-v3"
"icarus"
"seeed-xiao-s3"
"tbeam-s3-core"
"tracksenger"
)
MUIDB_8MB=(
"picomputer-s3"
"unphone"
"seeed-sensecap-indicator"
)
BIGDB_16MB=(
"dreamcatcher"
"elecrow-adv"
"ESP32-S3-Pico"
"heltec-v4"
"m5stack-cores3"
"mesh-tab"
"station-g2"
"t-deck"
"t-energy-s3"
"t-eth-elite"
"t-watch-s3"
"tlora-pager"
)
S3_VARIANTS=(
"s3"
"-v3"
"-v4"
"t-deck"
"wireless-paper"
"wireless-tracker"
"station-g2"
"unphone"
"t-eth-elite"
"tlora-pager"
"mesh-tab"
"dreamcatcher"
"ESP32-S3-Pico"
"seeed-sensecap-indicator"
"heltec_capsule_sensor_v3"
"vision-master"
"icarus"
"tracksenger"
"elecrow-adv"
)
# Determine the correct esptool command to use # Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then if "$PYTHON" -m esptool version >/dev/null 2>&1; then
@@ -24,14 +78,6 @@ else
exit 1 exit 1
fi fi
# Check for jq
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq not found" >&2
echo "Install jq with your package manager." >&2
echo "e.g. 'apt install jq', 'dnf install jq', 'brew install jq', etc." >&2
exit 1
fi
set -e set -e
# Usage info # Usage info
@@ -43,7 +89,7 @@ Flash image file to device, but first erasing and writing system information.
-h Display this help and exit. -h Display this help and exit.
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
-f FILENAME The firmware *.factory.bin file to flash. Custom to your device type and region. -f FILENAME The firmware .bin file to flash. Custom to your device type and region.
--1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) --1200bps-reset Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF EOF
@@ -92,43 +138,69 @@ fi
shift shift
} }
if [[ $(basename "$FILENAME") != firmware-*.factory.bin ]]; then if [[ "$FILENAME" != firmware-* ]]; then
echo "Filename must be a firmware-*.factory.bin file." echo "Filename must be a firmware-* file."
exit 1 exit 1
fi fi
# Extract PROGNAME from %FILENAME% for later use. # Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
PROGNAME="${FILENAME/.factory.bin/}" if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then
# Derive metadata filename from %PROGNAME%. TFT_BUILD=true
METAFILE="${PROGNAME}.mt.json" fi
if [[ -f "$FILENAME" && "$FILENAME" == *.factory.bin ]]; then # Extract BASENAME from %FILENAME% for later use.
# Display metadata if it exists BASENAME="${FILENAME/firmware-/}"
if [[ -f "$METAFILE" ]]; then
echo "Firmware metadata: ${METAFILE}" if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
jq . "$METAFILE" # Default littlefs* offset.
# Extract relevant fields from metadata OFFSET=0x300000
if [[ $(jq -r '.part' "$METAFILE") != "null" ]]; then
OTA_OFFSET=$(jq -r '.part[] | select(.subtype == "ota_1") | .offset' "$METAFILE") # Default OTA Offset
SPIFFS_OFFSET=$(jq -r '.part[] | select(.subtype == "spiffs") | .offset' "$METAFILE") OTA_OFFSET=0x260000
# littlefs* offset for BigDB 8mb and OTA OFFSET.
for variant in "${BIGDB_8MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0x670000
OTA_OFFSET=0x340000
fi fi
MCU=$(jq -r '.mcu' "$METAFILE") done
else
echo "ERROR: No metadata file found at ${METAFILE}"
exit 1
fi
# Determine OTA filename based on MCU type for variant in "${MUIDB_8MB[@]}"; do
if [ "$MCU" == "esp32s3" ]; then if [ -z "${FILENAME##*"$variant"*}" ]; then
OTAFILE=bleota-s3.bin OFFSET=0x670000
elif [ "$MCU" == "esp32c3" ]; then OTA_OFFSET=0x5D0000
OTAFILE=bleota-c3.bin fi
done
# littlefs* offset for BigDB 16mb and OTA OFFSET.
for variant in "${BIGDB_16MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0xc90000
OTA_OFFSET=0x650000
fi
done
# Account for S3 board's different OTA partition
# FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable
for variant in "${S3_VARIANTS[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
MCU="esp32s3"
fi
done
if [ "$MCU" != "esp32s3" ]; then
if [ -n "${FILENAME##*"esp32c3"*}" ]; then
OTAFILE=bleota.bin
else
OTAFILE=bleota-c3.bin
fi
else else
OTAFILE=bleota.bin OTAFILE=bleota-s3.bin
fi fi
# Set SPIFFS filename with "littlefs-" prefix. # Set SPIFFS filename with "littlefs-" prefix.
SPIFFSFILE="littlefs-${PROGNAME/firmware-/}.bin" SPIFFSFILE=littlefs-${BASENAME}
if [[ ! -f "$FILENAME" ]]; then if [[ ! -f "$FILENAME" ]]; then
echo "Error: file ${FILENAME} wasn't found. Terminating." echo "Error: file ${FILENAME} wasn't found. Terminating."

View File

@@ -30,11 +30,11 @@ ECHO --change-mode Attempt to place the device in correct mode. (1200bps
ECHO Some hardware requires this twice. ECHO Some hardware requires this twice.
ECHO. ECHO.
ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode
ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11
GOTO eof GOTO eof
:version :version
ECHO %SCRIPT_NAME% [Version 2.7.0] ECHO %SCRIPT_NAME% [Version 2.6.2]
ECHO Meshtastic ECHO Meshtastic
GOTO eof GOTO eof
@@ -78,12 +78,12 @@ IF NOT EXIST !FILENAME! (
GOTO eof GOTO eof
) )
IF NOT "__!FILENAME:.factory.bin=!__"=="__!FILENAME!__" ( IF "!FILENAME:update=!"=="!FILENAME!" (
CALL :LOG_MESSAGE DEBUG "We are working with a *.factory.bin* file. !FILENAME!" CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!"
CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!." CALL :LOG_MESSAGE INFO "Use script device-install.bat to flash !FILENAME!."
GOTO eof GOTO eof
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "We are not working with a *.factory.bin* file. !FILENAME!" CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!"
) )
:skip-filename :skip-filename

View File

@@ -29,7 +29,7 @@ Flash image file to device, leave existing system intact."
-h Display this help and exit -h Display this help and exit
-p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous).
-P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON")
-f FILENAME The *.bin file to flash. Custom to your device type. -f FILENAME The *update.bin file to flash. Custom to your device type.
--change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset)
EOF EOF
@@ -78,7 +78,7 @@ fi
shift shift
} }
if [[ -f "$FILENAME" && "$FILENAME" != *.factory.bin ]]; then if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
echo "Trying to flash update ${FILENAME}" echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" $ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}"
else else

View File

@@ -75,7 +75,7 @@ TOOLS = {
} }
BACKTRACE_REGEX = re.compile( BACKTRACE_REGEX = re.compile(
r"\b(0x4[0-9a-fA-F]{7,8}):0x[0-9a-fA-F]{8}\b" r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b"
) )
EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$") EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$")
COUNTER_REGEX = re.compile( COUNTER_REGEX = re.compile(
@@ -89,7 +89,7 @@ POINTER_REGEX = re.compile(
STACK_BEGIN = ">>>stack>>>" STACK_BEGIN = ">>>stack>>>"
STACK_END = "<<<stack<<<" STACK_END = "<<<stack<<<"
STACK_REGEX = re.compile( STACK_REGEX = re.compile(
r"^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$" "^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$"
) )
StackLine = namedtuple("StackLine", ["offset", "content"]) StackLine = namedtuple("StackLine", ["offset", "content"])
@@ -223,7 +223,7 @@ class AddressResolver(object):
if match is None: if match is None:
if last is not None and line.startswith("(inlined by)"): if last is not None and line.startswith("(inlined by)"):
line = line[12:].strip() line = line[12:].strip()
self._address_map[last] += "\n \\-> inlined by: " + line self._address_map[last] += "\n \-> inlined by: " + line
continue continue
if match.group("result") == "?? ??:0": if match.group("result") == "?? ??:0":

View File

@@ -2,4 +2,4 @@
set -e set -e
pio run --environment native pio run --environment native
gdbserver --once localhost:2345 .pio/build/native/meshtasticd "$@" gdbserver --once localhost:2345 .pio/build/native/program "$@"

View File

@@ -2,4 +2,4 @@
set -e set -e
pio run --environment native pio run --environment native
.pio/build/native/meshtasticd "$@" .pio/build/native/program "$@"

View File

@@ -87,9 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.17" date="2025-11-28">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.17</url>
</release>
<release version="2.7.16" date="2025-11-19"> <release version="2.7.16" date="2025-11-19">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.16</url>
</release> </release>

View File

@@ -2,77 +2,98 @@
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(flake8/F821): For SConstruct imports
import sys import sys
from os.path import join, basename, isfile from os.path import join
import subprocess import subprocess
import json import json
import re import re
import time
from datetime import datetime from datetime import datetime
from readprops import readProps from readprops import readProps
Import("env") Import("env")
platform = env.PioPlatform() platform = env.PioPlatform()
progname = env.get("PROGNAME")
lfsbin = f"{progname.replace('firmware-', 'littlefs-')}.bin"
def manifest_gather(source, target, env):
out = [] def esp32_create_combined_bin(source, target, env):
check_paths = [ # this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3
progname, # https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py
f"{progname}.elf", print("Generating combined binary for serial flashing")
f"{progname}.bin",
f"{progname}.factory.bin", app_offset = 0x10000
f"{progname}.hex",
f"{progname}.merged.hex", new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
f"{progname}.uf2", sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
f"{progname}.factory.uf2", firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
f"{progname}.zip", chip = env.get("BOARD_MCU")
lfsbin flash_size = env.BoardConfig().get("upload.flash_size")
flash_freq = env.BoardConfig().get("build.f_flash", "40m")
flash_freq = flash_freq.replace("000000L", "m")
flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
] ]
for p in check_paths:
f = env.File(env.subst(f"$BUILD_DIR/{p}"))
if f.exists():
d = {
"name": p,
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}
out.append(d)
print(d)
manifest_write(out, env)
def manifest_write(files, env): print(" Offset | File")
manifest = { for section in sections:
"version": verObj["long"], sect_adr, sect_file = section.split(" ", 1)
"build_epoch": build_epoch, print(f" - {sect_adr} | {sect_file}")
"board": env.get("PIOENV"), cmd += [sect_adr, sect_file]
"mcu": env.get("BOARD_MCU"),
"repo": repo_owner,
"files": files,
"part": None,
"has_mui": False,
"has_inkhud": False,
}
# Get partition table (generated in esp32_pre.py) if it exists
if env.get("custom_mtjson_part"):
# custom_mtjson_part is a JSON string, convert it back to a dict
pj = json.loads(env.get("custom_mtjson_part"))
manifest["part"] = pj
# Enable has_mui for TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
manifest["has_mui"] = True
if "MESHTASTIC_INCLUDE_INKHUD" in env.get("CPPDEFINES", []):
manifest["has_inkhud"] = True
# Write the manifest to the build directory print(f" - {hex(app_offset)} | {firmware_name}")
with open(env.subst("$BUILD_DIR/${PROGNAME}.mt.json"), "w") as f: cmd += [hex(app_offset), firmware_name]
json.dump(manifest, f, indent=2)
print("Using esptool.py arguments: %s" % " ".join(cmd))
esptool.main(cmd)
if platform.name == "espressif32":
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
esp32_kind = env.GetProjectOption("custom_esp32_kind")
if esp32_kind == "esp32":
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
env.Append(
LINKFLAGS=[
"-Wl,--wrap=esp_flash_chip_gd",
"-Wl,--wrap=esp_flash_chip_issi",
"-Wl,--wrap=esp_flash_chip_winbond",
]
)
else:
# For newer ESP32 targets, using newlib nano works better.
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
if platform.name == "nordicnrf52":
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"",
"Generating UF2 file"))
Import("projenv") Import("projenv")
prefsLoc = projenv["PROJECT_DIR"] + "/version.properties" prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc) verObj = readProps(prefsLoc)
print(f"Using meshtastic platformio-custom.py, firmware version {verObj['long']} on {env.get('PIOENV')}") print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
# get repository owner if git is installed # get repository owner if git is installed
try: try:
@@ -118,10 +139,10 @@ flags = [
"-DBUILD_EPOCH=" + str(build_epoch), "-DBUILD_EPOCH=" + str(build_epoch),
] + pref_flags ] + pref_flags
print("Using flags:") print ("Using flags:")
for flag in flags: for flag in flags:
print(flag) print(flag)
projenv.Append( projenv.Append(
CCFLAGS=flags, CCFLAGS=flags,
) )
@@ -159,22 +180,4 @@ def load_boot_logo(source, target, env):
# Load the boot logo on TFT builds # Load the boot logo on TFT builds
if ("HAS_TFT", 1) in env.get("CPPDEFINES", []): if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
env.AddPreAction(f"$BUILD_DIR/{lfsbin}", load_boot_logo) env.AddPreAction('$BUILD_DIR/littlefs.bin', load_boot_logo)
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=mtjson_deps,
actions=[manifest_gather],
title="Meshtastic Manifest",
description="Generating Meshtastic manifest JSON + Checksums",
always_build=False,
)

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
Import("env")
platform = env.PioPlatform()
if platform.name == "native":
env.Replace(PROGNAME="meshtasticd")
else:
from readprops import readProps
prefsLoc = env["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc)
env.Replace(PROGNAME=f"firmware-{env.get('PIOENV')}-{verObj['long']}")
env.Replace(ESP32_FS_IMAGE_NAME=f"littlefs-{env.get('PIOENV')}-{verObj['long']}")
# Print the new program name for verification
print(f"PROGNAME: {env.get('PROGNAME')}")
if platform.name == "espressif32":
print(f"ESP32_FS_IMAGE_NAME: {env.get('ESP32_FS_IMAGE_NAME')}")

View File

@@ -3,7 +3,7 @@
set -e set -e
echo "Starting simulator" echo "Starting simulator"
.pio/build/native/meshtasticd -s & .pio/build/native/program &
sleep 20 # 5 seconds was not enough sleep 20 # 5 seconds was not enough
echo "Simulator started, launching python test..." echo "Simulator started, launching python test..."

View File

@@ -1,41 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "hackaday-communicator"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "hackaday-communicator (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 1500000
},
"url": "hackaday.com",
"vendor": "hackaday"
}

View File

@@ -9,7 +9,7 @@
"extra_flags": [ "extra_flags": [
"-DBOARD_HAS_PSRAM", "-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1", "-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1", "-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1" "-DARDUINO_EVENT_RUNNING_CORE=1"
], ],

6
debian/changelog vendored
View File

@@ -1,9 +1,3 @@
meshtasticd (2.7.17.0) unstable; urgency=medium
* Version 2.7.17
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Fri, 28 Nov 2025 15:11:34 +0000
meshtasticd (2.7.16.0) unstable; urgency=medium meshtasticd (2.7.16.0) unstable; urgency=medium
* Version 2.7.16 * Version 2.7.16

1
debian/rules vendored
View File

@@ -28,4 +28,5 @@ override_dh_auto_build:
# Build with platformio # Build with platformio
$(PIO_ENV) platformio run -e native-tft $(PIO_ENV) platformio run -e native-tft
# Move the binary and default config to the correct name # Move the binary and default config to the correct name
mv .pio/build/native-tft/program .pio/build/native-tft/meshtasticd
cp bin/config-dist.yaml bin/config.yaml cp bin/config-dist.yaml bin/config.yaml

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# trunk-ignore-all(flake8/F821) # trunk-ignore-all(flake8/F821)
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
Import("env") Import("env")
# NOTE: This is not currently used, but can serve as an example on how to write extra_scripts
# print("Current CLI targets", COMMAND_LINE_TARGETS) # print("Current CLI targets", COMMAND_LINE_TARGETS)
# print("Current Build targets", BUILD_TARGETS) # print("Current Build targets", BUILD_TARGETS)
# print("CPP defs", env.get("CPPDEFINES")) # print("CPP defs", env.get("CPPDEFINES"))

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
# trunk-ignore-all(ruff/E402): Hacky esptool import
# trunk-ignore-all(flake8/E402): Hacky esptool import
import sys
from os.path import join
Import("env")
platform = env.PioPlatform()
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
# IntelHex workaround, remove after fixed upstream
# https://github.com/platformio/platform-espressif32/issues/1632
try:
import intelhex
except ImportError:
env.Execute("$PYTHONEXE -m pip install intelhex")
import esptool
def esp32_create_combined_bin(source, target, env):
# this sub is borrowed from ESPEasy build toolchain. It's licensed under GPL V3
# https://github.com/letscontrolit/ESPEasy/blob/mega/tools/pio/post_esp32.py
print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
board = env.BoardConfig()
flash_size = board.get("upload.flash_size")
flash_freq = board.get("build.f_flash", "40m")
flash_freq = flash_freq.replace("000000L", "m")
flash_mode = board.get("build.flash_mode", "dio")
memory_type = board.get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
]
print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
print("Using esptool.py arguments: %s" % " ".join(cmd))
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
esp32_kind = env.GetProjectOption("custom_esp32_kind")
if esp32_kind == "esp32":
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
env.Append(
LINKFLAGS=[
"-Wl,--wrap=esp_flash_chip_gd",
"-Wl,--wrap=esp_flash_chip_issi",
"-Wl,--wrap=esp_flash_chip_winbond",
]
)
else:
# For newer ESP32 targets, using newlib nano works better.
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import json
import sys
from os.path import isfile
Import("env")
# From https://github.com/platformio/platform-espressif32/blob/develop/builder/main.py
def _parse_size(value):
if isinstance(value, int):
return value
elif value.isdigit():
return int(value)
elif value.startswith("0x"):
return int(value, 16)
elif value[-1].upper() in ("K", "M"):
base = 1024 if value[-1].upper() == "K" else 1024 * 1024
return int(value[:-1]) * base
return value
def _parse_partitions(env):
partitions_csv = env.subst("$PARTITIONS_TABLE_CSV")
if not isfile(partitions_csv):
sys.stderr.write(
"Could not find the file %s with partitions " "table.\n" % partitions_csv
)
env.Exit(1)
return
result = []
# The first offset is 0x9000 because partition table is flashed to 0x8000 and
# occupies an entire flash sector, which size is 0x1000
next_offset = 0x9000
with open(partitions_csv) as fp:
for line in fp.readlines():
line = line.strip()
if not line or line.startswith("#"):
continue
tokens = [t.strip() for t in line.split(",")]
if len(tokens) < 5:
continue
bound = 0x10000 if tokens[1] in ("0", "app") else 4
calculated_offset = (next_offset + bound - 1) & ~(bound - 1)
partition = {
"name": tokens[0],
"type": tokens[1],
"subtype": tokens[2],
"offset": tokens[3] or calculated_offset,
"size": tokens[4],
"flags": tokens[5] if len(tokens) > 5 else None,
}
result.append(partition)
next_offset = _parse_size(partition["offset"]) + _parse_size(
partition["size"]
)
return result
def mtjson_esp32_part(target, source, env):
part = _parse_partitions(env)
pj = json.dumps(part)
# print(f"JSON_PARTITIONS: {pj}")
# Dump json string to 'custom_mtjson_part' variable to use later when writing the manifest
env.Replace(custom_mtjson_part=pj)
env.AddPreAction("mtjson", mtjson_esp32_part)

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821) # trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(flake8/F821): For SConstruct imports
Import("env") Import("env")
# Custom HEX from ELF # Custom HEX from ELF
env.AddPostAction( env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.elf", "$BUILD_DIR/${PROGNAME}.elf",

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env python3
# trunk-ignore-all(ruff/F821)
# trunk-ignore-all(flake8/F821): For SConstruct imports
import sys
from os.path import basename
Import("env")
# Custom HEX from ELF
# Convert hex to uf2 for nrf52
def nrf52_hex_to_uf2(source, target, env):
hex_path = target[0].get_abspath()
# When using merged hex, drop 'merged' from uf2 filename
uf2_path = hex_path.replace(".merged.", ".")
uf2_path = uf2_path.replace(".hex", ".uf2")
env.Execute(
env.VerboseAction(
f'"{sys.executable}" ./bin/uf2conv.py "{hex_path}" -c -f 0xADA52840 -o "{uf2_path}"',
f"Generating UF2 file from {basename(hex_path)}",
)
)
def nrf52_mergehex(source, target, env):
hex_path = target[0].get_abspath()
merged_hex_path = hex_path.replace(".hex", ".merged.hex")
merge_with = None
if "wio-sdk-wm1110" == str(env.get("PIOENV")):
merge_with = env.subst("$PROJECT_DIR/bin/s140_nrf52_7.3.0_softdevice.hex")
else:
print("merge_with not defined for this target")
if merge_with is not None:
env.Execute(
env.VerboseAction(
f'"$PROJECT_DIR/bin/mergehex" -m "{hex_path}" "{merge_with}" -o "{merged_hex_path}"',
"Merging HEX with SoftDevice",
)
)
print(f'Merged file saved at "{basename(merged_hex_path)}"')
nrf52_hex_to_uf2([hex_path, merge_with], [env.File(merged_hex_path)], env)
# if WM1110 target, merge hex with softdevice 7.3.0
if "wio-sdk-wm1110" == env.get("PIOENV"):
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_mergehex)
else:
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", nrf52_hex_to_uf2)

View File

@@ -33,6 +33,7 @@ BuildRequires: python3dist(grpcio[protobuf])
BuildRequires: python3dist(grpcio-tools) BuildRequires: python3dist(grpcio-tools)
BuildRequires: git-core BuildRequires: git-core
BuildRequires: gcc-c++ BuildRequires: gcc-c++
BuildRequires: (glibc-devel >= 2.38) or pkgconfig(libbsd-overlay)
BuildRequires: pkgconfig(yaml-cpp) BuildRequires: pkgconfig(yaml-cpp)
BuildRequires: pkgconfig(libgpiod) BuildRequires: pkgconfig(libgpiod)
BuildRequires: pkgconfig(bluez) BuildRequires: pkgconfig(bluez)
@@ -76,7 +77,7 @@ platformio run -e native-tft
%install %install
# Install meshtasticd binary # Install meshtasticd binary
mkdir -p %{buildroot}%{_bindir} mkdir -p %{buildroot}%{_bindir}
install -m 0755 .pio/build/native-tft/meshtasticd %{buildroot}%{_bindir}/meshtasticd install -m 0755 .pio/build/native-tft/program %{buildroot}%{_bindir}/meshtasticd
# Install portduino VFS dir # Install portduino VFS dir
install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd install -p -d -m 0770 %{buildroot}%{_localstatedir}/lib/meshtasticd

View File

@@ -5,7 +5,7 @@
default_envs = tbeam default_envs = tbeam
extra_configs = extra_configs =
variants/*/*.ini arch/*/*.ini
variants/*/*/platformio.ini variants/*/*/platformio.ini
variants/*/diy/*/platformio.ini variants/*/diy/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini src/graphics/niche/InkHUD/PlatformioConfig.ini
@@ -14,9 +14,7 @@ description = Meshtastic
[env] [env]
test_build_src = true test_build_src = true
extra_scripts = extra_scripts = bin/platformio-custom.py
pre:bin/platformio-pre.py
bin/platformio-custom.py
; note: we add src to our include search path so that lmic_project_config can override ; note: we add src to our include search path so that lmic_project_config can override
; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile ; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile
; of code is a heap corruption bug! ; of code is a heap corruption bug!
@@ -92,7 +90,7 @@ framework = arduino
lib_deps = lib_deps =
${env.lib_deps} ${env.lib_deps}
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL # renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL
end2endzone/NonBlockingRTTTL@1.4.0 end2endzone/NonBlockingRTTTL@1.3.0
build_flags = ${env.build_flags} -Os build_flags = ${env.build_flags} -Os
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/> build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
@@ -123,7 +121,7 @@ lib_deps =
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/2746a1ce3804998460a2cb319b8ea8a238dfd8c9.zip https://github.com/meshtastic/device-ui/archive/28167c67dfd13015a0b5eef1828f95fe8e3ab7c3.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
@@ -171,7 +169,7 @@ lib_deps =
# renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master
https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip
# renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226
robtillaart/INA226@0.6.5 robtillaart/INA226@0.6.4
# renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library # renovate: datasource=custom.pio depName=SparkFun MAX3010x packageName=sparkfun/library/SparkFun MAX3010x Pulse and Proximity Sensor Library
sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2 sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2
# renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library # renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library
@@ -184,8 +182,8 @@ lib_deps =
dfrobot/DFRobot_BMM150@1.0.0 dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561 # renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2 adafruit/Adafruit TSL2561@1.1.2
# renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/library/BH1750_WE # renovate: datasource=custom.pio depName=BH1750_WE packageName=wollewald/BH1750_WE@^1.1.10
wollewald/BH1750_WE@1.1.10 wollewald/BH1750_WE@^1.1.10
; (not included in native / portduino) ; (not included in native / portduino)
[environmental_extra] [environmental_extra]
@@ -207,7 +205,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library # renovate: datasource=custom.pio depName=SparkFun Qwiic Scale NAU7802 packageName=sparkfun/library/SparkFun Qwiic Scale NAU7802 Arduino Library
sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6 sparkfun/SparkFun Qwiic Scale NAU7802 Arduino Library@1.0.6
# renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001 # renovate: datasource=custom.pio depName=ClosedCube OPT3001 packageName=closedcube/library/ClosedCube OPT3001
closedcube/ClosedCube OPT3001@1.1.2 ClosedCube OPT3001@1.1.2
# renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2 # renovate: datasource=custom.pio depName=Bosch BSEC2 packageName=boschsensortec/library/bsec2
boschsensortec/bsec2@1.10.2610 boschsensortec/bsec2@1.10.2610
# renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library
@@ -215,6 +213,6 @@ lib_deps =
# renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master
https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip
# renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core # renovate: datasource=custom.pio depName=Sensirion Core packageName=sensirion/library/Sensirion Core
sensirion/Sensirion Core@0.7.2 sensirion/Sensirion Core@0.7.1
# renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x # renovate: datasource=custom.pio depName=Sensirion I2C SCD4x packageName=sensirion/library/Sensirion I2C SCD4x
sensirion/Sensirion I2C SCD4x@1.1.0 sensirion/Sensirion I2C SCD4x@1.1.0

View File

@@ -50,11 +50,8 @@ class AudioThread : public concurrency::OSThread
delete i2sRtttl; delete i2sRtttl;
i2sRtttl = nullptr; i2sRtttl = nullptr;
} }
delete rtttlFile;
if (rtttlFile != nullptr) { rtttlFile = nullptr;
delete rtttlFile;
rtttlFile = nullptr;
}
setCPUFast(false); setCPUFast(false);
#ifdef T_LORA_PAGER #ifdef T_LORA_PAGER
@@ -102,9 +99,9 @@ class AudioThread : public concurrency::OSThread
}; };
AudioGeneratorRTTTL *i2sRtttl = nullptr; AudioGeneratorRTTTL *i2sRtttl = nullptr;
AudioOutputI2S *audioOut = nullptr; AudioOutputI2S *audioOut;
AudioFileSourcePROGMEM *rtttlFile = nullptr; AudioFileSourcePROGMEM *rtttlFile;
}; };
#endif #endif

View File

@@ -13,6 +13,11 @@ extern MemGet memGet;
#define LED_STATE_ON 1 #define LED_STATE_ON 1
#endif #endif
// WIFI LED
#ifndef WIFI_STATE_ON
#define WIFI_STATE_ON 1
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// DEBUG // DEBUG
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -147,7 +152,9 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
// Default Bluetooth PIN // Default Bluetooth PIN
#define defaultBLEPin 123456 #define defaultBLEPin 123456
#if HAS_ETHERNET && !defined(USE_WS5500) #if HAS_ETHERNET && defined(USE_CH390D)
#include <ESP32_CH390.h>
#elif HAS_ETHERNET && !defined(USE_WS5500)
#include <RAK13800_W5100S.h> #include <RAK13800_W5100S.h>
#endif // HAS_ETHERNET #endif // HAS_ETHERNET

View File

@@ -1,426 +0,0 @@
#include "configuration.h"
#if HAS_SCREEN
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "SPILock.h"
#include "SafeFile.h"
#include "gps/RTC.h"
#include "graphics/draw/MessageRenderer.h"
#include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Global message text pool and state
static char *g_messagePool = nullptr;
static size_t g_poolWritePos = 0;
// Reset pool (called on boot or clear)
static inline void resetMessagePool()
{
if (!g_messagePool) {
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
if (!g_messagePool) {
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
return;
}
}
g_poolWritePos = 0;
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
}
// Allocate text in pool and return offset
// If not enough space remains, wrap around (ring buffer style)
static inline uint16_t storeTextInPool(const char *src, size_t len)
{
if (len >= MAX_MESSAGE_SIZE)
len = MAX_MESSAGE_SIZE - 1;
// Wrap pool if out of space
if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) {
g_poolWritePos = 0;
}
uint16_t offset = g_poolWritePos;
memcpy(&g_messagePool[g_poolWritePos], src, len);
g_messagePool[g_poolWritePos + len] = '\0';
g_poolWritePos += (len + 1);
return offset;
}
// Retrieve a const pointer to message text by offset
static inline const char *getTextFromPool(uint16_t offset)
{
if (!g_messagePool || offset >= MESSAGE_TEXT_POOL_SIZE)
return "";
return &g_messagePool[offset];
}
// Helper: assign a timestamp (RTC if available, else boot-relative)
static inline void assignTimestamp(StoredMessage &sm)
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs) {
sm.timestamp = nowSecs;
sm.isBootRelative = false;
} else {
sm.timestamp = millis() / 1000;
sm.isBootRelative = true;
}
}
// Generic push with cap (used by live + persisted queues)
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, const T &msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.push_back(msg);
}
template <typename T> static inline void pushWithLimit(std::deque<T> &queue, T &&msg)
{
if (queue.size() >= MAX_MESSAGES_SAVED)
queue.pop_front();
queue.emplace_back(std::move(msg));
}
MessageStore::MessageStore(const std::string &label)
{
filename = "/Messages_" + label + ".msgs";
resetMessagePool(); // initialize text pool on boot
}
// Live message handling (RAM only)
void MessageStore::addLiveMessage(StoredMessage &&msg)
{
pushWithLimit(liveMessages, std::move(msg));
}
void MessageStore::addLiveMessage(const StoredMessage &msg)
{
pushWithLimit(liveMessages, msg);
}
// Add from incoming/outgoing packet
const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet)
{
StoredMessage sm;
assignTimestamp(sm);
sm.channelIndex = packet.channel;
const char *payload = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1);
sm.textOffset = storeTextInPool(payload, len);
sm.textLength = len;
// Determine sender
uint32_t localNode = nodeDB->getNodeNum();
sm.sender = (packet.from == 0) ? localNode : packet.from;
sm.dest = packet.to;
bool isDM = (sm.dest != 0 && sm.dest != NODENUM_BROADCAST);
if (packet.from == 0) {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::NONE;
} else {
sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST;
sm.ackStatus = AckStatus::ACKED;
}
addLiveMessage(sm);
return liveMessages.back();
}
// Outgoing/manual message
void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text)
{
StoredMessage sm;
// Always use our local time (helper handles RTC vs boot time)
assignTimestamp(sm);
sm.sender = sender;
sm.channelIndex = channelIndex;
sm.textOffset = storeTextInPool(text.c_str(), text.size());
sm.textLength = text.size();
// Use the provided destination
sm.dest = sender;
sm.type = MessageType::DM_TO_US;
// Outgoing messages always start with unknown ack status
sm.ackStatus = AckStatus::NONE;
addLiveMessage(sm);
}
#if ENABLE_MESSAGE_PERSISTENCE
// Compact, fixed-size on-flash representation using offset + length
struct __attribute__((packed)) StoredMessageRecord {
uint32_t timestamp;
uint32_t sender;
uint8_t channelIndex;
uint32_t dest;
uint8_t isBootRelative;
uint8_t ackStatus; // static_cast<uint8_t>(AckStatus)
uint8_t type; // static_cast<uint8_t>(MessageType)
uint16_t textLength; // message length
char text[MAX_MESSAGE_SIZE]; // store actual text here
};
// Serialize one StoredMessage to flash
static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m)
{
StoredMessageRecord rec = {};
rec.timestamp = m.timestamp;
rec.sender = m.sender;
rec.channelIndex = m.channelIndex;
rec.dest = m.dest;
rec.isBootRelative = m.isBootRelative;
rec.ackStatus = static_cast<uint8_t>(m.ackStatus);
rec.type = static_cast<uint8_t>(m.type);
rec.textLength = m.textLength;
// Copy the actual text into the record from RAM pool
const char *txt = getTextFromPool(m.textOffset);
strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1);
rec.text[MAX_MESSAGE_SIZE - 1] = '\0';
f.write(reinterpret_cast<const uint8_t *>(&rec), sizeof(rec));
}
// Deserialize one StoredMessage from flash; returns false on short read
static inline bool readMessageRecord(File &f, StoredMessage &m)
{
StoredMessageRecord rec = {};
if (f.readBytes(reinterpret_cast<char *>(&rec), sizeof(rec)) != sizeof(rec))
return false;
m.timestamp = rec.timestamp;
m.sender = rec.sender;
m.channelIndex = rec.channelIndex;
m.dest = rec.dest;
m.isBootRelative = rec.isBootRelative;
m.ackStatus = static_cast<AckStatus>(rec.ackStatus);
m.type = static_cast<MessageType>(rec.type);
m.textLength = rec.textLength;
// 💡 Re-store text into pool and update offset
m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1);
m.textOffset = storeTextInPool(rec.text, m.textLength);
return true;
}
void MessageStore::saveToFlash()
{
#ifdef FSCom
// Ensure root exists
spiLock->lock();
FSCom.mkdir("/");
spiLock->unlock();
SafeFile f(filename.c_str(), false);
spiLock->lock();
uint8_t count = static_cast<uint8_t>(liveMessages.size());
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
f.write(&count, 1);
for (uint8_t i = 0; i < count; ++i) {
writeMessageRecord(f, liveMessages[i]);
}
spiLock->unlock();
f.close();
#endif
}
void MessageStore::loadFromFlash()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool(); // reset pool when loading
#ifdef FSCom
concurrency::LockGuard guard(spiLock);
if (!FSCom.exists(filename.c_str()))
return;
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
if (!f)
return;
uint8_t count = 0;
f.readBytes(reinterpret_cast<char *>(&count), 1);
if (count > MAX_MESSAGES_SAVED)
count = MAX_MESSAGES_SAVED;
for (uint8_t i = 0; i < count; ++i) {
StoredMessage m;
if (!readMessageRecord(f, m))
break;
liveMessages.push_back(m);
}
f.close();
#endif
}
#else
// If persistence is disabled, these functions become no-ops
void MessageStore::saveToFlash() {}
void MessageStore::loadFromFlash() {}
#endif
// Clear all messages (RAM + persisted queue)
void MessageStore::clearAllMessages()
{
std::deque<StoredMessage>().swap(liveMessages);
resetMessagePool();
#ifdef FSCom
SafeFile f(filename.c_str(), false);
uint8_t count = 0;
f.write(&count, 1); // write "0 messages"
f.close();
#endif
}
// Internal helper: erase first or last message matching a predicate
template <typename Predicate> static void eraseIf(std::deque<StoredMessage> &deque, Predicate pred, bool fromBack = false)
{
if (fromBack) {
// Iterate from the back and erase all matches from the end
for (auto it = deque.rbegin(); it != deque.rend();) {
if (pred(*it)) {
it = std::deque<StoredMessage>::reverse_iterator(deque.erase(std::next(it).base()));
} else {
++it;
}
}
} else {
// Manual forward search to erase all matches
for (auto it = deque.begin(); it != deque.end();) {
if (pred(*it)) {
it = deque.erase(it);
} else {
++it;
}
}
}
}
// Delete oldest message (RAM + persisted queue)
void MessageStore::deleteOldestMessage()
{
eraseIf(liveMessages, [](StoredMessage &) { return true; });
saveToFlash();
}
// Delete oldest message in a specific channel
void MessageStore::deleteOldestMessageInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred);
saveToFlash();
}
void MessageStore::deleteAllMessagesInChannel(uint8_t channel)
{
auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; };
eraseIf(liveMessages, pred, false /* delete ALL, not just first */);
saveToFlash();
}
void MessageStore::deleteAllMessagesWithPeer(uint32_t peer)
{
uint32_t local = nodeDB->getNodeNum();
auto pred = [&](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == local) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred, false);
saveToFlash();
}
// Delete oldest message in a direct chat with a node
void MessageStore::deleteOldestMessageWithPeer(uint32_t peer)
{
auto pred = [peer](const StoredMessage &m) {
if (m.type != MessageType::DM_TO_US)
return false;
uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
return other == peer;
};
eraseIf(liveMessages, pred);
saveToFlash();
}
std::deque<StoredMessage> MessageStore::getChannelMessages(uint8_t channel) const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::BROADCAST && m.channelIndex == channel) {
result.push_back(m);
}
}
return result;
}
std::deque<StoredMessage> MessageStore::getDirectMessages() const
{
std::deque<StoredMessage> result;
for (const auto &m : liveMessages) {
if (m.type == MessageType::DM_TO_US) {
result.push_back(m);
}
}
return result;
}
// Upgrade boot-relative timestamps once RTC is valid
// Only same-boot boot-relative messages are healed.
// Persisted boot-relative messages from old boots stay ??? forever.
void MessageStore::upgradeBootRelativeTimestamps()
{
uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true);
if (nowSecs == 0)
return; // Still no valid RTC
uint32_t bootNow = millis() / 1000;
auto fix = [&](std::deque<StoredMessage> &dq) {
for (auto &m : dq) {
if (m.isBootRelative && m.timestamp <= bootNow) {
uint32_t bootOffset = nowSecs - bootNow;
m.timestamp += bootOffset;
m.isBootRelative = false;
}
}
};
fix(liveMessages);
}
const char *MessageStore::getText(const StoredMessage &msg)
{
// Wrapper around the internal helper
return getTextFromPool(msg.textOffset);
}
uint16_t MessageStore::storeText(const char *src, size_t len)
{
// Wrapper around the internal helper
return storeTextInPool(src, len);
}
// Global definition
MessageStore messageStore("default");
#endif

View File

@@ -1,131 +0,0 @@
#pragma once
#if HAS_SCREEN
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
#if defined(HELTEC_MESH_SOLAR)
#define LOG_DEBUG(...)
#endif
// Enable or disable message persistence (flash storage)
// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely
#ifndef ENABLE_MESSAGE_PERSISTENCE
#define ENABLE_MESSAGE_PERSISTENCE 1
#endif
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <cstdint>
#include <deque>
#include <string>
// How many messages are stored (RAM + flash).
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
#ifndef MESSAGE_HISTORY_LIMIT
#define MESSAGE_HISTORY_LIMIT 20
#endif
// Internal alias used everywhere in code do NOT redefine elsewhere.
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
// Maximum text payload size per message in bytes.
// This still defines the max message length, but we no longer reserve this space per message.
#define MAX_MESSAGE_SIZE 220
// Total shared text pool size for all messages combined.
// The text pool is RAM-only. Text is re-stored from flash into the pool on boot.
#ifndef MESSAGE_TEXT_POOL_SIZE
#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE)
#endif
// Explicit message classification
enum class MessageType : uint8_t {
BROADCAST = 0, // broadcast message
DM_TO_US = 1 // direct message addressed to this node
};
// Delivery status for messages we sent
enum class AckStatus : uint8_t {
NONE = 0, // just sent, waiting (no symbol shown)
ACKED = 1, // got a valid ACK from destination
NACKED = 2, // explicitly failed
TIMEOUT = 3, // no ACK after retry window
RELAYED = 4 // got an ACK from relay, not destination
};
struct StoredMessage {
uint32_t timestamp; // When message was created (secs since boot or RTC)
uint32_t sender; // NodeNum of sender
uint8_t channelIndex; // Channel index used
uint32_t dest; // Destination node (broadcast or direct)
MessageType type; // Derived from dest (explicit classification)
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
// Text storage metadata — rebuilt from flash at boot
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
uint16_t textLength; // Length of text in bytes
// Default constructor initializes all fields safely
StoredMessage()
: timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false),
ackStatus(AckStatus::NONE), textOffset(0), textLength(0)
{
}
};
class MessageStore
{
public:
explicit MessageStore(const std::string &label);
// Live RAM methods (always current, used by UI and runtime)
void addLiveMessage(StoredMessage &&msg);
void addLiveMessage(const StoredMessage &msg); // convenience overload
const std::deque<StoredMessage> &getLiveMessages() const { return liveMessages; }
// Add new messages from packets or manual input
const StoredMessage &addFromPacket(const meshtastic_MeshPacket &mp); // Incoming/outgoing → RAM only
void addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text); // Manual add
// Persistence methods (used only on boot/shutdown)
void saveToFlash(); // Save messages to flash
void loadFromFlash(); // Load messages from flash
// Clear all messages (RAM + persisted queue + text pool)
void clearAllMessages();
// Delete helpers
void deleteOldestMessage(); // remove oldest from RAM (and flash on save)
void deleteOldestMessageInChannel(uint8_t channel);
void deleteOldestMessageWithPeer(uint32_t peer);
void deleteAllMessagesInChannel(uint8_t channel);
void deleteAllMessagesWithPeer(uint32_t peer);
// Unified accessor (for UI code, defaults to RAM buffer)
const std::deque<StoredMessage> &getMessages() const { return liveMessages; }
// Helper filters for future use
std::deque<StoredMessage> getChannelMessages(uint8_t channel) const; // Only broadcast messages on a channel
std::deque<StoredMessage> getDirectMessages() const; // Only direct messages
// Upgrade boot-relative timestamps once RTC is valid
void upgradeBootRelativeTimestamps();
// Retrieve the C-string text for a stored message
static const char *getText(const StoredMessage &msg);
// Allocate text into pool (used by sender-side code)
static uint16_t storeText(const char *src, size_t len);
// Used when loading from flash to rebuild the text pool
static uint16_t rebuildTextFromFlash(const char *src, size_t len);
private:
std::deque<StoredMessage> liveMessages; // Single in-RAM message buffer (also used for persistence)
std::string filename; // Flash filename for persistence
};
// Global instance (defined in MessageStore.cpp)
extern MessageStore messageStore;
#endif

View File

@@ -11,7 +11,6 @@
* For more information, see: https://meshtastic.org/ * For more information, see: https://meshtastic.org/
*/ */
#include "power.h" #include "power.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "PowerFSM.h" #include "PowerFSM.h"
#include "Throttle.h" #include "Throttle.h"
@@ -708,6 +707,7 @@ bool Power::setup()
[]() { []() {
power->setIntervalFromNow(0); power->setIntervalFromNow(0);
runASAP = true; runASAP = true;
BaseType_t higherWake = 0;
}, },
CHANGE); CHANGE);
#endif #endif
@@ -717,6 +717,7 @@ bool Power::setup()
[]() { []() {
power->setIntervalFromNow(0); power->setIntervalFromNow(0);
runASAP = true; runASAP = true;
BaseType_t higherWake = 0;
}, },
CHANGE); CHANGE);
#endif #endif
@@ -776,7 +777,7 @@ void Power::shutdown()
if (screen) { if (screen) {
#ifdef T_DECK_PRO #ifdef T_DECK_PRO
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
#elif defined(USE_EINK) #elif USE_EINK
screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen
#else #else
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
@@ -787,9 +788,7 @@ void Power::shutdown()
playShutdownMelody(); playShutdownMelody();
#endif #endif
nodeDB->saveToDisk(); nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1 #ifdef PIN_LED1
ledOff(PIN_LED1); ledOff(PIN_LED1);
@@ -1456,7 +1455,7 @@ class LipoCharger : public HasBatteryLevel
/** /**
* return true if there is an external power source detected * return true if there is an external power source detected
*/ */
virtual bool isVbusIn() override { return PPM->isVbusIn(); } virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; }
/** /**
* return true if the battery is currently charging * return true if the battery is currently charging

View File

@@ -57,21 +57,21 @@ static bool isPowered()
static void sdsEnter() static void sdsEnter()
{ {
LOG_POWERFSM("State: SDS"); LOG_DEBUG("State: SDS");
// FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw // FIXME - make sure GPS and LORA radio are off first - because we want close to zero current draw
doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false); doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, false);
} }
static void lowBattSDSEnter() static void lowBattSDSEnter()
{ {
LOG_POWERFSM("State: Lower batt SDS"); LOG_DEBUG("State: Lower batt SDS");
doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true); doDeepSleep(Default::getConfiguredOrDefaultMs(config.power.sds_secs), false, true);
} }
extern Power *power; extern Power *power;
static void shutdownEnter() static void shutdownEnter()
{ {
LOG_POWERFSM("State: SHUTDOWN"); LOG_DEBUG("State: SHUTDOWN");
shutdownAtMsec = millis(); shutdownAtMsec = millis();
} }
@@ -81,7 +81,7 @@ static uint32_t secsSlept;
static void lsEnter() static void lsEnter()
{ {
LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs); LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs);
if (screen) if (screen)
screen->setOn(false); screen->setOn(false);
secsSlept = 0; // How long have we been sleeping this time secsSlept = 0; // How long have we been sleeping this time
@@ -155,12 +155,12 @@ static void lsIdle()
static void lsExit() static void lsExit()
{ {
LOG_POWERFSM("State: lsExit"); LOG_INFO("Exit state: LS");
} }
static void nbEnter() static void nbEnter()
{ {
LOG_POWERFSM("State: nbEnter"); LOG_DEBUG("State: NB");
if (screen) if (screen)
screen->setOn(false); screen->setOn(false);
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -173,7 +173,6 @@ static void nbEnter()
static void darkEnter() static void darkEnter()
{ {
LOG_POWERFSM("State: darkEnter");
setBluetoothEnable(true); setBluetoothEnable(true);
if (screen) if (screen)
screen->setOn(false); screen->setOn(false);
@@ -181,7 +180,7 @@ static void darkEnter()
static void serialEnter() static void serialEnter()
{ {
LOG_POWERFSM("State: serialEnter"); LOG_DEBUG("State: SERIAL");
setBluetoothEnable(false); setBluetoothEnable(false);
if (screen) { if (screen) {
screen->setOn(true); screen->setOn(true);
@@ -190,14 +189,13 @@ static void serialEnter()
static void serialExit() static void serialExit()
{ {
LOG_POWERFSM("State: serialExit");
// Turn bluetooth back on when we leave serial stream API // Turn bluetooth back on when we leave serial stream API
setBluetoothEnable(true); setBluetoothEnable(true);
} }
static void powerEnter() static void powerEnter()
{ {
LOG_POWERFSM("State: powerEnter"); // LOG_DEBUG("State: POWER");
if (!isPowered()) { if (!isPowered()) {
// If we got here, we are in the wrong state - we should be in powered, let that state handle things // If we got here, we are in the wrong state - we should be in powered, let that state handle things
LOG_INFO("Loss of power in Powered"); LOG_INFO("Loss of power in Powered");
@@ -212,7 +210,6 @@ static void powerEnter()
static void powerIdle() static void powerIdle()
{ {
// LOG_POWERFSM("State: powerIdle"); // very chatty
if (!isPowered()) { if (!isPowered()) {
// If we got here, we are in the wrong state // If we got here, we are in the wrong state
LOG_INFO("Loss of power in Powered"); LOG_INFO("Loss of power in Powered");
@@ -222,13 +219,14 @@ static void powerIdle()
static void powerExit() static void powerExit()
{ {
LOG_POWERFSM("State: powerExit"); if (screen)
screen->setOn(true);
setBluetoothEnable(true); setBluetoothEnable(true);
} }
static void onEnter() static void onEnter()
{ {
LOG_POWERFSM("State: onEnter"); LOG_DEBUG("State: ON");
if (screen) if (screen)
screen->setOn(true); screen->setOn(true);
setBluetoothEnable(true); setBluetoothEnable(true);
@@ -236,7 +234,6 @@ static void onEnter()
static void onIdle() static void onIdle()
{ {
LOG_POWERFSM("State: onIdle");
if (isPowered()) { if (isPowered()) {
// If we got here, we are in the wrong state - we should be in powered, let that state handle things // If we got here, we are in the wrong state - we should be in powered, let that state handle things
powerFSM.trigger(EVENT_POWER_CONNECTED); powerFSM.trigger(EVENT_POWER_CONNECTED);
@@ -245,7 +242,7 @@ static void onIdle()
static void bootEnter() static void bootEnter()
{ {
LOG_POWERFSM("State: bootEnter"); LOG_DEBUG("State: BOOT");
} }
State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN"); State stateSHUTDOWN(shutdownEnter, NULL, NULL, "SHUTDOWN");
@@ -322,6 +319,11 @@ void PowerFSM_setup()
// if any packet destined for phone arrives, turn on bluetooth at least // if any packet destined for phone arrives, turn on bluetooth at least
powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone");
// Removed 2.7: we don't show the nodes individually for every node on the screen anymore
// powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
// powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
// powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update");
// Show the received text message // Show the received text message
powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); powerFSM.add_transition(&stateNB, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text");
@@ -370,7 +372,7 @@ void PowerFSM_setup()
// Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated // Don't add power saving transitions if we are a power saving tracker or sensor or have Wifi enabled. Sleep will be initiated
// through the modules // through the modules
#if HAS_WIFI && !defined(MESHTASTIC_EXCLUDE_WIFI) #if HAS_WIFI || !defined(MESHTASTIC_EXCLUDE_WIFI)
bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER || bool isTrackerOrSensor = config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER || config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER ||
config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR; config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR;

View File

@@ -2,12 +2,6 @@
#include "configuration.h" #include "configuration.h"
#ifdef PowerFSMDebug
#define LOG_POWERFSM(...) LOG_DEBUG(__VA_ARGS__)
#else
#define LOG_POWERFSM(...)
#endif
// See sw-design.md for documentation // See sw-design.md for documentation
#define EVENT_PRESS 1 #define EVENT_PRESS 1

View File

@@ -131,7 +131,6 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
int hour = hms / SEC_PER_HOUR; int hour = hms / SEC_PER_HOUR;
int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
::printf("%s ", logLevel); ::printf("%s ", logLevel);
if (color) { if (color) {

View File

@@ -16,7 +16,6 @@ struct ToneDuration {
}; };
// Some common frequencies. // Some common frequencies.
#define NOTE_SILENT 1
#define NOTE_C3 131 #define NOTE_C3 131
#define NOTE_CS3 139 #define NOTE_CS3 139
#define NOTE_D3 147 #define NOTE_D3 147
@@ -30,16 +29,11 @@ struct ToneDuration {
#define NOTE_AS3 233 #define NOTE_AS3 233
#define NOTE_B3 247 #define NOTE_B3 247
#define NOTE_CS4 277 #define NOTE_CS4 277
#define NOTE_B4 494
#define NOTE_F5 698
#define NOTE_G6 1568
#define NOTE_E7 2637
const int DURATION_1_16 = 62; // 1/16 note
const int DURATION_1_8 = 125; // 1/8 note const int DURATION_1_8 = 125; // 1/8 note
const int DURATION_1_4 = 250; // 1/4 note const int DURATION_1_4 = 250; // 1/4 note
const int DURATION_1_2 = 500; // 1/2 note const int DURATION_1_2 = 500; // 1/2 note
const int DURATION_3_4 = 750; // 3/4 note const int DURATION_3_4 = 750; // 1/4 note
const int DURATION_1_1 = 1000; // 1/1 note const int DURATION_1_1 = 1000; // 1/1 note
void playTones(const ToneDuration *tone_durations, int size) void playTones(const ToneDuration *tone_durations, int size)
@@ -77,24 +71,13 @@ void playLongBeep()
void playGPSEnableBeep() void playGPSEnableBeep()
{ {
#if defined(R1_NEO) || defined(MUZI_BASE)
ToneDuration melody[] = {
{NOTE_F5, DURATION_1_2}, {NOTE_G6, DURATION_1_8}, {NOTE_E7, DURATION_1_4}, {NOTE_SILENT, DURATION_1_2}};
#else
ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}}; ToneDuration melody[] = {{NOTE_C3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_CS4, DURATION_1_4}};
#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); playTones(melody, sizeof(melody) / sizeof(ToneDuration));
} }
void playGPSDisableBeep() void playGPSDisableBeep()
{ {
#if defined(R1_NEO) || defined(MUZI_BASE)
ToneDuration melody[] = {{NOTE_B4, DURATION_1_16}, {NOTE_B4, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
{NOTE_F3, DURATION_1_16}, {NOTE_F3, DURATION_1_16}, {NOTE_SILENT, DURATION_1_8},
{NOTE_C3, DURATION_1_1}, {NOTE_SILENT, DURATION_1_1}};
#else
ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}}; ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}, {NOTE_C3, DURATION_1_4}};
#endif
playTones(melody, sizeof(melody) / sizeof(ToneDuration)); playTones(melody, sizeof(melody) / sizeof(ToneDuration));
} }

View File

@@ -36,29 +36,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Offer chance for variant-specific defines */ /* Offer chance for variant-specific defines */
#include "variant.h" #include "variant.h"
// -----------------------------------------------------------------------------
// Display feature overrides
// -----------------------------------------------------------------------------
// Allow build environments to opt-in explicitly to the E-Ink UI stack while
// keeping headless targets slim by default. Existing variants that already
// define USE_EINK continue to work without additional flags.
#ifndef MESHTASTIC_USE_EINK_UI
#ifdef USE_EINK
#define MESHTASTIC_USE_EINK_UI 1
#else
#define MESHTASTIC_USE_EINK_UI 0
#endif
#endif
#if MESHTASTIC_USE_EINK_UI
#ifndef USE_EINK
#define USE_EINK
#endif
#else
#undef USE_EINK
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Version // Version
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -273,9 +250,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Touchscreen // Touchscreen
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#define FT6336U_ADDR 0x48 #define FT6336U_ADDR 0x48
#define CST328_ADDR 0x1A // same address as CST226SE #define CST328_ADDR 0x1A
#define CHSC6X_ADDR 0x2E #define CHSC6X_ADDR 0x2E
#define CST226SE_ADDR_ALT 0x5A
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) // RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
@@ -394,9 +370,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifndef HAS_BLUETOOTH #ifndef HAS_BLUETOOTH
#define HAS_BLUETOOTH 0 #define HAS_BLUETOOTH 0
#endif #endif
#ifndef USE_TFTDISPLAY
#define USE_TFTDISPLAY 0
#endif
#ifndef HW_VENDOR #ifndef HW_VENDOR
#error HW_VENDOR must be defined #error HW_VENDOR must be defined

View File

@@ -85,8 +85,7 @@ class ScanI2C
DRV2605, DRV2605,
BH1750, BH1750,
DA217, DA217,
CHSC6X, CHSC6X
CST226SE
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;

View File

@@ -499,18 +499,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
case CST328_ADDR: SCAN_SIMPLE_CASE(CST328_ADDR, CST328, "CST328", (uint8_t)addr.address);
// Do we have the CST328 or the CST226SE
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
if (registerValue == 0xA9) {
type = CST226SE;
logFoundDevice("CST226SE", (uint8_t)addr.address);
} else {
type = CST328;
logFoundDevice("CST328", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address); SCAN_SIMPLE_CASE(CHSC6X_ADDR, CHSC6X, "CHSC6X", (uint8_t)addr.address);
case LTR553ALS_ADDR: case LTR553ALS_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x86), 1); // Part ID register
@@ -539,12 +528,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#endif #endif
case MLX90614_ADDR_DEF: case MLX90614_ADDR_DEF:
// Do we have the MLX90614 or the MPR121KB or the CST226SE registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1);
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x06), 1); if (registerValue == 0x5a) {
if (registerValue == 0xAB) {
type = CST226SE;
logFoundDevice("CST226SE", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0e), 1) == 0x5a) {
type = MLX90614; type = MLX90614;
logFoundDevice("MLX90614", (uint8_t)addr.address); logFoundDevice("MLX90614", (uint8_t)addr.address);
} else { } else {

View File

@@ -46,7 +46,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
#include "FSCommon.h" #include "FSCommon.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h" #include "RadioLibInterface.h"
#include "error.h" #include "error.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
@@ -65,13 +64,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "modules/WaypointModule.h" #include "modules/WaypointModule.h"
#include "sleep.h" #include "sleep.h"
#include "target_specific.h" #include "target_specific.h"
extern MessageStore messageStore;
#if USE_TFTDISPLAY using graphics::Emote;
using graphics::emotes;
using graphics::numEmotes;
extern uint16_t TFT_MESH; extern uint16_t TFT_MESH;
#else
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
#endif
#if HAS_WIFI && !defined(ARCH_PORTDUINO) #if HAS_WIFI && !defined(ARCH_PORTDUINO)
#include "mesh/wifi/WiFiAPClient.h" #include "mesh/wifi/WiFiAPClient.h"
@@ -117,6 +115,10 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
// we'll need to hold onto pointers for the modules that can draw a frame. // we'll need to hold onto pointers for the modules that can draw a frame.
std::vector<MeshModule *> moduleFrames; std::vector<MeshModule *> moduleFrames;
// Global variables for screen function overlay symbols
std::vector<std::string> functionSymbol;
std::string functionSymbolString;
#if HAS_GPS #if HAS_GPS
// GeoCoord object for the screen // GeoCoord object for the screen
GeoCoord geoCoord; GeoCoord geoCoord;
@@ -224,9 +226,24 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
{ {
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
// Start OnScreenKeyboardModule session (non-touch variant) if (NotificationRenderer::virtualKeyboard) {
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); delete NotificationRenderer::virtualKeyboard;
NotificationRenderer::virtualKeyboard = nullptr;
}
NotificationRenderer::textInputCallback = nullptr;
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
if (header) {
NotificationRenderer::virtualKeyboard->setHeader(header);
}
if (initialText) {
NotificationRenderer::virtualKeyboard->setInputText(initialText);
}
// Set up callback with safer cleanup mechanism
NotificationRenderer::textInputCallback = textCallback; NotificationRenderer::textInputCallback = textCallback;
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications) // Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
@@ -257,11 +274,19 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
} else { } else {
// otherwise, just display the module frame that's aligned with the current frame // otherwise, just display the module frame that's aligned with the current frame
module_frame = state->currentFrame; module_frame = state->currentFrame;
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
} }
// LOG_DEBUG("Draw Module Frame %d", module_frame);
MeshModule &pi = *moduleFrames.at(module_frame); MeshModule &pi = *moduleFrames.at(module_frame);
pi.drawFrame(display, state, x, y); pi.drawFrame(display, state, x, y);
} }
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
{
return packet->from != 0 && !moduleConfig.store_forward.enabled;
}
/** /**
* Given a recent lat/lon return a guess of the heading the user is walking on. * Given a recent lat/lon return a guess of the heading the user is walking on.
* *
@@ -299,7 +324,7 @@ static int8_t prevFrame = -1;
// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes
// Uses a single frame and changes data every few seconds (E-Ink variant is separate) // Uses a single frame and changes data every few seconds (E-Ink variant is separate)
#if defined(ESP_PLATFORM) && (defined(USE_ST7789) || defined(USE_ST7796)) #if defined(ESP_PLATFORM) && defined(USE_ST7789)
SPIClass SPI1(HSPI); SPIClass SPI1(HSPI);
#endif #endif
@@ -308,16 +333,16 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
{ {
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
int32_t rawRGB = uiconfig.screen_rgb_color; int32_t rawRGB = uiconfig.screen_rgb_color;
// Only validate the combined value once
if (rawRGB > 0 && rawRGB <= 255255255) { if (rawRGB > 0 && rawRGB <= 255255255) {
// Extract each component as a normal int first uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF;
int r = (rawRGB >> 16) & 0xFF; uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF;
int g = (rawRGB >> 8) & 0xFF; uint8_t TFT_MESH_b = rawRGB & 0xFF;
int b = rawRGB & 0xFF; LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b)); if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) {
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b);
} }
} }
@@ -331,13 +356,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
#else #else
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
#endif #endif
#elif defined(USE_ST7796) static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#ifdef ESP_PLATFORM
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7796_SDA,
ST7796_MISO, ST7796_SCK, TFT_SPI_FREQUENCY);
#else
dispdev = new ST7796Spi(&SPI1, ST7796_RESET, ST7796_RS, ST7796_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
#endif
#elif defined(USE_SSD1306) #elif defined(USE_SSD1306)
dispdev = new SSD1306Wire(address.address, -1, -1, geometry, dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -350,7 +369,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
LOG_INFO("SSD1306 init success"); LOG_INFO("SSD1306 init success");
} }
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
dispdev = new TFTDisplay(address.address, -1, -1, geometry, dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
@@ -380,12 +399,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
isAUTOOled = true; isAUTOOled = true;
#endif #endif
#if defined(USE_ST7789)
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#elif defined(USE_ST7796)
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
ui = new OLEDDisplayUi(dispdev); ui = new OLEDDisplayUi(dispdev);
cmdQueue.setReader(this); cmdQueue.setReader(this);
} }
@@ -461,15 +474,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
pinMode(VTFT_LEDA, OUTPUT); pinMode(VTFT_LEDA, OUTPUT);
digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON); digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON);
#endif #endif
#endif
#ifdef USE_ST7796
ui->init();
#ifdef ESP_PLATFORM
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
#else
pinMode(VTFT_LEDA, OUTPUT);
digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON);
#endif
#endif #endif
enabled = true; enabled = true;
setInterval(0); // Draw ASAP setInterval(0); // Draw ASAP
@@ -508,21 +512,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
nrf_gpio_cfg_default(ST7789_NSS); nrf_gpio_cfg_default(ST7789_NSS);
#endif #endif
#endif #endif
#ifdef USE_ST7796
SPI1.end();
#if defined(ARCH_ESP32)
pinMode(VTFT_LEDA, OUTPUT);
digitalWrite(VTFT_LEDA, LOW);
pinMode(ST7796_RESET, ANALOG);
pinMode(ST7796_RS, ANALOG);
pinMode(ST7796_NSS, ANALOG);
#else
nrf_gpio_cfg_default(VTFT_LEDA);
nrf_gpio_cfg_default(ST7796_RESET);
nrf_gpio_cfg_default(ST7796_RS);
nrf_gpio_cfg_default(ST7796_NSS);
#endif
#endif
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
PMU->disablePowerOutput(XPOWERS_ALDO2); PMU->disablePowerOutput(XPOWERS_ALDO2);
@@ -536,10 +525,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
void Screen::setup() void Screen::setup()
{ {
// Enable display rendering // === Enable display rendering ===
useDisplay = true; useDisplay = true;
// Load saved brightness from UI config // === Load saved brightness from UI config ===
// For OLED displays (SSD1306), default brightness is 255 if not set // For OLED displays (SSD1306), default brightness is 255 if not set
if (uiconfig.screen_brightness == 0) { if (uiconfig.screen_brightness == 0) {
#if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) #if defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
@@ -551,7 +540,7 @@ void Screen::setup()
brightness = uiconfig.screen_brightness; brightness = uiconfig.screen_brightness;
} }
// Detect OLED subtype (if supported by board variant) // === Detect OLED subtype (if supported by board variant) ===
#ifdef AutoOLEDWire_h #ifdef AutoOLEDWire_h
if (isAUTOOled) if (isAUTOOled)
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model); static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
@@ -568,12 +557,8 @@ void Screen::setup()
#if defined(MUZI_BASE) #if defined(MUZI_BASE)
dispdev->delayPoweron = true; dispdev->delayPoweron = true;
#endif #endif
#if defined(USE_ST7796) && defined(TFT_MESH)
// Custom text color, if defined in variant.h
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
// Initialize display and UI system // === Initialize display and UI system ===
ui->init(); ui->init();
displayWidth = dispdev->width(); displayWidth = dispdev->width();
displayHeight = dispdev->height(); displayHeight = dispdev->height();
@@ -585,7 +570,7 @@ void Screen::setup()
ui->disableAllIndicators(); // Disable page indicator dots ui->disableAllIndicators(); // Disable page indicator dots
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
// Apply loaded brightness // === Apply loaded brightness ===
#if defined(ST7789_CS) #if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
@@ -593,20 +578,20 @@ void Screen::setup()
#endif #endif
LOG_INFO("Applied screen brightness: %d", brightness); LOG_INFO("Applied screen brightness: %d", brightness);
// Set custom overlay callbacks // === Set custom overlay callbacks ===
static OverlayCallback overlays[] = { static OverlayCallback overlays[] = {
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
}; };
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
// Enable UTF-8 to display mapping // === Enable UTF-8 to display mapping ===
dispdev->setFontTableLookupFunction(customFontTableLookup); dispdev->setFontTableLookupFunction(customFontTableLookup);
#ifdef USERPREFS_OEM_TEXT #ifdef USERPREFS_OEM_TEXT
logo_timeout *= 2; // Give more time for branded boot logos logo_timeout *= 2; // Give more time for branded boot logos
#endif #endif
// Configure alert frames (e.g., "Resuming..." or region name) // === Configure alert frames (e.g., "Resuming..." or region name) ===
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
@@ -622,28 +607,26 @@ void Screen::setup()
ui->setFrames(alertFrames, 1); ui->setFrames(alertFrames, 1);
ui->disableAutoTransition(); // Require manual navigation between frames ui->disableAutoTransition(); // Require manual navigation between frames
// Log buffer for on-screen logs (3 lines max) // === Log buffer for on-screen logs (3 lines max) ===
dispdev->setLogBuffer(3, 32); dispdev->setLogBuffer(3, 32);
// Optional screen mirroring or flipping (e.g. for T-Beam orientation) // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
#ifdef SCREEN_MIRROR #ifdef SCREEN_MIRROR
dispdev->mirrorScreen(); dispdev->mirrorScreen();
#else #else
if (!config.display.flip_screen) { if (!config.display.flip_screen) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically(); static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789) #elif defined(USE_ST7789)
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically(); static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
#elif defined(USE_ST7796)
static_cast<ST7796Spi *>(dispdev)->mirrorScreen();
#elif !defined(M5STACK_UNITC6L) #elif !defined(M5STACK_UNITC6L)
dispdev->flipScreenVertically(); dispdev->flipScreenVertically();
#endif #endif
} }
#endif #endif
// Generate device ID from MAC address // === Generate device ID from MAC address ===
uint8_t dmac[6]; uint8_t dmac[6];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
@@ -652,7 +635,7 @@ void Screen::setup()
handleSetOn(false); // Ensure proper init for Arduino targets handleSetOn(false); // Ensure proper init for Arduino targets
#endif #endif
// Turn on display and trigger first draw // === Turn on display and trigger first draw ===
handleSetOn(true); handleSetOn(true);
determineResolution(dispdev->height(), dispdev->width()); determineResolution(dispdev->height(), dispdev->width());
ui->update(); ui->update();
@@ -669,13 +652,13 @@ void Screen::setup()
touchScreenImpl1->init(); touchScreenImpl1->init();
} }
} }
#elif HAS_TOUCHSCREEN && !defined(USE_EINK) && !HAS_CST226SE #elif HAS_TOUCHSCREEN && !defined(USE_EINK)
touchScreenImpl1 = touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch); new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1->init(); touchScreenImpl1->init();
#endif #endif
// Subscribe to device status updates // === Subscribe to device status updates ===
powerStatusObserver.observe(&powerStatus->onNewStatus); powerStatusObserver.observe(&powerStatus->onNewStatus);
gpsStatusObserver.observe(&gpsStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus);
nodeStatusObserver.observe(&nodeStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus);
@@ -683,14 +666,12 @@ void Screen::setup()
#if !MESHTASTIC_EXCLUDE_ADMIN #if !MESHTASTIC_EXCLUDE_ADMIN
adminMessageObserver.observe(adminModule); adminMessageObserver.observe(adminModule);
#endif #endif
if (textMessageModule)
textMessageObserver.observe(textMessageModule);
if (inputBroker) if (inputBroker)
inputObserver.observe(inputBroker); inputObserver.observe(inputBroker);
// Load persisted messages into RAM // === Notify modules that support UI events ===
messageStore.loadFromFlash();
LOG_INFO("MessageStore loaded from flash");
// Notify modules that support UI events
MeshModule::observeUIEvents(&uiFrameEventObserver); MeshModule::observeUIEvents(&uiFrameEventObserver);
} }
@@ -761,23 +742,6 @@ int32_t Screen::runOnce()
if (displayHeight == 0) { if (displayHeight == 0) {
displayHeight = dispdev->getHeight(); displayHeight = dispdev->getHeight();
} }
// Detect frame transitions and clear message cache when leaving text message screen
{
static int8_t lastFrameIndex = -1;
int8_t currentFrameIndex = ui->getUiState()->currentFrame;
int8_t textMsgIndex = framesetInfo.positions.textMessage;
if (lastFrameIndex != -1 && currentFrameIndex != lastFrameIndex) {
if (lastFrameIndex == textMsgIndex && currentFrameIndex != textMsgIndex) {
graphics::MessageRenderer::clearMessageCache();
}
}
lastFrameIndex = currentFrameIndex;
}
menuHandler::handleMenuSwitch(dispdev); menuHandler::handleMenuSwitch(dispdev);
// Show boot screen for first logo_timeout seconds, then switch to normal operation. // Show boot screen for first logo_timeout seconds, then switch to normal operation.
@@ -833,17 +797,17 @@ int32_t Screen::runOnce()
break; break;
case Cmd::ON_PRESS: case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT); handleOnPress();
} }
break; break;
case Cmd::SHOW_PREV_FRAME: case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::PREVIOUS); handleShowPrevFrame();
} }
break; break;
case Cmd::SHOW_NEXT_FRAME: case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
showFrame(FrameDirection::NEXT); handleShowNextFrame();
} }
break; break;
case Cmd::START_ALERT_FRAME: { case Cmd::START_ALERT_FRAME: {
@@ -864,7 +828,6 @@ int32_t Screen::runOnce()
break; break;
case Cmd::STOP_ALERT_FRAME: case Cmd::STOP_ALERT_FRAME:
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
break;
case Cmd::STOP_BOOT_SCREEN: case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
@@ -1035,6 +998,9 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) { if (!hiddenFrames.home) {
fsi.positions.home = numframes; fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
@@ -1046,16 +1012,11 @@ void Screen::setFrames(FrameFocus focus)
indicatorIcons.push_back(icon_mail); indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK #ifndef USE_EINK
if (!hiddenFrames.nodelist_nodes) { if (!hiddenFrames.nodelist) {
fsi.positions.nodelist_nodes = numframes; fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Nodes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes); indicatorIcons.push_back(icon_nodes);
} }
if (!hiddenFrames.nodelist_location) {
fsi.positions.nodelist_location = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicListScreen_Location;
indicatorIcons.push_back(icon_list);
}
#endif #endif
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
@@ -1077,13 +1038,11 @@ void Screen::setFrames(FrameFocus focus)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (!hiddenFrames.nodelist_bearings) { if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes; fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list); indicatorIcons.push_back(icon_list);
} }
#endif
if (!hiddenFrames.gps) { if (!hiddenFrames.gps) {
fsi.positions.gps = numframes; fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
@@ -1183,7 +1142,7 @@ void Screen::setFrames(FrameFocus focus)
} }
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // Save frame count for use in custom overlay this->frameCount = numframes; // Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes); LOG_DEBUG("Finished build frames. numframes: %d", numframes);
ui->setFrames(normalFrames, numframes); ui->setFrames(normalFrames, numframes);
@@ -1203,6 +1162,10 @@ void Screen::setFrames(FrameFocus focus)
case FOCUS_FAULT: case FOCUS_FAULT:
ui->switchToFrame(fsi.positions.fault); ui->switchToFrame(fsi.positions.fault);
break; break;
case FOCUS_TEXTMESSAGE:
hasUnreadMessage = false; // ✅ Clear when message is *viewed*
ui->switchToFrame(fsi.positions.textMessage);
break;
case FOCUS_MODULE: case FOCUS_MODULE:
// Whichever frame was marked by MeshModule::requestFocus(), if any // Whichever frame was marked by MeshModule::requestFocus(), if any
// If no module requested focus, will show the first frame instead // If no module requested focus, will show the first frame instead
@@ -1245,11 +1208,8 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
void Screen::toggleFrameVisibility(const std::string &frameName) void Screen::toggleFrameVisibility(const std::string &frameName)
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist_nodes") { if (frameName == "nodelist") {
hiddenFrames.nodelist_nodes = !hiddenFrames.nodelist_nodes; hiddenFrames.nodelist = !hiddenFrames.nodelist;
}
if (frameName == "nodelist_location") {
hiddenFrames.nodelist_location = !hiddenFrames.nodelist_location;
} }
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
@@ -1264,11 +1224,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
} }
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") { if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
} }
#endif
if (frameName == "gps") { if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps; hiddenFrames.gps = !hiddenFrames.gps;
} }
@@ -1290,10 +1248,8 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
bool Screen::isFrameHidden(const std::string &frameName) const bool Screen::isFrameHidden(const std::string &frameName) const
{ {
#ifndef USE_EINK #ifndef USE_EINK
if (frameName == "nodelist_nodes") if (frameName == "nodelist")
return hiddenFrames.nodelist_nodes; return hiddenFrames.nodelist;
if (frameName == "nodelist_location")
return hiddenFrames.nodelist_location;
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
if (frameName == "nodelist_lastheard") if (frameName == "nodelist_lastheard")
@@ -1304,10 +1260,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return hiddenFrames.nodelist_distance; return hiddenFrames.nodelist_distance;
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
if (frameName == "nodelist_bearings") if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings; return hiddenFrames.nodelist_bearings;
#endif
if (frameName == "gps") if (frameName == "gps")
return hiddenFrames.gps; return hiddenFrames.gps;
#endif #endif
@@ -1323,6 +1277,37 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false; return false;
} }
// Dismisses the currently displayed screen frame, if possible
// Relevant for text message, waypoint, others in future?
// Triggered with a CardKB keycombo
void Screen::hideCurrentFrame()
{
uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false;
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
LOG_INFO("Hide Text Message");
devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
LOG_DEBUG("Hide Waypoint");
devicestate.has_rx_waypoint = false;
hiddenFrames.waypoint = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Hide WiFi Screen");
hiddenFrames.wifi = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.lora) {
LOG_INFO("Hide LoRa");
hiddenFrames.lora = true;
dismissed = true;
}
if (dismissed) {
setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE
}
}
void Screen::handleStartFirmwareUpdateScreen() void Screen::handleStartFirmwareUpdateScreen()
{ {
LOG_DEBUG("Show firmware screen"); LOG_DEBUG("Show firmware screen");
@@ -1375,6 +1360,28 @@ void Screen::decreaseBrightness()
/* TO DO: add little popup in center of screen saying what brightness level it is set to*/ /* TO DO: add little popup in center of screen saying what brightness level it is set to*/
} }
void Screen::setFunctionSymbol(std::string sym)
{
if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) {
functionSymbol.push_back(sym);
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
}
void Screen::removeFunctionSymbol(std::string sym)
{
functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end());
functionSymbolString = "";
for (auto symbol : functionSymbol) {
functionSymbolString = symbol + " " + functionSymbolString;
}
setFastFramerate();
}
void Screen::handleOnPress() void Screen::handleOnPress()
{ {
// If screen was off, just wake it, otherwise advance to next frame // If screen was off, just wake it, otherwise advance to next frame
@@ -1386,17 +1393,23 @@ void Screen::handleOnPress()
} }
} }
void Screen::showFrame(FrameDirection direction) void Screen::handleShowPrevFrame()
{ {
// Only advance frames when UI is stable // If screen was off, just wake it, otherwise go back to previous frame
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) { if (ui->getUiState()->frameState == FIXED) {
ui->previousFrame();
lastScreenTransition = millis();
setFastFramerate();
}
}
if (direction == FrameDirection::NEXT) { void Screen::handleShowNextFrame()
ui->nextFrame(); {
} else { // If screen was off, just wake it, otherwise advance to next frame
ui->previousFrame(); // If we are in a transition, the press must have bounced, drop it.
} if (ui->getUiState()->frameState == FIXED) {
ui->nextFrame();
lastScreenTransition = millis(); lastScreenTransition = millis();
setFastFramerate(); setFastFramerate();
} }
@@ -1422,6 +1435,7 @@ void Screen::setFastFramerate()
int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleStatusUpdate(const meshtastic::Status *arg)
{ {
// LOG_DEBUG("Screen got status update %d", arg->getStatusType());
switch (arg->getStatusType()) { switch (arg->getStatusType()) {
case STATUS_TYPE_NODE: case STATUS_TYPE_NODE:
if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) {
@@ -1453,14 +1467,14 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Incoming message // Incoming message
devicestate.has_rx_text_message = true; // Needed to include the message frame devicestate.has_rx_text_message = true; // Needed to include the message frame
hasUnreadMessage = true; // Enables mail icon in the header hasUnreadMessage = true; // Enables mail icon in the header
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view (no-op during text_input) setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
// Only wake/force display if the configuration allows it // Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) { if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw forceDisplay(); // Forces screen redraw
} }
// === Prepare banner/popup content === // === Prepare banner content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel = const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex()); channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
@@ -1484,84 +1498,38 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any // Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
// 'mute' preferences set to any specific node or channel. // 'mute' preferences set to any specific node or channel.
// If on-screen keyboard is active, show a transient popup over keyboard instead of interrupting it if (isAlert) {
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
// Wake and force redraw so popup is visible immediately
if (shouldWakeOnReceivedMessage()) {
setOn(true);
forceDisplay();
}
// Build popup: title = message source name, content = message text (sanitized)
// Title
char titleBuf[64] = {0};
if (longName && longName[0]) { if (longName && longName[0]) {
// Sanitize sender name snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
std::string t = sanitizeString(longName);
strncpy(titleBuf, t.c_str(), sizeof(titleBuf) - 1);
} else { } else {
strncpy(titleBuf, "Message", sizeof(titleBuf) - 1); strcpy(banner, "Alert Received");
} }
screen->showSimpleBanner(banner, 3000);
// Content: payload bytes may not be null-terminated, remove ASCII_BELL and sanitize } else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
char content[256] = {0}; if (longName && longName[0]) {
{
std::string raw;
raw.reserve(packet->decoded.payload.size);
for (size_t i = 0; i < packet->decoded.payload.size; ++i) {
char c = msgRaw[i];
if (c == ASCII_BELL)
continue; // strip bell
raw.push_back(c);
}
std::string sanitized = sanitizeString(raw);
strncpy(content, sanitized.c_str(), sizeof(content) - 1);
}
NotificationRenderer::showKeyboardMessagePopupWithTitle(titleBuf, content, 3000);
// Maintain existing buzzer behavior on M5 if applicable
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message");
#else
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
#endif
} else {
strcpy(banner, "New Message");
}
#if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY || if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) || (isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) { (!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep(); playLongBeep();
} }
#endif
} else {
// No keyboard active: use regular banner flow, respecting mute settings
if (isAlert) {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
} else {
strcpy(banner, "Alert Received");
}
screen->showSimpleBanner(banner, 3000);
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
if (longName && longName[0]) {
#if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message");
#else #else
snprintf(banner, sizeof(banner), "New Message from\n%s", longName); screen->showSimpleBanner(banner, 3000);
#endif #endif
} else {
strcpy(banner, "New Message");
}
#if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep();
}
#else
screen->showSimpleBanner(banner, 3000);
#endif
}
} }
} }
} }
@@ -1579,26 +1547,16 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
if (showingNormalScreen) { if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call // Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) { if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
setFrames(FOCUS_MODULE); setFrames(FOCUS_MODULE);
}
// Regenerate the frameset, while attempting to maintain focus on the current frame // Regenerate the frameset, while Attempt to maintain focus on the current frame
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) { else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
setFrames(FOCUS_PRESERVE); setFrames(FOCUS_PRESERVE);
}
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP // Don't regenerate the frameset, just re-draw whatever is on screen ASAP
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) { else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
setFastFramerate(); setFastFramerate();
}
// Jump directly to the Text Message screen
else if (event->action == UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE) {
setFrames(FOCUS_PRESERVE); // preserve current frame ordering
ui->switchToFrame(framesetInfo.positions.textMessage);
setFastFramerate(); // force redraw ASAP
}
} }
return 0; return 0;
@@ -1606,7 +1564,6 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event)
int Screen::handleInputEvent(const InputEvent *event) int Screen::handleInputEvent(const InputEvent *event)
{ {
LOG_INPUT("Screen Input event %u! kb %u", event->inputEvent, event->kbchar);
if (!screenOn) if (!screenOn)
return 0; return 0;
@@ -1636,48 +1593,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::handleMenuSwitch(dispdev); menuHandler::handleMenuSwitch(dispdev);
return 0; return 0;
} }
// UP/DOWN in message screen scrolls through message threads
if (ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (event->inputEvent == INPUT_BROKER_UP) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollUp();
setFastFramerate(); // match existing behavior
return 0;
}
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
if (messageStore.getMessages().empty()) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else {
graphics::MessageRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
}
// UP/DOWN in node list screens scrolls through node pages
if (ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
if (event->inputEvent == INPUT_BROKER_UP) {
graphics::NodeListRenderer::scrollUp();
setFastFramerate();
return 0;
}
if (event->inputEvent == INPUT_BROKER_DOWN) {
graphics::NodeListRenderer::scrollDown();
setFastFramerate();
return 0;
}
}
// Use left or right input from a keyboard to move between frames, // Use left or right input from a keyboard to move between frames,
// so long as a mesh module isn't using these events for some other purpose // so long as a mesh module isn't using these events for some other purpose
if (showingNormalScreen) { if (showingNormalScreen) {
@@ -1691,39 +1607,10 @@ int Screen::handleInputEvent(const InputEvent *event)
// If no modules are using the input, move between frames // If no modules are using the input, move between frames
if (!inputIntercepted) { if (!inputIntercepted) {
#if defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2
bool handledEncoderScroll = false;
const bool isTextMessageFrame = (framesetInfo.positions.textMessage != 255 &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
!messageStore.getMessages().empty());
if (isTextMessageFrame) {
if (event->inputEvent == INPUT_BROKER_UP_LONG) {
graphics::MessageRenderer::nudgeScroll(-1);
handledEncoderScroll = true;
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) {
graphics::MessageRenderer::nudgeScroll(1);
handledEncoderScroll = true;
}
}
if (handledEncoderScroll) {
setFastFramerate();
return 0;
}
#endif
if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) {
showFrame(FrameDirection::PREVIOUS);
} else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
showFrame(FrameDirection::NEXT);
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
// Long press up button for fast frame switching
showPrevFrame(); showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_DOWN_LONG) { } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) {
// Long press down button for fast frame switching
showNextFrame(); showNextFrame();
} else if ((event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) &&
this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
} else if (event->inputEvent == INPUT_BROKER_SELECT) { } else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu(); menuHandler::homeBaseMenu();
@@ -1738,7 +1625,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu(); menuHandler::loraMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (!messageStore.getMessages().empty()) { if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu(); menuHandler::messageResponseMenu();
} else { } else {
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
@@ -1751,8 +1638,7 @@ int Screen::handleInputEvent(const InputEvent *event)
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
menuHandler::favoriteBaseMenu(); menuHandler::favoriteBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_nodes || } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_location ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
@@ -1763,7 +1649,7 @@ int Screen::handleInputEvent(const InputEvent *event)
menuHandler::wifiBaseMenu(); menuHandler::wifiBaseMenu();
} }
} else if (event->inputEvent == INPUT_BROKER_BACK) { } else if (event->inputEvent == INPUT_BROKER_BACK) {
showFrame(FrameDirection::PREVIOUS); showPrevFrame();
} else if (event->inputEvent == INPUT_BROKER_CANCEL) { } else if (event->inputEvent == INPUT_BROKER_CANCEL) {
setOn(false); setOn(false);
} }

View File

@@ -40,6 +40,7 @@ class Screen
FOCUS_DEFAULT, // No specific frame FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT, FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK, FOCUS_CLOCK,
FOCUS_SYSTEM, FOCUS_SYSTEM,
@@ -54,6 +55,8 @@ class Screen
void startFirmwareUpdateScreen() {} void startFirmwareUpdateScreen() {}
void increaseBrightness() {} void increaseBrightness() {}
void decreaseBrightness() {} void decreaseBrightness() {}
void setFunctionSymbol(std::string) {}
void removeFunctionSymbol(std::string) {}
void startAlert(const char *) {} void startAlert(const char *) {}
void showSimpleBanner(const char *message, uint32_t durationMs = 0) {} void showSimpleBanner(const char *message, uint32_t durationMs = 0) {}
void showOverlayBanner(BannerOverlayOptions) {} void showOverlayBanner(BannerOverlayOptions) {}
@@ -80,8 +83,6 @@ class Screen
#include <ST7789Spi.h> #include <ST7789Spi.h>
#elif defined(USE_SPISSD1306) #elif defined(USE_SPISSD1306)
#include <SSD1306Spi.h> #include <SSD1306Spi.h>
#elif defined(USE_ST7796)
#include <ST7796Spi.h>
#else #else
// the SH1106/SSD1306 variant is auto-detected // the SH1106/SSD1306 variant is auto-detected
#include <AutoOLEDWire.h> #include <AutoOLEDWire.h>
@@ -169,8 +170,6 @@ class Point
namespace graphics namespace graphics
{ {
enum class FrameDirection { NEXT, PREVIOUS };
// Forward declarations // Forward declarations
class Screen; class Screen;
@@ -210,6 +209,8 @@ class Screen : public concurrency::OSThread
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate); CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver = CallbackObserver<Screen, const meshtastic::Status *> nodeStatusObserver =
CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate); CallbackObserver<Screen, const meshtastic::Status *>(this, &Screen::handleStatusUpdate);
CallbackObserver<Screen, const meshtastic_MeshPacket *> textMessageObserver =
CallbackObserver<Screen, const meshtastic_MeshPacket *>(this, &Screen::handleTextMessage);
CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver = CallbackObserver<Screen, const UIFrameEvent *> uiFrameEventObserver =
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
CallbackObserver<Screen, const InputEvent *> inputObserver = CallbackObserver<Screen, const InputEvent *> inputObserver =
@@ -220,10 +221,6 @@ class Screen : public concurrency::OSThread
public: public:
OLEDDisplay *getDisplayDevice() { return dispdev; } OLEDDisplay *getDisplayDevice() { return dispdev; }
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
// Screen dimension accessors
inline int getHeight() const { return displayHeight; }
inline int getWidth() const { return displayWidth; }
size_t frameCount = 0; // Total number of active frames size_t frameCount = 0; // Total number of active frames
~Screen(); ~Screen();
@@ -232,6 +229,7 @@ class Screen : public concurrency::OSThread
FOCUS_DEFAULT, // No specific frame FOCUS_DEFAULT, // No specific frame
FOCUS_PRESERVE, // Return to the previous frame FOCUS_PRESERVE, // Return to the previous frame
FOCUS_FAULT, FOCUS_FAULT,
FOCUS_TEXTMESSAGE,
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
FOCUS_CLOCK, FOCUS_CLOCK,
FOCUS_SYSTEM, FOCUS_SYSTEM,
@@ -279,7 +277,6 @@ class Screen : public concurrency::OSThread
void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); }
void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); }
void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); }
void showFrame(FrameDirection direction);
// generic alert start // generic alert start
void startAlert(FrameCallback _alertFrame) void startAlert(FrameCallback _alertFrame)
@@ -347,6 +344,9 @@ class Screen : public concurrency::OSThread
void increaseBrightness(); void increaseBrightness();
void decreaseBrightness(); void decreaseBrightness();
void setFunctionSymbol(std::string sym);
void removeFunctionSymbol(std::string sym);
/// Stops showing the boot screen. /// Stops showing the boot screen.
void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); }
@@ -577,7 +577,7 @@ class Screen : public concurrency::OSThread
// Handle observer events // Handle observer events
int handleStatusUpdate(const meshtastic::Status *arg); int handleStatusUpdate(const meshtastic::Status *arg);
int handleTextMessage(const meshtastic_MeshPacket *packet); int handleTextMessage(const meshtastic_MeshPacket *arg);
int handleUIFrameEvent(const UIFrameEvent *arg); int handleUIFrameEvent(const UIFrameEvent *arg);
int handleInputEvent(const InputEvent *arg); int handleInputEvent(const InputEvent *arg);
int handleAdminMessage(AdminModule_ObserverData *arg); int handleAdminMessage(AdminModule_ObserverData *arg);
@@ -588,6 +588,9 @@ class Screen : public concurrency::OSThread
/// Draws our SSL cert screen during boot (called from WebServer) /// Draws our SSL cert screen during boot (called from WebServer)
void setSSLFrames(); void setSSLFrames();
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
void hideCurrentFrame();
// Menu-driven Show / Hide Toggle // Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName); void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const; bool isFrameHidden(const std::string &frameName) const;
@@ -635,6 +638,8 @@ class Screen : public concurrency::OSThread
// Implementations of various commands, called from doTask(). // Implementations of various commands, called from doTask().
void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); void handleSetOn(bool on, FrameCallback einkScreensaver = NULL);
void handleOnPress(); void handleOnPress();
void handleShowNextFrame();
void handleShowPrevFrame();
void handleStartFirmwareUpdateScreen(); void handleStartFirmwareUpdateScreen();
// Info collected by setFrames method. // Info collected by setFrames method.
@@ -654,8 +659,7 @@ class Screen : public concurrency::OSThread
uint8_t gps = 255; uint8_t gps = 255;
uint8_t home = 255; uint8_t home = 255;
uint8_t textMessage = 255; uint8_t textMessage = 255;
uint8_t nodelist_nodes = 255; uint8_t nodelist = 255;
uint8_t nodelist_location = 255;
uint8_t nodelist_lastheard = 255; uint8_t nodelist_lastheard = 255;
uint8_t nodelist_hopsignal = 255; uint8_t nodelist_hopsignal = 255;
uint8_t nodelist_distance = 255; uint8_t nodelist_distance = 255;
@@ -678,8 +682,7 @@ class Screen : public concurrency::OSThread
bool home = false; bool home = false;
bool clock = false; bool clock = false;
#ifndef USE_EINK #ifndef USE_EINK
bool nodelist_nodes = false; bool nodelist = false;
bool nodelist_location = false;
#endif #endif
#ifdef USE_EINK #ifdef USE_EINK
bool nodelist_lastheard = false; bool nodelist_lastheard = false;
@@ -687,9 +690,7 @@ class Screen : public concurrency::OSThread
bool nodelist_distance = false; bool nodelist_distance = false;
#endif #endif
#if HAS_GPS #if HAS_GPS
#ifdef USE_EINK
bool nodelist_bearings = false; bool nodelist_bearings = false;
#endif
bool gps = false; bool gps = false;
#endif #endif
bool lora = false; bool lora = false;

View File

@@ -73,8 +73,7 @@
#endif #endif
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts // The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19

View File

@@ -17,12 +17,6 @@ namespace graphics
void determineResolution(int16_t screenheight, int16_t screenwidth) void determineResolution(int16_t screenheight, int16_t screenwidth)
{ {
#ifdef FORCE_LOW_RES
isHighResolution = false;
return;
#endif
if (screenwidth > 128) { if (screenwidth > 128) {
isHighResolution = true; isHighResolution = true;
} }
@@ -30,19 +24,11 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
if (screenwidth > 128 && screenheight <= 64) { if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false; isHighResolution = false;
} }
}
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) // Special case for Heltec Wireless Tracker v1.1
{ if (screenwidth == 160 && screenheight == 80) {
hour = 0; isHighResolution = false;
minute = 0; }
second = 0;
if (rtc_sec == 0)
return;
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
hour = hms / SEC_PER_HOUR;
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
second = hms % SEC_PER_MIN;
} }
// === Shared External State === // === Shared External State ===
@@ -213,8 +199,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
if (rtc_sec > 0) { if (rtc_sec > 0) {
// === Build Time String === // === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour, minute, second; int hour = hms / SEC_PER_HOUR;
graphics::decomposeTime(rtc_sec, hour, minute, second); int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String === // === Build Date String ===
@@ -427,12 +413,8 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
} }
if (drawConnectionState) { if (drawConnectionState) {
const int scale = isHighResolution ? 2 : 1;
display->setColor(BLACK);
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
(connection_icon_height * scale) + (2 * scale));
display->setColor(WHITE);
if (isHighResolution) { if (isHighResolution) {
const int scale = 2;
const int bytesPerRow = (connection_icon_width + 7) / 8; const int bytesPerRow = (connection_icon_width + 7) / 8;
int iconX = 0; int iconX = 0;
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2); int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);

View File

@@ -45,8 +45,6 @@ extern bool isMuted;
extern bool isHighResolution; extern bool isHighResolution;
void determineResolution(int16_t screenheight, int16_t screenwidth); void determineResolution(int16_t screenheight, int16_t screenwidth);
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
// Rounded highlight (used for inverted headers) // Rounded highlight (used for inverted headers)
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);

View File

@@ -1,6 +1,5 @@
#include "configuration.h" #include "configuration.h"
#include "main.h" #include "main.h"
#if USE_TFTDISPLAY
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/PortduinoGlue.h"
@@ -124,11 +123,6 @@ static void rak14014_tpIntHandle(void)
_rak14014_touch_int = true; _rak14014_touch_int = true;
} }
#elif defined(HACKADAY_COMMUNICATOR)
#include <Arduino_GFX_Library.h>
Arduino_DataBus *bus = nullptr;
Arduino_GFX *tft = nullptr;
#elif defined(ST72xx_DE) #elif defined(ST72xx_DE)
#include <LovyanGFX.hpp> #include <LovyanGFX.hpp>
#include <TCA9534.h> #include <TCA9534.h>
@@ -1139,6 +1133,9 @@ static LGFX *tft = nullptr;
#endif #endif
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \
defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \
(ARCH_PORTDUINO && HAS_SCREEN != 0)
#include "SPILock.h" #include "SPILock.h"
#include "TFTDisplay.h" #include "TFTDisplay.h"
#include <SPI.h> #include <SPI.h>
@@ -1274,15 +1271,12 @@ void TFTDisplay::display(bool fromBlank)
x_LastPixelUpdate = x; x_LastPixelUpdate = x;
} }
} }
#if defined(HACKADAY_COMMUNICATOR)
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
#else
// Step 4: Send the changed pixels on this line to the screen as a single block transfer. // Step 4: Send the changed pixels on this line to the screen as a single block transfer.
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
&linePixelBuffer[x_FirstPixelUpdate]); &linePixelBuffer[x_FirstPixelUpdate]);
#endif
somethingChanged = true; somethingChanged = true;
} }
y++; y++;
@@ -1346,8 +1340,6 @@ void TFTDisplay::sendCommand(uint8_t com)
display(true); display(true);
if (portduino_config.displayBacklight.pin > 0) if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup(); tft->wakeup();
tft->powerSaveOff(); tft->powerSaveOff();
@@ -1360,8 +1352,7 @@ void TFTDisplay::sendCommand(uint8_t com)
unphone.backlight(true); // using unPhone library unphone.backlight(true); // using unPhone library
#endif #endif
#ifdef RAK14014 #ifdef RAK14014
#elif !defined(M5STACK) && !defined(ST7789_CS) && \ #elif !defined(M5STACK) && !defined(ST7789_CS) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172); tft->setBrightness(172);
#endif #endif
break; break;
@@ -1373,8 +1364,6 @@ void TFTDisplay::sendCommand(uint8_t com)
tft->clear(); tft->clear();
if (portduino_config.displayBacklight.pin > 0) if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep(); tft->sleep();
tft->powerSaveOn(); tft->powerSaveOn();
@@ -1387,7 +1376,7 @@ void TFTDisplay::sendCommand(uint8_t com)
unphone.backlight(false); // using unPhone library unphone.backlight(false); // using unPhone library
#endif #endif
#ifdef RAK14014 #ifdef RAK14014
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) #elif !defined(M5STACK)
tft->setBrightness(0); tft->setBrightness(0);
#endif #endif
break; break;
@@ -1403,7 +1392,7 @@ void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{ {
#ifdef RAK14014 #ifdef RAK14014
// todo // todo
#elif !defined(HACKADAY_COMMUNICATOR) #else
tft->setBrightness(_brightness); tft->setBrightness(_brightness);
LOG_DEBUG("Brightness is set to value: %i ", _brightness); LOG_DEBUG("Brightness is set to value: %i ", _brightness);
#endif #endif
@@ -1421,7 +1410,7 @@ bool TFTDisplay::hasTouch(void)
{ {
#ifdef RAK14014 #ifdef RAK14014
return true; return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) #elif !defined(M5STACK)
return tft->touch() != nullptr; return tft->touch() != nullptr;
#else #else
return false; return false;
@@ -1440,7 +1429,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else { } else {
return false; return false;
} }
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) #elif !defined(M5STACK)
return tft->getTouch(x, y); return tft->getTouch(x, y);
#else #else
return false; return false;
@@ -1459,12 +1448,6 @@ bool TFTDisplay::connect()
LOG_INFO("Do TFT init"); LOG_INFO("Do TFT init");
#ifdef RAK14014 #ifdef RAK14014
tft = new TFT_eSPI; tft = new TFT_eSPI;
#elif defined(HACKADAY_COMMUNICATOR)
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */, 12 /* col offset 1 */,
0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */, nv3007_279_init_operations,
sizeof(nv3007_279_init_operations));
#else #else
tft = new LGFX; tft = new LGFX;
#endif #endif
@@ -1475,15 +1458,8 @@ bool TFTDisplay::connect()
#ifdef UNPHONE #ifdef UNPHONE
unphone.backlight(true); // using unPhone library unphone.backlight(true); // using unPhone library
#endif #endif
#ifdef HACKADAY_COMMUNICATOR
bool beginStatus = tft->begin();
if (beginStatus)
LOG_DEBUG("TFT Success!");
else
LOG_ERROR("TFT Fail!");
#else
tft->init(); tft->init();
#endif
#if defined(M5STACK) #if defined(M5STACK)
tft->setRotation(0); tft->setRotation(0);
@@ -1516,4 +1492,4 @@ bool TFTDisplay::connect()
return true; return true;
} }
#endif // USE_TFTDISPLAY #endif

View File

@@ -354,6 +354,8 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
if (screenHeight <= 64) { if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2; textY = boxY + (boxHeight - inputLineH) / 2;
} else { } else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1; const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2; const int innerBottom = boxY + boxHeight - 2;
@@ -504,9 +506,6 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
centeredTextY -= 1; centeredTextY -= 1;
} }
} }
#ifdef MUZI_BASE // Correct issue with character vertical position on MUZI_BASE
centeredTextY -= 2;
#endif
display->drawString(textX, centeredTextY, keyText.c_str()); display->drawString(textX, centeredTextY, keyText.c_str());
} }

View File

@@ -1,10 +1,15 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "main.h" #include "main.h"
@@ -18,31 +23,6 @@ namespace graphics
namespace ClockRenderer namespace ClockRenderer
{ {
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
// Segment layout reference:
//
// ___1___
// 6 | | 2
// |_7___|
// 5 | | 3
// |___4_|
//
// Segment order: [1, 2, 3, 4, 5, 6, 7]
//
static const uint8_t PROGMEM digitSegments[10][7] = {
{1, 1, 1, 1, 1, 1, 0}, // 0
{0, 1, 1, 0, 0, 0, 0}, // 1
{1, 1, 0, 1, 1, 0, 1}, // 2
{1, 1, 1, 1, 0, 0, 1}, // 3
{0, 1, 1, 0, 0, 1, 1}, // 4
{1, 0, 1, 1, 0, 1, 1}, // 5
{1, 0, 1, 1, 1, 1, 1}, // 6
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8
{1, 1, 1, 1, 0, 1, 1} // 9
};
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
{ {
uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentWidth = SEGMENT_WIDTH * scale;
@@ -50,7 +30,7 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale); uint16_t topAndBottomX = x + (4 * scale);
uint16_t quarterCellHeight = cellHeight / 4; uint16_t quarterCellHeight = cellHeight / 4;
@@ -63,16 +43,34 @@ void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
{ {
// Read 7-segment pattern for the digit from flash // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
uint8_t seg[7]; // segment {innerIndex + 1}
for (uint8_t i = 0; i < 7; i++) { // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
seg[i] = pgm_read_byte(&digitSegments[number][i]); uint8_t numbers[10][7] = {
} {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
{0, 1, 1, 0, 0, 0, 0}, // 1 1
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
{1, 1, 1, 0, 0, 1, 0}, // 7
{1, 1, 1, 1, 1, 1, 1}, // 8 4
{1, 1, 1, 1, 0, 1, 1}, // 9
};
// the width and height of each segment's central rectangle:
// _____________________
// ⋰| (only this part, |⋱
// ⋰ | not including | ⋱
// ⋱ | the triangles | ⋰
// ⋱| on the ends) |⋰
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
uint16_t segmentWidth = SEGMENT_WIDTH * scale; uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale; uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
// Precompute segment positions // segment x and y coordinates
uint16_t segmentOneX = x + segmentHeight + 2; uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y; uint16_t segmentOneY = y;
@@ -94,21 +92,33 @@ void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t n
uint16_t segmentSevenX = segmentOneX; uint16_t segmentSevenX = segmentOneX;
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
// Draw only the active segments if (numbers[number][0]) {
if (seg[0]) graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); }
if (seg[1])
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); if (numbers[number][1]) {
if (seg[2]) graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); }
if (seg[3])
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); if (numbers[number][2]) {
if (seg[4]) graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); }
if (seg[5])
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); if (numbers[number][3]) {
if (seg[6]) graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); }
if (numbers[number][4]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
}
if (numbers[number][5]) {
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
}
if (numbers[number][6]) {
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
}
} }
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
@@ -137,6 +147,42 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
} }
/*
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
{
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
if (digitalMode) {
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
display->drawCircle(centerX, centerY, radius);
display->drawCircle(centerX, centerY, radius + 1);
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
} else {
uint16_t segmentOneX = x + segmentHeight + 2;
uint16_t segmentOneY = y;
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
uint16_t segmentThreeX = segmentOneX;
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
uint16_t segmentFourX = x;
uint16_t segmentFourY = y + segmentHeight + 2;
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
}
}
*/
// Draw a digital clock
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->clear(); display->clear();
@@ -146,6 +192,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
char timeString[16]; char timeString[16];
@@ -221,9 +268,10 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
scaleInitialized = true; scaleInitialized = true;
} }
// calculate hours:minutes string width
size_t len = strlen(timeString); size_t len = strlen(timeString);
uint16_t timeStringWidth = len * 5;
// calculate hours:minutes string width
uint16_t timeStringWidth = len * 5; // base spacing between characters
for (size_t i = 0; i < len; i++) { for (size_t i = 0; i < len; i++) {
char character = timeString[i]; char character = timeString[i];
@@ -291,13 +339,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true, true);
int line = 0;
// clock face center coordinates // clock face center coordinates
int16_t centerX = display->getWidth() / 2; int16_t centerX = display->getWidth() / 2;
int16_t centerY = display->getHeight() / 2; int16_t centerY = display->getHeight() / 2;
// clock face radius // clock face radius
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9; int16_t radius = 0;
if (display->getHeight() < display->getWidth()) {
radius = (display->getHeight() / 2) * 0.9;
} else {
radius = (display->getWidth() / 2) * 0.9;
}
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
radius = (display->getWidth() / 2) * 0.8; radius = (display->getWidth() / 2) * 0.8;
#endif #endif
@@ -312,8 +366,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// tick mark outer y coordinate; (first nested circle) // tick mark outer y coordinate; (first nested circle)
int16_t tickMarkOuterNoonY = secondHandNoonY; int16_t tickMarkOuterNoonY = secondHandNoonY;
double secondsTickMarkInnerNoonY = noonY + (isHighResolution ? 8 : 4); // seconds tick mark inner y coordinate; (second nested circle)
double hoursTickMarkInnerNoonY = noonY + (isHighResolution ? 16 : 6); double secondsTickMarkInnerNoonY = (double)noonY + 4;
if (isHighResolution) {
secondsTickMarkInnerNoonY = (double)noonY + 8;
}
// hours tick mark inner y coordinate; (third nested circle)
double hoursTickMarkInnerNoonY = (double)noonY + 6;
if (isHighResolution) {
hoursTickMarkInnerNoonY = (double)noonY + 16;
}
// minute hand y coordinate // minute hand y coordinate
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
@@ -333,11 +396,17 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
if (rtc_sec > 0) { if (rtc_sec > 0) {
int hour, minute, second; long hms = rtc_sec % SEC_PER_DAY;
decomposeTime(rtc_sec, hour, minute, second); hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
bool isPM = hour >= 12;
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
bool isPM = hour >= 12; isPM = hour >= 12;
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0; int yOffset = isHighResolution ? 1 : 0;
#ifdef USE_EINK #ifdef USE_EINK

View File

@@ -97,7 +97,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
8, imgQuestionL1); 8, imgQuestionL1);
@@ -109,8 +109,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
#endif #endif
} else { } else {
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
8, imgSFL1); 8, imgSFL1);
@@ -126,7 +125,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
// TODO: Raspberry Pi supports more than just the one screen size // TODO: Raspberry Pi supports more than just the one screen size
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796) || ARCH_PORTDUINO) && \ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
imgInfoL1); imgInfoL1);
@@ -301,8 +300,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s // Tear apart hms into h:m:s
int hour, min, sec; int hour = hms / SEC_PER_HOUR;
graphics::decomposeTime(rtc_sec, hour, min, sec); int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
char timebuf[12]; char timebuf[12];
@@ -531,10 +531,8 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
const int labelX = x; const int labelX = x;
int barsOffset = (isHighResolution) ? 24 : 0; int barsOffset = (isHighResolution) ? 24 : 0;
#ifdef USE_EINK #ifdef USE_EINK
#ifndef T_DECK_PRO
barsOffset -= 12; barsOffset -= 12;
#endif #endif
#endif
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset; const int barX = x + 45 + barsOffset;
#else #else
@@ -575,7 +573,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#endif #endif
// Value string // Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT); display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(SCREEN_WIDTH, getTextPositions(display)[line], combinedStr); display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
}; };
// === Memory values === // === Memory values ===
@@ -706,22 +704,10 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10; int textX_offset = 10;
if (isHighResolution) { if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4; textX_offset = textX_offset * 4;
const int scale = 2; display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
const int bytesPerRow = (chirpy_width + 7) / 8;
for (int yy = 0; yy < chirpy_height; ++yy) {
iconX = SCREEN_WIDTH - (chirpy_width * 2) - ((chirpy_width * 2) / 3);
iconY = (SCREEN_HEIGHT - (chirpy_height * 2)) / 2;
const uint8_t *rowPtr = chirpy + yy * bytesPerRow;
for (int xx = 0; xx < chirpy_width; ++xx) {
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
if (byteVal & bitMask) {
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
}
}
}
} else { } else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
} }

View File

@@ -11,6 +11,7 @@
#include "graphics/draw/CompassRenderer.h" #include "graphics/draw/CompassRenderer.h"
#include "graphics/draw/DebugRenderer.h" #include "graphics/draw/DebugRenderer.h"
#include "graphics/draw/NodeListRenderer.h" #include "graphics/draw/NodeListRenderer.h"
#include "graphics/draw/ScreenRenderer.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
namespace graphics namespace graphics
@@ -29,6 +30,8 @@ using namespace ClockRenderer;
using namespace CompassRenderer; using namespace CompassRenderer;
using namespace DebugRenderer; using namespace DebugRenderer;
using namespace NodeListRenderer; using namespace NodeListRenderer;
using namespace ScreenRenderer;
using namespace UIRenderer;
} // namespace DrawRenderers } // namespace DrawRenderers

View File

@@ -1,22 +1,18 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "Default.h"
#include "GPS.h" #include "GPS.h"
#include "MenuHandler.h" #include "MenuHandler.h"
#include "MeshRadio.h" #include "MeshRadio.h"
#include "MeshService.h" #include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "buzz.h" #include "buzz.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h" #include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h"
#include "main.h" #include "main.h"
#include "mesh/Default.h"
#include "mesh/MeshTypes.h" #include "mesh/MeshTypes.h"
#include "modules/AdminModule.h" #include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h" #include "modules/CannedMessageModule.h"
@@ -410,35 +406,27 @@ void menuHandler::clockMenu()
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::messageResponseMenu() void menuHandler::messageResponseMenu()
{ {
enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, Aloud, enumEnd }; enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
int options = 3;
static const char *optionsArray[enumEnd]; if (kb_found) {
static int optionsEnumArray[enumEnd]; optionsArray[options] = "Reply via Freetext";
int options = 0; optionsEnumArray[options++] = Freetext;
}
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// New Reply submenu (replaces Preset and Freetext directly in this menu)
optionsArray[options] = "Reply";
optionsEnumArray[options++] = ReplyMenu;
optionsArray[options] = "View Chats";
optionsEnumArray[options++] = ViewMode;
// Delete submenu
optionsArray[options] = "Delete";
optionsEnumArray[options++] = 900;
#ifdef HAS_I2S #ifdef HAS_I2S
optionsArray[options] = "Read Aloud"; optionsArray[options] = "Read Aloud";
optionsEnumArray[options++] = Aloud; optionsEnumArray[options++] = Aloud;
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message"; bannerOptions.message = "Message";
@@ -449,376 +437,29 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
LOG_DEBUG("messageResponseMenu: selected %d", selected); if (selected == Dismiss) {
screen->hideCurrentFrame();
auto mode = graphics::MessageRenderer::getThreadMode(); } else if (selected == Preset) {
int ch = graphics::MessageRenderer::getThreadChannel(); if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
uint32_t peer = graphics::MessageRenderer::getThreadPeer(); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
if (selected == ViewMode) {
menuHandler::menuQueue = menuHandler::message_viewmode_menu;
screen->runNow();
// Reply submenu
} else if (selected == ReplyMenu) {
menuHandler::menuQueue = menuHandler::reply_menu;
screen->runNow();
// Delete submenu
} else if (selected == 900) {
menuHandler::menuQueue = menuHandler::delete_messages_menu;
screen->runNow();
// Delete oldest FIRST (only change)
} else if (selected == DeleteOldest) {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
// Global oldest
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
// Oldest in current channel
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
// Oldest in current DM
messageStore.deleteOldestMessageWithPeer(peer);
} }
} else if (selected == Freetext) {
// Delete all messages if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
} else if (selected == DeleteAll) { cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
messageStore.clearAllMessages(); } else {
graphics::MessageRenderer::clearThreadRegistries(); cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
graphics::MessageRenderer::clearMessageCache(); }
}
#ifdef HAS_I2S #ifdef HAS_I2S
} else if (selected == Aloud) { else if (selected == Aloud) {
const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes); const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
audioThread->readAloud(msg); audioThread->readAloud(msg);
}
#endif #endif
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::replyMenu()
{
enum replyOptions { Back = 0, ReplyPreset, ReplyFreetext, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
// Back
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
// Preset reply
optionsArray[options] = "With Preset";
optionsEnumArray[options++] = ReplyPreset;
// Freetext reply (only when keyboard exists)
if (kb_found) {
optionsArray[options] = "With Freetext";
optionsEnumArray[options++] = ReplyFreetext;
}
BannerOverlayOptions bannerOptions;
// Dynamic title based on thread mode
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
bannerOptions.message = "Reply to Channel";
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
bannerOptions.message = "Reply to DM";
} else {
// View All
bannerOptions.message = "Reply to Last Msg";
}
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void {
auto mode = graphics::MessageRenderer::getThreadMode();
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
// Preset reply
if (selected == ReplyPreset) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
// Freetext reply
if (selected == ReplyFreetext) {
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
cannedMessageModule->LaunchFreetextWithDestination(peer);
} else {
// Fallback for last received message
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
} else {
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
}
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::deleteMessagesMenu()
{
enum optionsNumbers { Back = 0, DeleteOldest, DeleteThis, DeleteAll, enumEnd };
static const char *optionsArray[enumEnd];
static int optionsEnumArray[enumEnd];
int options = 0;
auto mode = graphics::MessageRenderer::getThreadMode();
optionsArray[options] = "Back";
optionsEnumArray[options++] = Back;
optionsArray[options] = "Delete Oldest";
optionsEnumArray[options++] = DeleteOldest;
// If viewing ALL chats → hide “Delete This Chat”
if (mode != graphics::MessageRenderer::ThreadMode::ALL) {
optionsArray[options] = "Delete This Chat";
optionsEnumArray[options++] = DeleteThis;
}
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Delete All";
#else
optionsArray[options] = "Delete All Chats";
#endif
optionsEnumArray[options++] = DeleteAll;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Delete Messages";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [mode](int selected) -> void {
int ch = graphics::MessageRenderer::getThreadChannel();
uint32_t peer = graphics::MessageRenderer::getThreadPeer();
if (selected == Back) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
return;
}
if (selected == DeleteAll) {
LOG_INFO("Deleting all messages");
messageStore.clearAllMessages();
graphics::MessageRenderer::clearThreadRegistries();
graphics::MessageRenderer::clearMessageCache();
return;
}
if (selected == DeleteOldest) {
LOG_INFO("Deleting oldest message");
if (mode == graphics::MessageRenderer::ThreadMode::ALL) {
messageStore.deleteOldestMessage();
} else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteOldestMessageInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteOldestMessageWithPeer(peer);
}
return;
}
// This only appears in non-ALL modes
if (selected == DeleteThis) {
LOG_INFO("Deleting all messages in this thread");
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) {
messageStore.deleteAllMessagesInChannel(ch);
} else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
messageStore.deleteAllMessagesWithPeer(peer);
}
return;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::messageViewModeMenu()
{
auto encodeChannelId = [](int ch) -> int { return 100 + ch; };
auto isChannelSel = [](int id) -> bool { return id >= 100 && id < 200; };
static std::vector<std::string> labels;
static std::vector<int> ids;
static std::vector<uint32_t> idToPeer; // DM lookup
labels.clear();
ids.clear();
idToPeer.clear();
labels.push_back("Back");
ids.push_back(-1);
labels.push_back("View All Chats");
ids.push_back(-2);
// Channels with messages
for (int ch = 0; ch < 8; ++ch) {
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (!msgs.empty()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(encodeChannelId(ch));
LOG_DEBUG("messageViewModeMenu: Added live channel %s (id=%d)", buf, encodeChannelId(ch));
}
}
// Registry channels
for (int ch : graphics::MessageRenderer::getSeenChannels()) {
if (ch < 0 || ch >= 8)
continue;
auto msgs = messageStore.getChannelMessages((uint8_t)ch);
if (msgs.empty())
continue;
int enc = encodeChannelId(ch);
if (std::find(ids.begin(), ids.end(), enc) == ids.end()) {
char buf[40];
const char *cname = channels.getName(ch);
snprintf(buf, sizeof(buf), cname && cname[0] ? "#%s" : "#Ch%d", cname ? cname : "", ch);
labels.push_back(buf);
ids.push_back(enc);
LOG_DEBUG("messageViewModeMenu: Added registry channel %s (id=%d)", buf, enc);
}
}
// Gather unique peers
auto dms = messageStore.getDirectMessages();
std::vector<uint32_t> uniquePeers;
for (auto &m : dms) {
uint32_t peer = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender;
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
for (uint32_t peer : graphics::MessageRenderer::getSeenPeers()) {
if (peer != nodeDB->getNodeNum() && std::find(uniquePeers.begin(), uniquePeers.end(), peer) == uniquePeers.end())
uniquePeers.push_back(peer);
}
std::sort(uniquePeers.begin(), uniquePeers.end());
// Encode peers
for (size_t i = 0; i < uniquePeers.size(); ++i) {
uint32_t peer = uniquePeers[i];
auto node = nodeDB->getMeshNode(peer);
std::string name;
if (node && node->has_user)
name = sanitizeString(node->user.long_name).substr(0, 15);
else {
char buf[20];
snprintf(buf, sizeof(buf), "Node %08X", peer);
name = buf;
}
labels.push_back("@" + name);
int encPeer = 1000 + (int)idToPeer.size();
ids.push_back(encPeer);
idToPeer.push_back(peer);
LOG_DEBUG("messageViewModeMenu: Added DM %s peer=0x%08x id=%d", name.c_str(), (unsigned int)peer, encPeer);
}
// Active ID
int activeId = -2;
auto mode = graphics::MessageRenderer::getThreadMode();
if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL)
activeId = encodeChannelId(graphics::MessageRenderer::getThreadChannel());
else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) {
uint32_t cur = graphics::MessageRenderer::getThreadPeer();
for (size_t i = 0; i < idToPeer.size(); ++i)
if (idToPeer[i] == cur) {
activeId = 1000 + (int)i;
break;
}
}
LOG_DEBUG("messageViewModeMenu: Active thread id=%d", activeId);
// Build banner
static std::vector<const char *> options;
static std::vector<int> optionIds;
options.clear();
optionIds.clear();
int initialIndex = 0;
for (size_t i = 0; i < labels.size(); i++) {
options.push_back(labels[i].c_str());
optionIds.push_back(ids[i]);
if (ids[i] == activeId)
initialIndex = (int)i;
}
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Conversation";
bannerOptions.optionsArrayPtr = options.data();
bannerOptions.optionsEnumPtr = optionIds.data();
bannerOptions.optionsCount = options.size();
bannerOptions.InitialSelected = initialIndex;
bannerOptions.bannerCallback = [=](int selected) -> void {
LOG_DEBUG("messageViewModeMenu: selected=%d", selected);
if (selected == -1) {
menuHandler::menuQueue = menuHandler::message_response_menu;
screen->runNow();
} else if (selected == -2) {
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL);
} else if (isChannelSel(selected)) {
int ch = selected - 100;
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::CHANNEL, ch);
} else if (selected >= 1000) {
int idx = selected - 1000;
if (idx >= 0 && (size_t)idx < idToPeer.size()) {
uint32_t peer = idToPeer[idx];
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1, peer);
}
}
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
@@ -844,6 +485,16 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Send Node Info"; optionsArray[options] = "Send Node Info";
} }
optionsEnumArray[options++] = Position; optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
#else
optionsArray[options] = "New Preset Msg";
#endif
optionsEnumArray[options++] = Preset;
if (kb_found) {
optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext;
}
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
@@ -925,7 +576,7 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu() void menuHandler::systemBaseMenu()
{ {
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, WiFiToggle, PowerMenu, Test, enumEnd }; enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"}; static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back}; static int optionsEnumArray[enumEnd] = {Back};
int options = 1; int options = 1;
@@ -941,10 +592,6 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Bluetooth Toggle"; optionsArray[options] = "Bluetooth Toggle";
#endif #endif
optionsEnumArray[options++] = Bluetooth; optionsEnumArray[options++] = Bluetooth;
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
optionsArray[options] = "WiFi Toggle";
optionsEnumArray[options++] = WiFiToggle;
#endif
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
optionsArray[options] = "Power"; optionsArray[options] = "Power";
#else #else
@@ -982,11 +629,6 @@ void menuHandler::systemBaseMenu()
} else if (selected == Bluetooth) { } else if (selected == Bluetooth) {
menuQueue = bluetooth_toggle_menu; menuQueue = bluetooth_toggle_menu;
screen->runNow(); screen->runNow();
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
} else if (selected == WiFiToggle) {
menuQueue = wifi_toggle_menu;
screen->runNow();
#endif
} else if (selected == Back && !test_enabled) { } else if (selected == Back && !test_enabled) {
test_count++; test_count++;
if (test_count > 4) { if (test_count > 4) {
@@ -999,37 +641,19 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu() void menuHandler::favoriteBaseMenu()
{ {
enum optionsNumbers { Back, Preset, Freetext, GoToChat, Remove, TraceRoute, enumEnd }; enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
// Only show "View Conversation" if a message exists with this node
uint32_t peer = graphics::UIRenderer::currentFavoriteNodeNum;
bool hasConversation = false;
for (const auto &m : messageStore.getMessages()) {
if ((m.sender == peer || m.dest == peer)) {
hasConversation = true;
break;
}
}
if (hasConversation) {
optionsArray[options] = "Go To Chat";
optionsEnumArray[options++] = GoToChat;
}
#if defined(M5STACK_UNITC6L) #if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset"; static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
#else #else
optionsArray[options] = "New Preset Msg"; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
#endif #endif
optionsEnumArray[options++] = Preset; static int optionsEnumArray[enumEnd] = {Back, Preset};
int options = 2;
if (kb_found) { if (kb_found) {
optionsArray[options] = "New Freetext Msg"; optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext; optionsEnumArray[options++] = Freetext;
} }
#if !defined(M5STACK_UNITC6L) #if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Trace Route"; optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute; optionsEnumArray[options++] = TraceRoute;
@@ -1051,17 +675,6 @@ void menuHandler::favoriteBaseMenu()
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
} else if (selected == Freetext) { } else if (selected == Freetext) {
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
}
// Handle new Go To Thread action
else if (selected == GoToChat) {
// Switch thread to direct conversation with this node
graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::DIRECT, -1,
graphics::UIRenderer::currentFavoriteNodeNum);
// Manually create and send a UIFrameEvent to trigger the jump
UIFrameEvent evt;
evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE;
screen->handleUIFrameEvent(&evt);
} else if (selected == Remove) { } else if (selected == Remove) {
menuHandler::menuQueue = menuHandler::remove_favorite; menuHandler::menuQueue = menuHandler::remove_favorite;
screen->runNow(); screen->runNow();
@@ -1111,31 +724,20 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu() void menuHandler::nodeListMenu()
{ {
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"}; #if defined(M5STACK_UNITC6L)
static int optionsEnumArray[enumEnd] = {Back}; static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
int options = 1; #else
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
optionsArray[options] = "Add Favorite";
optionsEnumArray[options++] = Favorite;
optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute;
#if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Key Verification";
optionsEnumArray[options++] = Verify;
#endif #endif
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
optionsArray[options] = "Reset NodeDB";
optionsEnumArray[options++] = Reset;
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action"; bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; #if defined(M5STACK_UNITC6L)
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsCount = 3;
#else
bannerOptions.optionsCount = 5;
#endif
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) { if (selected == Favorite) {
menuQueue = add_favorite; menuQueue = add_favorite;
@@ -1149,9 +751,6 @@ void menuHandler::nodeListMenu()
} else if (selected == TraceRoute) { } else if (selected == TraceRoute) {
menuQueue = trace_route_menu; menuQueue = trace_route_menu;
screen->runNow(); screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1175,7 +774,7 @@ void menuHandler::nodeNameLengthMenu()
LOG_INFO("Setting names to short"); LOG_INFO("Setting names to short");
config.display.use_long_node_name = false; config.display.use_long_node_name = false;
} else if (selected == Back) { } else if (selected == Back) {
menuQueue = node_base_menu; menuQueue = screen_options_menu;
screen->runNow(); screen->runNow();
} }
}; };
@@ -1203,9 +802,6 @@ void menuHandler::resetNodeDBMenu()
LOG_INFO("Initiate node-db reset but keeping favorites"); LOG_INFO("Initiate node-db reset but keeping favorites");
nodeDB->resetNodes(1); nodeDB->resetNodes(1);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == 0) {
menuQueue = node_base_menu;
screen->runNow();
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1435,16 +1031,14 @@ void menuHandler::switchToMUIMenu()
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
{ {
static const char *optionsArray[] = { static const char *optionsArray[] = {"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Teal",
"Back", "Default", "Meshtastic Green", "Yellow", "Red", "Orange", "Purple", "Blue", "Teal", "Cyan", "Ice", "Pink", "Pink", "White"};
"White", "Gray"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Select Screen Color"; bannerOptions.message = "Select Screen Color";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 14; bannerOptions.optionsCount = 10;
bannerOptions.bannerCallback = [display](int selected) -> void { bannerOptions.bannerCallback = [display](int selected) -> void {
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_r = 0;
uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_g = 0;
uint8_t TFT_MESH_b = 0; uint8_t TFT_MESH_b = 0;
@@ -1477,40 +1071,20 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
TFT_MESH_g = 153; TFT_MESH_g = 153;
TFT_MESH_b = 255; TFT_MESH_b = 255;
} else if (selected == 7) { } else if (selected == 7) {
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"); LOG_INFO("Setting color to Teal");
TFT_MESH_r = 16; TFT_MESH_r = 64;
TFT_MESH_g = 102; TFT_MESH_g = 224;
TFT_MESH_b = 102; TFT_MESH_b = 208;
} else if (selected == 9) { } else if (selected == 8) {
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"); LOG_INFO("Setting color to Pink");
TFT_MESH_r = 255; TFT_MESH_r = 255;
TFT_MESH_g = 105; TFT_MESH_g = 105;
TFT_MESH_b = 180; TFT_MESH_b = 180;
} else if (selected == 12) { } else if (selected == 9) {
LOG_INFO("Setting color to White"); LOG_INFO("Setting color to White");
TFT_MESH_r = 255; TFT_MESH_r = 255;
TFT_MESH_g = 255; TFT_MESH_g = 255;
TFT_MESH_b = 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 { } else {
menuQueue = system_base_menu; menuQueue = system_base_menu;
screen->runNow(); screen->runNow();
@@ -1564,7 +1138,6 @@ void menuHandler::rebootMenu()
if (selected == 1) { if (selected == 1) {
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0)); IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk(); nodeDB->saveToDisk();
messageStore.saveToFlash();
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
} else { } else {
menuQueue = power_menu; menuQueue = power_menu;
@@ -1705,28 +1278,19 @@ void menuHandler::wifiBaseMenu()
void menuHandler::wifiToggleMenu() void menuHandler::wifiToggleMenu()
{ {
enum optionsNumbers { Back, Wifi_disable, Wifi_enable }; enum optionsNumbers { Back, Wifi_toggle };
static const char *optionsArray[] = {"Back", "WiFi Disabled", "WiFi Enabled"}; static const char *optionsArray[] = {"Back", "Disable"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "WiFi Actions"; bannerOptions.message = "Disable Wifi and\nEnable Bluetooth?";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3; bannerOptions.optionsCount = 2;
if (config.network.wifi_enabled == true)
bannerOptions.InitialSelected = 2;
else
bannerOptions.InitialSelected = 1;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Wifi_disable) { if (selected == Wifi_toggle) {
config.network.wifi_enabled = false; config.network.wifi_enabled = false;
config.bluetooth.enabled = true; config.bluetooth.enabled = true;
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} else if (selected == Wifi_enable) {
config.network.wifi_enabled = true;
config.bluetooth.enabled = false;
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1769,11 +1333,16 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false; hasSupportBrightness = false;
#endif #endif
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor, FrameToggles, DisplayUnits };
static const char *optionsArray[5] = {"Back"}; static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back}; static int optionsEnumArray[5] = {Back};
int options = 1; int options = 1;
#if defined(T_DECK) || defined(T_LORA_PAGER)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
// Only show brightness for B&W displays // Only show brightness for B&W displays
if (hasSupportBrightness) { if (hasSupportBrightness) {
optionsArray[options] = "Brightness"; optionsArray[options] = "Brightness";
@@ -1781,13 +1350,12 @@ void menuHandler::screenOptionsMenu()
} }
// Only show screen color for TFT displays // Only show screen color for TFT displays
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \ #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
optionsArray[options] = "Screen Color"; optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor; optionsEnumArray[options++] = ScreenColor;
#endif #endif
optionsArray[options] = "Frame Visibility"; optionsArray[options] = "Frame Visibility Toggle";
optionsEnumArray[options++] = FrameToggles; optionsEnumArray[options++] = FrameToggles;
optionsArray[options] = "Display Units"; optionsArray[options] = "Display Units";
@@ -1805,6 +1373,9 @@ void menuHandler::screenOptionsMenu()
} else if (selected == ScreenColor) { } else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker; menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow(); screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else if (selected == FrameToggles) { } else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
@@ -1900,8 +1471,7 @@ void menuHandler::FrameToggles_menu()
{ {
enum optionsNumbers { enum optionsNumbers {
Finish, Finish,
nodelist_nodes, nodelist,
nodelist_location,
nodelist_lastheard, nodelist_lastheard,
nodelist_hopsignal, nodelist_hopsignal,
nodelist_distance, nodelist_distance,
@@ -1922,25 +1492,20 @@ void menuHandler::FrameToggles_menu()
static int lastSelectedIndex = 0; static int lastSelectedIndex = 0;
#ifndef USE_EINK #ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_nodes") ? "Show Node Lists" : "Hide Node Lists"; optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist_nodes; optionsEnumArray[options++] = nodelist;
#else #endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard; optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal; optionsEnumArray[options++] = nodelist_hopsignal;
#endif
#if HAS_GPS
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_location") ? "Show Position Lists" : "Hide Position Lists";
optionsEnumArray[options++] = nodelist_location;
#else
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance; optionsEnumArray[options++] = nodelist_distance;
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show NL - Bearings" : "Hide NL - Bearings";
optionsEnumArray[options++] = nodelist_bearings;
#endif #endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsEnumArray[options++] = nodelist_bearings;
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps; optionsEnumArray[options++] = gps;
@@ -1979,12 +1544,8 @@ void menuHandler::FrameToggles_menu()
if (selected == Finish) { if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT); screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist_nodes) { } else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist_nodes"); screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_location) {
screen->toggleFrameVisibility("nodelist_location");
menuHandler::menuQueue = menuHandler::FrameToggles; menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow(); screen->runNow();
} else if (selected == nodelist_lastheard) { } else if (selected == nodelist_lastheard) {
@@ -2100,9 +1661,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case position_base_menu: case position_base_menu:
positionBaseMenu(); positionBaseMenu();
break; break;
case node_base_menu:
nodeListMenu();
break;
#if !MESHTASTIC_EXCLUDE_GPS #if !MESHTASTIC_EXCLUDE_GPS
case gps_toggle_menu: case gps_toggle_menu:
GPSToggleMenu(); GPSToggleMenu();
@@ -2183,18 +1741,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case throttle_message: case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break; break;
case message_response_menu:
messageResponseMenu();
break;
case reply_menu:
replyMenu();
break;
case delete_messages_menu:
deleteMessagesMenu();
break;
case message_viewmode_menu:
messageViewModeMenu();
break;
} }
menuQueue = menu_none; menuQueue = menu_none;
} }
@@ -2206,4 +1752,4 @@ void menuHandler::saveUIConfig()
} // namespace graphics } // namespace graphics
#endif #endif

View File

@@ -19,7 +19,6 @@ class menuHandler
clock_face_picker, clock_face_picker,
clock_menu, clock_menu,
position_base_menu, position_base_menu,
node_base_menu,
gps_toggle_menu, gps_toggle_menu,
gps_format_menu, gps_format_menu,
compass_point_north_menu, compass_point_north_menu,
@@ -44,10 +43,6 @@ class menuHandler
key_verification_final_prompt, key_verification_final_prompt,
trace_route_menu, trace_route_menu,
throttle_message, throttle_message,
message_response_menu,
message_viewmode_menu,
reply_menu,
delete_messages_menu,
node_name_length_menu, node_name_length_menu,
FrameToggles, FrameToggles,
DisplayUnits DisplayUnits
@@ -66,9 +61,6 @@ class menuHandler
static void TwelveHourPicker(); static void TwelveHourPicker();
static void ClockFacePicker(); static void ClockFacePicker();
static void messageResponseMenu(); static void messageResponseMenu();
static void messageViewModeMenu();
static void replyMenu();
static void deleteMessagesMenu();
static void homeBaseMenu(); static void homeBaseMenu();
static void textMessageBaseMenu(); static void textMessageBaseMenu();
static void systemBaseMenu(); static void systemBaseMenu();
@@ -108,4 +100,4 @@ class menuHandler
}; };
} // namespace graphics } // namespace graphics
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,7 @@
#pragma once #pragma once
#include "MessageStore.h" // for StoredMessage
#if HAS_SCREEN
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include "mesh/generated/meshtastic/mesh.pb.h" // for meshtastic_MeshPacket
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -14,27 +10,6 @@ namespace graphics
namespace MessageRenderer namespace MessageRenderer
{ {
// Thread filter modes
enum class ThreadMode { ALL, CHANNEL, DIRECT };
// Setter for switching thread mode
void setThreadMode(ThreadMode mode, int channel = -1, uint32_t peer = 0);
// Getter for current mode
ThreadMode getThreadMode();
// Getter for current channel (valid if mode == CHANNEL)
int getThreadChannel();
// Getter for current peer (valid if mode == DIRECT)
uint32_t getThreadPeer();
// Registry accessors for menuHandler
const std::vector<int> &getSeenChannels();
const std::vector<uint32_t> &getSeenPeers();
void clearThreadRegistries();
// Text and emote rendering // Text and emote rendering
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
@@ -45,27 +20,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
// Function to calculate heights for each line // Function to calculate heights for each line
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes, std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
const std::vector<bool> &isHeaderVec);
// Reset scroll state when new messages arrive // Function to render the message content
void resetScrollState(); void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
// Manual scroll control for encoder-style inputs
void nudgeScroll(int8_t direction);
// Helper to auto-select the correct thread mode from a message
void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Handles a new incoming/outgoing message: banner, wake, thread select, scroll reset
void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const meshtastic_MeshPacket &packet);
// Clear Message Line Cache from Message Renderer
void clearMessageCache();
void scrollUp();
void scrollDown();
} // namespace MessageRenderer } // namespace MessageRenderer
} // namespace graphics } // namespace graphics
#endif

View File

@@ -46,99 +46,69 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
} }
// Static variables for dynamic cycling // Static variables for dynamic cycling
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD; static NodeListMode currentMode = MODE_LAST_HEARD;
static ListMode_Location currentMode_Location = MODE_DISTANCE;
static int scrollIndex = 0; static int scrollIndex = 0;
// Popup overlay state
static uint32_t popupTime = 0;
static int popupTotal = 0;
static int popupStart = 0;
static int popupEnd = 0;
static int popupPage = 1;
static int popupMaxPage = 1;
static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible
// =============================
// Scrolling Logic
// =============================
void scrollUp()
{
if (scrollIndex > 0)
scrollIndex--;
popupTime = millis(); // show popup
}
void scrollDown()
{
scrollIndex++;
popupTime = millis();
}
// ============================= // =============================
// Utility Functions // Utility Functions
// ============================= // =============================
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
{ {
static char nodeName[25]; // single static buffer we return const char *name = NULL;
nodeName[0] = '\0'; static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) {
auto writeFallbackId = [&] { if (node->has_user && strlen(node->user.long_name) > 0) {
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0)); name = node->user.long_name;
}; } else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
// 1) Choose target candidate (long vs short) only if present }
const char *raw = nullptr;
if (node && node->has_user) {
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
}
// 2) Sanitize (empty if raw is null/empty)
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
writeFallbackId();
} else { } else {
// %.*s ensures null-termination and safe truncation to buffer size - 1 if (node->has_user && strlen(node->user.short_name) > 0) {
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str()); name = node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
} }
// 4) Width-based truncation + ellipsis (long-name mode only) // Use sanitizeString() function and copy directly into nodeName
if (config.display.use_long_node_name && display) { std::string sanitized_name = sanitizeString(name ? name : "");
int availWidth = columnWidth - (isHighResolution ? 65 : 38);
if (!sanitized_name.empty()) {
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
if (config.display.use_long_node_name == true) {
int availWidth = (SCREEN_WIDTH / 2) - 65;
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
const size_t beforeLen = std::strlen(nodeName); size_t origLen = strlen(nodeName);
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
// Trim from the end until it fits or is empty nodeName[strlen(nodeName) - 1] = '\0';
size_t len = beforeLen;
while (len && display->getStringWidth(nodeName) > availWidth) {
nodeName[--len] = '\0';
} }
// If truncated, append "..." (respect buffer size) // If we actually truncated, append "..." (ensure space remains in buffer)
if (len < beforeLen) { if (strlen(nodeName) < origLen) {
// Make sure there's room for "..." and '\0' size_t len = strlen(nodeName);
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0' size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
const size_t needed = 3; // "..." if (len > maxLen) {
if (len > capForText - needed) { nodeName[maxLen] = '\0';
len = capForText - needed; len = maxLen;
nodeName[len] = '\0';
} }
std::strcat(nodeName, "..."); strcat(nodeName, "...");
} }
} }
return nodeName; return nodeName;
} }
const char *getCurrentModeTitle_Nodes(int screenWidth) const char *getCurrentModeTitle(int screenWidth)
{ {
switch (currentMode_Nodes) { switch (currentMode) {
case MODE_LAST_HEARD: case MODE_LAST_HEARD:
return "Last Heard"; return "Last Heard";
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
@@ -147,18 +117,8 @@ const char *getCurrentModeTitle_Nodes(int screenWidth)
#else #else
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
#endif #endif
default:
return "Nodes";
}
}
const char *getCurrentModeTitle_Location(int screenWidth)
{
switch (currentMode_Location) {
case MODE_DISTANCE: case MODE_DISTANCE:
return "Distance"; return "Distance";
case MODE_BEARING:
return "Bearings";
default: default:
return "Nodes"; return "Nodes";
} }
@@ -177,8 +137,10 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{ {
int columnWidth = display->getWidth() / 2;
int separatorX = x + columnWidth - 2;
for (int y = yStart; y <= yEnd; y += 2) { for (int y = yStart; y <= yEnd; y += 2) {
display->setPixel(x, y); display->setPixel(separatorX, y);
} }
} }
@@ -190,8 +152,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
int scrollbarX = display->getWidth() - 2; int scrollbarX = display->getWidth() - 2;
int scrollbarHeight = display->getHeight() - scrollStartY - 10; int scrollbarHeight = display->getHeight() - scrollStartY - 10;
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
int perPage = visibleNodeRows * columns; int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
for (int i = 0; i < thumbHeight; i++) { for (int i = 0; i < thumbHeight; i++) {
@@ -208,7 +169,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
char timeStr[10]; char timeStr[10];
uint32_t seconds = sinceLastSeen(node); uint32_t seconds = sinceLastSeen(node);
@@ -253,7 +214,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -297,7 +258,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -368,15 +329,18 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
} }
} }
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
switch (currentMode_Nodes) { switch (currentMode) {
case MODE_LAST_HEARD: case MODE_LAST_HEARD:
drawEntryLastHeard(display, node, x, y, columnWidth); drawEntryLastHeard(display, node, x, y, columnWidth);
break; break;
case MODE_HOP_SIGNAL: case MODE_HOP_SIGNAL:
drawEntryHopSignal(display, node, x, y, columnWidth); drawEntryHopSignal(display, node, x, y, columnWidth);
break; break;
case MODE_DISTANCE:
drawNodeDistance(display, node, x, y, columnWidth);
break;
default: default:
break; break;
} }
@@ -389,7 +353,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
// Adjust max text width depending on column and screen width // Adjust max text width depending on column and screen width
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const char *nodeName = getSafeNodeName(display, node, columnWidth); const char *nodeName = getSafeNodeName(display, node);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -467,6 +431,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
locationScreen = true; locationScreen = true;
else if (strcmp(title, "Distance") == 0) else if (strcmp(title, "Distance") == 0)
locationScreen = true; locationScreen = true;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
int columnWidth = display->getWidth() / 2;
#endif
display->clear(); display->clear();
// Draw the battery/time header // Draw the battery/time header
@@ -475,74 +444,39 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Space below header // Space below header
y += COMMON_HEADER_HEIGHT; y += COMMON_HEADER_HEIGHT;
int totalColumns = 1; // Default to 1 column
if (config.display.use_long_node_name) {
if (SCREEN_WIDTH <= 240) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 240) {
totalColumns = 2;
}
} else {
if (SCREEN_WIDTH <= 64) {
totalColumns = 1;
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
totalColumns = 2;
} else {
totalColumns = 3;
}
}
int columnWidth = display->getWidth() / totalColumns;
int totalEntries = nodeDB->getNumMeshNodes(); int totalEntries = nodeDB->getNumMeshNodes();
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int numskipped = 0; int numskipped = 0;
int visibleNodeRows = totalRowsAvailable; int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
// Build filtered + ordered list int totalColumns = 1;
std::vector<int> drawList; #else
drawList.reserve(totalEntries); int totalColumns = 2;
for (int i = 0; i < totalEntries; i++) { #endif
auto *n = nodeDB->getMeshNodeByIndex(i);
if (!n)
continue;
if (n->num == nodeDB->getNodeNum())
continue;
if (locationScreen && !n->has_position)
continue;
drawList.push_back(n->num);
}
totalEntries = drawList.size();
int perPage = visibleNodeRows * totalColumns;
int maxScroll = 0;
if (perPage > 0) {
maxScroll = std::max(0, (totalEntries - 1) / perPage);
}
if (scrollIndex > maxScroll)
scrollIndex = maxScroll;
int startIndex = scrollIndex * visibleNodeRows * totalColumns; int startIndex = scrollIndex * visibleNodeRows * totalColumns;
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
startIndex++; // skip own node
}
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
int yOffset = 0; int yOffset = 0;
int col = 0; int col = 0;
int lastNodeY = y; int lastNodeY = y;
int shownCount = 0; int shownCount = 0;
int rowCount = 0; int rowCount = 0;
for (int idx = startIndex; idx < endIndex; idx++) { for (int i = startIndex; i < endIndex; ++i) {
uint32_t nodeNum = drawList[idx]; if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
auto *node = nodeDB->getMeshNode(nodeNum); numskipped++;
continue;
}
int xPos = x + (col * columnWidth); int xPos = x + (col * columnWidth);
int yPos = y + yOffset; int yPos = y + yOffset;
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
renderer(display, node, xPos, yPos, columnWidth); if (extras) {
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
if (extras) }
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
yOffset += rowYOffset; yOffset += rowYOffset;
@@ -565,71 +499,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// Draw column separator // Draw column separator
if (shownCount > 0) { if (shownCount > 0) {
const int firstNodeY = y + 3; const int firstNodeY = y + 3;
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) { drawColumnSeparator(display, x, firstNodeY, lastNodeY);
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
}
} }
#endif #endif
const int scrollStartY = y + 3; const int scrollStartY = y + 3;
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
graphics::drawCommonFooter(display, x, y); graphics::drawCommonFooter(display, x, y);
// Scroll Popup Overlay
if (millis() - popupTime < POPUP_DURATION_MS) {
popupTotal = totalEntries;
int perPage = visibleNodeRows * totalColumns;
popupStart = startIndex + 1;
popupEnd = std::min(startIndex + perPage, totalEntries);
popupPage = (scrollIndex + 1);
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
char buf[32];
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
display->setTextAlignment(TEXT_ALIGN_LEFT);
// Box padding
int padding = 2;
int textW = display->getStringWidth(buf);
int textH = FONT_HEIGHT_SMALL;
int boxWidth = textW + padding * 3;
int boxHeight = textH + padding * 2;
// Center of usable screen area:
int headerHeight = FONT_HEIGHT_SMALL - 1;
int footerHeight = FONT_HEIGHT_SMALL + 2;
int usableTop = headerHeight;
int usableBottom = display->getHeight() - footerHeight;
int usableHeight = usableBottom - usableTop;
// Center point inside usable area
int boxLeft = (display->getWidth() - boxWidth) / 2;
int boxTop = usableTop + (usableHeight - boxHeight) / 2;
// Draw Box
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
// Text
display->drawString(boxLeft + padding, boxTop + padding, buf);
}
} }
// ============================= // =============================
@@ -637,11 +513,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
// ============================= // =============================
#ifndef USE_EINK #ifndef USE_EINK
// Node list for Last Heard and Hop Signal views void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
// Static variables to track mode and duration // Static variables to track mode and duration
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE; static NodeListMode lastRenderedMode = MODE_COUNT;
static unsigned long modeStartTime = 0; static unsigned long modeStartTime = 0;
unsigned long now = millis(); unsigned long now = millis();
@@ -654,65 +529,23 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state
} }
#endif #endif
// On very first call (on boot or state enter) // On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_NODE) { if (lastRenderedMode == MODE_COUNT) {
currentMode_Nodes = MODE_LAST_HEARD; currentMode = MODE_LAST_HEARD;
modeStartTime = now; modeStartTime = now;
} }
// Time to switch to next mode? // Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) { if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE); currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
modeStartTime = now; modeStartTime = now;
} }
// Render screen based on currentMode // Render screen based on currentMode
const char *title = getCurrentModeTitle_Nodes(display->getWidth()); const char *title = getCurrentModeTitle(display->getWidth());
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
// Track the last mode to avoid reinitializing modeStartTime // Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Nodes; lastRenderedMode = currentMode;
}
// Node list for Distance and Bearings views
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
// Static variables to track mode and duration
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
static unsigned long modeStartTime = 0;
unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
lastSwitchTime = now;
}
#endif
// On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT_LOCATION) {
currentMode_Location = MODE_DISTANCE;
modeStartTime = now;
}
// Time to switch to next mode?
if (now - modeStartTime >= getModeCycleIntervalMs()) {
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
modeStartTime = now;
}
// Render screen based on currentMode
const char *title = getCurrentModeTitle_Location(display->getWidth());
// Render screen based on currentMode_Location
if (currentMode_Location == MODE_DISTANCE) {
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} else if (currentMode_Location == MODE_BEARING) {
drawNodeListWithCompasses(display, state, x, y);
}
// Track the last mode to avoid reinitializing modeStartTime
lastRenderedMode = currentMode_Location;
} }
#endif #endif
@@ -733,12 +566,14 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
#endif #endif
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
} }
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
const char *title = "Distance"; const char *title = "Distance";
drawNodeListScreen(display, state, x, y, title, drawNodeDistance); drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
} }
#endif #endif
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
float heading = 0; float heading = 0;

View File

@@ -23,11 +23,8 @@ namespace NodeListRenderer
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
// Node list mode enumeration for Last Heard and Hop Signal views // Node list mode enumeration
enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE = 2 }; enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
// Node list mode enumeration for Distance and Bearings views
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
// Main node list screen function // Main node list screen function
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
@@ -38,7 +35,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
// Extras renderers // Extras renderers
@@ -49,20 +46,14 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Utility functions // Utility functions
const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth); const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
const char *getSafeNodeName(meshtastic_NodeInfoLite *node, int columnWidth);
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
// Scrolling controls
void scrollUp();
void scrollDown();
// Bitmap drawing function // Bitmap drawing function
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);

View File

@@ -1,6 +1,6 @@
#include "configuration.h" #include "configuration.h"
#if HAS_SCREEN #if HAS_SCREEN
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NotificationRenderer.h" #include "NotificationRenderer.h"
@@ -38,7 +38,7 @@ extern bool hasUnreadMessage;
namespace graphics namespace graphics
{ {
int bannerSignalBars = -1;
InputEvent NotificationRenderer::inEvent; InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0; int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0}; char NotificationRenderer::alertBannerMessage[256] = {0};
@@ -85,13 +85,9 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
void NotificationRenderer::resetBanner() void NotificationRenderer::resetBanner()
{ {
notificationTypeEnum previousType = current_notification_type;
alertBannerMessage[0] = '\0'; alertBannerMessage[0] = '\0';
current_notification_type = notificationTypeEnum::none; current_notification_type = notificationTypeEnum::none;
OnScreenKeyboardModule::instance().clearPopup();
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0; inEvent.kbchar = 0;
curSelected = 0; curSelected = 0;
@@ -104,13 +100,6 @@ void NotificationRenderer::resetBanner()
currentNumber = 0; currentNumber = 0;
nodeDB->pause_sort(false); nodeDB->pause_sort(false);
// If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
// to ensure any messages received during keyboard use are now displayed
if (previousType == notificationTypeEnum::text_input && screen) {
OnScreenKeyboardModule::instance().stop(false);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
} }
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
@@ -174,15 +163,13 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
// modulo to extract // modulo to extract
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1)); uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
// Handle input // Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
if (this_digit == 9) { if (this_digit == 9) {
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1)); currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
} else { } else {
currentNumber += (pow_of_10(numDigits - curSelected - 1)); currentNumber += (pow_of_10(numDigits - curSelected - 1));
} }
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
if (this_digit == 0) { if (this_digit == 0) {
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1)); currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
} else { } else {
@@ -264,10 +251,10 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
// Handle input // Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum); alertBannerCallback(selectedNodenum);
@@ -381,10 +368,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input // Handle input
if (alertBannerOptions > 0) { if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS || inEvent.inputEvent == INPUT_BROKER_UP_LONG) { inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS || inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) { if (optionsEnumPtr != nullptr) {
@@ -477,7 +464,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
bool is_picker = false; bool is_picker = false;
uint16_t lineCount = 0; uint16_t lineCount = 0;
// Layout Configuration // === Layout Configuration ===
constexpr uint16_t hPadding = 5; constexpr uint16_t hPadding = 5;
constexpr uint16_t vPadding = 2; constexpr uint16_t vPadding = 2;
bool needs_bell = false; bool needs_bell = false;
@@ -491,32 +478,13 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
// Track widest line INCLUDING bars (but don't change per-line widths)
uint16_t widestLineWithBars = 0;
while (lines[lineCount] != nullptr) { while (lines[lineCount] != nullptr) {
auto newlinePointer = strchr(lines[lineCount], '\n'); auto newlinePointer = strchr(lines[lineCount], '\n');
if (newlinePointer) if (newlinePointer)
lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first lineLengths[lineCount] = (newlinePointer - lines[lineCount]); // Check for newlines first
else // if the newline wasn't found, then pull string length from strlen else // if the newline wasn't found, then pull string length from strlen
lineLengths[lineCount] = strlen(lines[lineCount]); lineLengths[lineCount] = strlen(lines[lineCount]);
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true); lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
// Consider extra width for signal bars on lines that contain "Signal:"
uint16_t potentialWidth = lineWidths[lineCount];
if (graphics::bannerSignalBars >= 0 && strncmp(lines[lineCount], "Signal:", 7) == 0) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int gap = 6; // space between text and bars
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
potentialWidth += barsWidth;
}
if (potentialWidth > widestLineWithBars)
widestLineWithBars = potentialWidth;
if (!is_picker) { if (!is_picker) {
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr); needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
if (lineWidths[lineCount] > maxWidth) if (lineWidths[lineCount] > maxWidth)
@@ -526,12 +494,136 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
} }
// count lines // count lines
// Ensure box accounts for signal bars if present
if (widestLineWithBars > maxWidth)
maxWidth = widestLineWithBars;
uint16_t boxWidth = hPadding * 2 + maxWidth; uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1)
boxHeight += (isHighResolution ? 4 : 3);
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines)
boxWidth += (isHighResolution ? 4 : 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
if (visibleTotalLines == 1) {
boxTop += 25;
}
if (alertBannerOptions < 3) {
int missingLines = 3 - alertBannerOptions;
int moveUp = missingLines * (effectiveLineHeight / 2);
boxTop -= moveUp;
if (boxTop < 0)
boxTop = 0;
}
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
int16_t lineY = boxTop + vPadding;
int swingRange = 8;
static int swingOffset = 0;
static bool swingRight = true;
static unsigned long lastSwingTime = 0;
unsigned long now = millis();
int swingSpeedMs = 10 / (swingRange * 2);
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
lastSwingTime = now;
if (swingRight) {
swingOffset++;
if (swingOffset >= swingRange)
swingRight = false;
} else {
swingOffset--;
if (swingOffset <= 0)
swingRight = true;
}
}
for (int i = 0; i < lineCount; i++) {
bool isTitle = (i == 0);
int globalOptionIndex = (i - 1) + firstOptionToShow;
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
uint16_t visibleWidth = 64 - hPadding * 2;
if (totalLines > visibleTotalLines)
visibleWidth -= 6;
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
if (isTitle) {
if (visibleTotalLines == 1) {
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
} else {
display->setColor(WHITE);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
display->setColor(WHITE);
if (needs_bell) {
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
}
}
lineY = boxTop + effectiveLineHeight + 1;
} else if (isSelectedOption) {
display->setColor(WHITE);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
int textX = boxLeft + hPadding + swingOffset;
display->drawString(textX, lineY - 1, lineBuffer);
} else {
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
}
display->setColor(WHITE);
lineY += effectiveLineHeight;
} else {
display->setColor(BLACK);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
lineY += effectiveLineHeight;
}
}
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#else
if (needs_bell) { if (needs_bell) {
if (isHighResolution && boxWidth <= 150) if (isHighResolution && boxWidth <= 150)
boxWidth += 26; boxWidth += 26;
@@ -553,21 +645,8 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
boxWidth += (isHighResolution) ? 4 : 2; boxWidth += (isHighResolution) ? 4 : 2;
} }
int16_t boxTop = (display->height() / 2) - (boxHeight / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
boxHeight += (isHighResolution) ? 2 : 1;
#if defined(M5STACK_UNITC6L)
if (visibleTotalLines == 1) {
boxTop += 25;
}
if (alertBannerOptions < 3) {
int missingLines = 3 - alertBannerOptions;
int moveUp = missingLines * (effectiveLineHeight / 2);
boxTop -= moveUp;
if (boxTop < 0)
boxTop = 0;
}
#endif
// Draw Box // === Draw Box ===
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
@@ -583,7 +662,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE); display->setColor(WHITE);
// Draw Content // === Draw Content ===
int16_t lineY = boxTop + vPadding; int16_t lineY = boxTop + vPadding;
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
@@ -612,47 +691,17 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
lineY += (effectiveLineHeight - 2 - background_yOffset); lineY += (effectiveLineHeight - 2 - background_yOffset);
} else { } else {
// Pop-up // Pop-up
// If this is the Signal line, center text + bars as one group display->drawString(textX, lineY, lineBuffer);
bool isSignalLine = (graphics::bannerSignalBars >= 0 && strstr(lineBuffer, "Signal:") != nullptr);
if (isSignalLine) {
const int totalBars = 5;
const int barWidth = 3;
const int barSpacing = 2;
const int barHeightStep = 2;
const int gap = 6;
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
int totalWidth = textWidth + barsWidth;
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
display->drawString(groupStartX, lineY, lineBuffer);
int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1;
for (int b = 0; b < totalBars; b++) {
int barHeight = (b + 1) * barHeightStep;
int x = baseX + b * (barWidth + barSpacing);
int y = baseY - barHeight;
if (b < graphics::bannerSignalBars) {
display->fillRect(x, y, barWidth, barHeight);
} else {
display->drawRect(x, y, barWidth, barHeight);
}
}
} else {
display->drawString(textX, lineY, lineBuffer);
}
lineY += (effectiveLineHeight); lineY += (effectiveLineHeight);
} }
} }
// Scroll Bar (Thicker, inside box, not over title) // === Scroll Bar (Thicker, inside box, not over title) ===
if (totalLines > visibleTotalLines) { if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5; const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines; float ratio = (float)visibleTotalLines / totalLines;
@@ -663,6 +712,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
} }
#endif
} }
/// Draw the last text message we received /// Draw the last text message we received
@@ -719,8 +769,40 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
} }
if (inEvent.inputEvent != INPUT_BROKER_NONE) { if (inEvent.inputEvent != INPUT_BROKER_NONE) {
bool handled = OnScreenKeyboardModule::processVirtualKeyboardInput(inEvent, virtualKeyboard); if (inEvent.inputEvent == INPUT_BROKER_UP) {
if (!handled && inEvent.inputEvent == INPUT_BROKER_CANCEL) { // high frequency for move cursor left/right than up/down with encoders
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorLeft();
} else {
virtualKeyboard->moveCursorUp();
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorRight();
} else {
virtualKeyboard->moveCursorDown();
}
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
virtualKeyboard->moveCursorUp();
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
virtualKeyboard->moveCursorDown();
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
virtualKeyboard->handlePress();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
virtualKeyboard->handleLongPress();
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback; auto callback = textInputCallback;
delete virtualKeyboard; delete virtualKeyboard;
virtualKeyboard = nullptr; virtualKeyboard = nullptr;
@@ -739,28 +821,12 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
} }
// Re-check pointer before drawing to avoid use-after-free and crashes
if (!virtualKeyboard) {
// Ensure we exit text_input state and restore frames
if (current_notification_type == notificationTypeEnum::text_input) {
resetBanner();
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
// If screen is null, do nothing (safe fallback)
return;
}
// Clear the screen to avoid overlapping with underlying frames or overlays // Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK); display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE); display->setColor(WHITE);
// Draw the virtual keyboard // Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0); virtualKeyboard->draw(display, 0, 0);
// Draw transient popup overlay (if any) managed by OnScreenKeyboardModule
OnScreenKeyboardModule::instance().drawPopupOverlay(display);
} else { } else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck // If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner"); LOG_INFO("Virtual keyboard is null - resetting banner");
@@ -773,12 +839,5 @@ bool NotificationRenderer::isOverlayBannerShowing()
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
} }
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
{
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
return;
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
}
} // namespace graphics } // namespace graphics
#endif #endif

View File

@@ -4,7 +4,6 @@
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h" #include "graphics/VirtualKeyboard.h"
#include "modules/OnScreenKeyboardModule.h"
#include <functional> #include <functional>
#include <string> #include <string>
#define MAX_LINES 5 #define MAX_LINES 5
@@ -32,7 +31,6 @@ class NotificationRenderer
static bool pauseBanner; static bool pauseBanner;
static void resetBanner(); static void resetBanner();
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);

View File

@@ -6,7 +6,10 @@
#include "NodeListRenderer.h" #include "NodeListRenderer.h"
#include "UIRenderer.h" #include "UIRenderer.h"
#include "airtime.h" #include "airtime.h"
#include "configuration.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h" #include "graphics/TimeFormatters.h"
#include "graphics/images.h" #include "graphics/images.h"
@@ -26,16 +29,6 @@ namespace graphics
NodeNum UIRenderer::currentFavoriteNodeNum = 0; NodeNum UIRenderer::currentFavoriteNodeNum = 0;
std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes; std::vector<meshtastic_NodeInfoLite *> graphics::UIRenderer::favoritedNodes;
static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
{
int yOffset = (isHighResolution) ? -5 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite);
}
}
void graphics::UIRenderer::rebuildFavoritedNodes() void graphics::UIRenderer::rebuildFavoritedNodes()
{ {
favoritedNodes.clear(); favoritedNodes.clear();
@@ -251,21 +244,20 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
// Draw nodes status // Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, const char *additional_words) bool show_total, String additional_words)
{ {
char usersString[20]; char usersString[20];
int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words); snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str());
if (show_total) { if (show_total) {
int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0;
snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words); snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str());
} }
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) { if (isHighResolution) {
@@ -607,7 +599,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
} }
drawSatelliteIcon(display, x, getTextPositions(display)[line]); int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0; int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else { } else {
@@ -759,7 +758,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// Start Functions to write date/time to the screen // Start Functions to write date/time to the screen
// Helper function to check if a year is a leap year // Helper function to check if a year is a leap year
constexpr bool isLeapYear(int year) bool isLeapYear(int year)
{ {
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
} }
@@ -990,7 +989,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else { } else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
} }
drawSatelliteIcon(display, x, getTextPositions(display)[line]); int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0; int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else { } else {

View File

@@ -34,7 +34,7 @@ class UIRenderer
public: public:
// Common UI elements // Common UI elements
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
int node_offset = 0, bool show_total = true, const char *additional_words = ""); int node_offset = 0, bool show_total = true, String additional_words = "");
// GPS status functions // GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
@@ -43,6 +43,9 @@ class UIRenderer
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
// Layout and utility functions
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
// Overlay and special screens // Overlay and special screens
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
@@ -80,6 +83,8 @@ class UIRenderer
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
// Message filtering
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
// Check if the display can render a string (detect special chars; emoji) // Check if the display can render a string (detect special chars; emoji)
static bool haveGlyphs(const char *str); static bool haveGlyphs(const char *str);
}; // namespace UIRenderer }; // namespace UIRenderer

View File

@@ -28,7 +28,7 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \ ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
@@ -304,6 +304,58 @@ const uint8_t chirpy[] = {
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
#define chirpy_width_hirez 76
#define chirpy_height_hirez 100
const uint8_t chirpy_hirez[] = {
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
#define chirpy_small_image_width 8 #define chirpy_small_image_width 8
#define chirpy_small_image_height 8 #define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};

View File

@@ -124,7 +124,7 @@ uint32_t InkHUD::AppletFont::toUtf32(std::string utf8)
utf32 |= (utf8.at(3) & 0b00111111); utf32 |= (utf8.at(3) & 0b00111111);
break; break;
default: default:
return 0; assert(false);
} }
return utf32; return utf32;

View File

@@ -1,217 +0,0 @@
#if defined(HACKADAY_COMMUNICATOR)
#include "HackadayCommunicatorKeyboard.h"
#include "main.h"
#define _TCA8418_COLS 10
#define _TCA8418_ROWS 8
#define _TCA8418_NUM_KEYS 80
#define _TCA8418_MULTI_TAP_THRESHOLD 1500
using Key = TCA8418KeyboardBase::TCA8418Key;
constexpr uint8_t modifierRightShiftKey = 30;
constexpr uint8_t modifierRightShift = 0b0001;
constexpr uint8_t modifierLeftShiftKey = 76; // keynum -1
constexpr uint8_t modifierLeftShift = 0b0001;
// constexpr uint8_t modifierSymKey = 42;
// constexpr uint8_t modifierSym = 0b0010;
// Num chars per key, Modulus for rotating through characters
static uint8_t HackadayCommunicatorTapMod[_TCA8418_NUM_KEYS] = {
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2,
0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 2, 2, 2, 1, 2, 2, 0, 0, 0, 2, 1, 2, 2, 0, 1, 1, 0,
};
static unsigned char HackadayCommunicatorTapMap[_TCA8418_NUM_KEYS][2] = {{},
{},
{'+'},
{'9'},
{'8'},
{'7'},
{'2'},
{'3'},
{'4'},
{'5'},
{Key::ESC},
{'q', 'Q'},
{'w', 'W'},
{'e', 'E'},
{'r', 'R'},
{'t', 'T'},
{'y', 'Y'},
{'u', 'U'},
{'i', 'I'},
{'o', 'O'},
{Key::TAB},
{'a', 'A'},
{'s', 'S'},
{'d', 'D'},
{'f', 'F'},
{'g', 'G'},
{'h', 'H'},
{'j', 'J'},
{'k', 'K'},
{'l', 'L'},
{},
{'z', 'Z'},
{'x', 'X'},
{'c', 'C'},
{'v', 'V'},
{'b', 'B'},
{'n', 'N'},
{'m', 'M'},
{',', '<'},
{'.', '>'},
{},
{},
{},
{'\\'},
{' '},
{},
{Key::RIGHT},
{Key::DOWN},
{Key::LEFT},
{},
{},
{},
{'-'},
{'6', '^'},
{'5', '%'},
{'4', '$'},
{'[', '{'},
{']', '}'},
{'p', 'P'},
{},
{},
{},
{'*'},
{'3', '#'},
{'2', '@'},
{'1', '!'},
{Key::SELECT},
{'\'', '"'},
{';', ':'},
{},
{},
{},
{'/', '?'},
{'='},
{'.', '>'},
{'0', ')'},
{},
{Key::UP},
{Key::BSP},
{}};
HackadayCommunicatorKeyboard::HackadayCommunicatorKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
void HackadayCommunicatorKeyboard::reset(void)
{
TCA8418KeyboardBase::reset();
enableInterrupts();
}
// handle multi-key presses (shift and alt)
void HackadayCommunicatorKeyboard::trigger()
{
uint8_t count = keyCount();
if (count == 0)
return;
for (uint8_t i = 0; i < count; ++i) {
uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
uint8_t key = k & 0x7F;
if (k & 0x80) {
pressed(key);
} else {
released();
state = Idle;
}
}
}
void HackadayCommunicatorKeyboard::pressed(uint8_t key)
{
if (state == Init || state == Busy) {
return;
}
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
return; // Invalid key
}
next_key = row * _TCA8418_COLS + col;
state = Held;
uint32_t now = millis();
tap_interval = now - last_tap;
updateModifierFlag(next_key);
if (isModifierKey(next_key)) {
last_modifier_time = now;
}
if (tap_interval < 0) {
last_tap = 0;
state = Busy;
return;
}
if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
char_idx = 0;
} else {
char_idx += 1;
}
last_key = next_key;
last_tap = now;
}
void HackadayCommunicatorKeyboard::released()
{
if (state != Held) {
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
state = Idle;
return;
}
uint32_t now = millis();
last_tap = now;
if (HackadayCommunicatorTapMod[last_key])
queueEvent(HackadayCommunicatorTapMap[last_key][modifierFlag % HackadayCommunicatorTapMod[last_key]]);
if (isModifierKey(last_key) == false)
modifierFlag = 0;
}
void HackadayCommunicatorKeyboard::updateModifierFlag(uint8_t key)
{
if (key == modifierRightShiftKey) {
modifierFlag ^= modifierRightShift;
} else if (key == modifierLeftShiftKey) {
modifierFlag ^= modifierLeftShift;
}
}
bool HackadayCommunicatorKeyboard::isModifierKey(uint8_t key)
{
return (key == modifierRightShiftKey || key == modifierLeftShiftKey);
}
#endif

View File

@@ -1,26 +0,0 @@
#include "TCA8418KeyboardBase.h"
class HackadayCommunicatorKeyboard : public TCA8418KeyboardBase
{
public:
HackadayCommunicatorKeyboard();
void reset(void);
void trigger(void) override;
virtual ~HackadayCommunicatorKeyboard() {}
protected:
void pressed(uint8_t key) override;
void released(void) override;
void updateModifierFlag(uint8_t key);
bool isModifierKey(uint8_t key);
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
};

View File

@@ -3,12 +3,6 @@
#include "Observer.h" #include "Observer.h"
#include "freertosinc.h" #include "freertosinc.h"
#ifdef InputBrokerDebug
#define LOG_INPUT(...) LOG_DEBUG(__VA_ARGS__)
#else
#define LOG_INPUT(...)
#endif
enum input_broker_event { enum input_broker_event {
INPUT_BROKER_NONE = 0, INPUT_BROKER_NONE = 0,
INPUT_BROKER_SELECT = 10, INPUT_BROKER_SELECT = 10,
@@ -53,7 +47,6 @@ typedef struct _InputEvent {
class InputPollable class InputPollable
{ {
public: public:
virtual ~InputPollable() = default;
virtual void pollOnce() = 0; virtual void pollOnce() = 0;
}; };

View File

@@ -3,9 +3,6 @@
#include "RotaryEncoderImpl.h" #include "RotaryEncoderImpl.h"
#include "InputBroker.h" #include "InputBroker.h"
#include "RotaryEncoder.h" #include "RotaryEncoder.h"
#ifdef ARCH_ESP32
#include "sleep.h"
#endif
#define ORIGIN_NAME "RotaryEncoder" #define ORIGIN_NAME "RotaryEncoder"
@@ -14,20 +11,6 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl() RotaryEncoderImpl::RotaryEncoderImpl()
{ {
rotary = nullptr; 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() bool RotaryEncoderImpl::init()
@@ -42,22 +25,15 @@ bool RotaryEncoderImpl::init()
eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw); eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press); eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
if (rotary == nullptr) { rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press);
moduleConfig.canned_message.inputbroker_pin_press); rotary->resetButton();
}
attachRotaryEncoderInterrupts(); interruptInstance = this;
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
#ifdef ARCH_ESP32 attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
// Register callbacks for before and after lightsleep attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
// Used to detach and reattach interrupts attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
if (isFirstInit) {
lsObserver.observe(&notifyLightSleep);
lsEndObserver.observe(&notifyLightSleepEnd);
isFirstInit = false;
}
#endif
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, 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, moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
@@ -95,50 +71,6 @@ 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; RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
#endif #endif

View File

@@ -8,18 +8,12 @@
class RotaryEncoder; class RotaryEncoder;
class RotaryEncoderImpl final : public InputPollable class RotaryEncoderImpl : public InputPollable
{ {
public: public:
RotaryEncoderImpl(); RotaryEncoderImpl();
~RotaryEncoderImpl() override; bool init(void);
bool init();
virtual void pollOnce() override; 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: protected:
static RotaryEncoderImpl *interruptInstance; static RotaryEncoderImpl *interruptInstance;
@@ -29,21 +23,6 @@ class RotaryEncoderImpl final : public InputPollable
input_broker_event eventPressed = INPUT_BROKER_NONE; input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary; RotaryEncoder *rotary;
private:
#ifdef ARCH_ESP32
bool isFirstInit;
#endif
void detachRotaryEncoderInterrupts();
void attachRotaryEncoderInterrupts();
#ifdef ARCH_ESP32
// Get notified when lightsleep begins and ends
CallbackObserver<RotaryEncoderImpl, void *> lsObserver =
CallbackObserver<RotaryEncoderImpl, void *>(this, &RotaryEncoderImpl::beforeLightSleep);
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t> lsEndObserver =
CallbackObserver<RotaryEncoderImpl, esp_sleep_wakeup_cause_t>(this, &RotaryEncoderImpl::afterLightSleep);
#endif
}; };
extern RotaryEncoderImpl *rotaryEncoderImpl; extern RotaryEncoderImpl *rotaryEncoderImpl;

View File

@@ -2,8 +2,6 @@
#include "configuration.h" #include "configuration.h"
#include <Throttle.h> #include <Throttle.h>
SerialKeyboard *globalSerialKeyboard = nullptr;
#ifdef INPUTBROKER_SERIAL_TYPE #ifdef INPUTBROKER_SERIAL_TYPE
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file #define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
@@ -27,8 +25,6 @@ unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w',
SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
{ {
this->_originName = name; this->_originName = name;
globalSerialKeyboard = this;
} }
void SerialKeyboard::erase() void SerialKeyboard::erase()
@@ -89,21 +85,9 @@ int32_t SerialKeyboard::runOnce()
e.source = this->_originName; e.source = this->_originName;
// SELECT OR SEND OR CANCEL EVENT // SELECT OR SEND OR CANCEL EVENT
if (!(shiftRegister2 & (1 << 3))) { if (!(shiftRegister2 & (1 << 3))) {
if (shift > 0) { e.inputEvent = INPUT_BROKER_UP;
e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED
e.kbchar = 0x09; // TAB
shift = 0; // reset shift after TAB
} else {
e.inputEvent = INPUT_BROKER_LEFT;
}
} else if (!(shiftRegister2 & (1 << 2))) { } else if (!(shiftRegister2 & (1 << 2))) {
if (shift > 0) { e.inputEvent = INPUT_BROKER_RIGHT;
e.inputEvent = INPUT_BROKER_ANYKEY; // REQUIRED
e.kbchar = 0x09; // TAB
shift = 0; // reset shift after TAB
} else {
e.inputEvent = INPUT_BROKER_RIGHT;
}
e.kbchar = 0; e.kbchar = 0;
} else if (!(shiftRegister2 & (1 << 1))) { } else if (!(shiftRegister2 & (1 << 1))) {
e.inputEvent = INPUT_BROKER_SELECT; e.inputEvent = INPUT_BROKER_SELECT;

View File

@@ -8,8 +8,6 @@ class SerialKeyboard : public Observable<const InputEvent *>, public concurrency
public: public:
explicit SerialKeyboard(const char *name); explicit SerialKeyboard(const char *name);
uint8_t getShift() const { return shift; }
protected: protected:
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
void erase(); void erase();
@@ -24,6 +22,4 @@ class SerialKeyboard : public Observable<const InputEvent *>, public concurrency
int lastKeyPressed = 13; int lastKeyPressed = 13;
int quickPress = 0; int quickPress = 0;
unsigned long lastPressTime = 0; unsigned long lastPressTime = 0;
}; };
extern SerialKeyboard *globalSerialKeyboard;

Some files were not shown because too many files have changed in this diff Show More