Compare commits

..
Author SHA1 Message Date
Ben Meadors 38c4001f52 Merge branch 'develop' into feature/rak4631-lockdown 2026-03-02 06:40:58 -06:00
niccellular 067d6834c7 fix audit findings from Opus 4.6 specifically looking for race conditions and logic bugs
Three-agent security audit of the LOCKDOWN build identified race conditions,
  incomplete key zeroing, and missing memory hygiene. All findings except HIGH-1
  are addressed here. HIGH-1 (revokeAllAuth not resetting per-instance
  isAdminAuthorized) was reviewed and closed as wontfix: PKC admin key holders
  have persistent access regardless, so the 7-second pre-reboot window changes
  nothing about the threat model.

  HIGH-2: lockNow() previously only zeroed dek[]. Now also zeroes kek[],
  ephemeralKek[], and their derived flags so no key material survives a lock
  event in a RAM dump.

  HIGH-3: isAdminAuthorized (per-instance) and s_localAdminAuthorized (static)
  changed from plain bool to std::atomic<bool>. Both flags are written from the
  NimBLE FreeRTOS task and read from the main loop — non-atomic reads were a
  data race.

  MED-1: readAndDecrypt() and encryptAndWrite() now snapshot dek[] into a stack-
  local dekSnapshot[] on entry before acquiring any lock. A concurrent lockNow()
  that zeros dek[] mid-operation either zeroes the snapshot (HMAC fails securely)
  or has no effect on an already-captured copy. dekSnapshot is zeroed on all exit
  paths.

  MED-2: close() and handleStartConfig() no longer clear s_localAdminAuthorized.
  Resetting it on every disconnect was an auth DoS — any new client connection
  would silently revoke the passphrase-authenticated state, forcing the operator
  to re-enter the passphrase on every reconnect. Device-level auth now persists
  until explicit Lock Now (revokeAllAuth()).

  MED-3: readAndDecrypt() scoped the spiLock LockGuard to a nested block
  covering only the FSCom.open/read/close sequence. The lock is released before
  any HMAC or AES-CTR work. spiLock is a non-recursive binary semaphore;
  holding it across CC310 calls created a re-entry deadlock risk.

  MED-4: AdminModule zeros the passphrase bytes in toRadioScratch via a volatile
  pointer cast before returning from both the Lock Now and provision/unlock
  paths. Prevents the passphrase lingering in the scratch buffer until the next
  ToRadio message overwrites it.

  MED-5: readAndDecrypt() was missing memset(fileBuf) on the hmacData OOM early-
  exit path, leaving ciphertext in the heap until the allocator reuses it.

  MED-6: readAndDecrypt() now sets outLen=0 on entry so callers never observe a
  stale length value on any early failure return.

  MED-7: readAndDecrypt() calls memset(outBuf, 0, ciphertextLen) on AES-CTR
  failure to clear partial plaintext that may have been written to the caller's
  buffer before the error was detected.

  MED-8: Passphrase length limit corrected from 64 to 32 bytes throughout
  (EncryptedStorage.cpp provisionPassphrase/unlockWithPassphrase, AdminModule.cpp
  ppLen range check, EncryptedStorage.h doc comments). The proto private_key field
  is 32 bytes; accepting 33-64 silently failed inside EncryptedStorage while
  appearing to succeed at the AdminModule gate.

  L-1: readAndDecrypt() now rejects files larger than 65536+54 bytes before
  allocating the heap buffer, preventing OOM or integer overflow on corrupt or
  attacker-supplied oversized files.

  L-2: migrateFile() has a precondition comment that spiLock must not be held
  by the caller; both isEncrypted() and encryptAndWrite() acquire it internally.
2026-02-28 11:57:47 -05:00
niccellularandClaude Sonnet 4.6 3f9d76f054 addressing copilot comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 16:35:18 -05:00
niccellular 545ae99ff8 Merge branch 'develop' of https://github.com/meshtastic/firmware into develop 2026-02-27 08:00:19 -05:00
niccellular fd7fc40255 Add rak4631_lockdown hardened build variant
Introduces a new RAK4631 build variant (-DMESHTASTIC_LOCKDOWN=1) with
encrypted flash storage, BLE/USB config access control, and hardened
defaults.

New files:
- variants/nrf52840/rak4631_lockdown/ — PlatformIO env, variant header
- src/security/EncryptedStorage.h/.cpp — AES-128-CTR + HMAC-SHA256
  encrypted proto storage using CC310 hardware, passphrase-gated DEK,
  TTL/boot-count unlock token, backoff on failed attempts
- src/security/APProtect.h/.cpp — UICR APPROTECT (SWD lockout)

Modified files:
- configuration.h — MESHTASTIC_LOCKDOWN flag derives
  PHONEAPI_ACCESS_CONTROL, ENCRYPTED_STORAGE, ENABLE_APPROTECT,
  HARDENED_DEFAULTS (nRF52 only)
- NodeDB.cpp/.h — encrypted loadProto/saveProto, locked-boot early
  return, migration of plaintext files to MENC, hardened defaults,
  saveToDiskNoRetry guard prevents FSCom.format() when storage locked
- PhoneAPI.cpp/.h — per-connection auth reset, redacts channel PSKs
  and security keys for unauthorized clients, revokeAllAuth() for
  Lock Now, post-config TAK_LOCKED/TAK_NEEDS_PROVISION notification
- AdminModule.cpp — passphrase delivery via set_config(security),
  provision/unlock/re-verify flow, Lock Now sentinel (0xFF),
  PKC admin auth callback to PhoneAPI
- PowerFSM.cpp — guard serialEnter() BLE disable on nRF52
2026-02-26 22:15:40 -05:00
240 changed files with 3754 additions and 7778 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ runs:
id: version id: version
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }} name: firmware-${{ inputs.arch }}-${{ inputs.board }}-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
+3 -3
View File
@@ -4,16 +4,16 @@
- Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first
to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback
is appreciated." This will allow other devs to potentially save you time by not accidentally duplicating work etc... is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc...
- Please do not check in files that don't have real changes - Please do not check in files that don't have real changes
- Please do not reformat lines that you didn't have to change the code on - Please do not reformat lines that you didn't have to change the code on
- We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the Linux version), - We recommend using the [Visual Studio Code](https://platformio.org/install/ide?install=vscode) editor along with the ['Trunk Check' extension](https://marketplace.visualstudio.com/items?itemName=trunk.io) (In beta for windows, WSL2 for the linux version),
because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines. because it automatically follows our indentation rules and its auto reformatting will not cause spurious changes to lines.
- If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description. - If your PR fixes a bug, mention "fixes #bugnum" somewhere in your pull request description.
- If your other co-developers have comments on your PR please tweak as needed. - If your other co-developers have comments on your PR please tweak as needed.
- Please also enable "Allow edits by maintainers". - Please also enable "Allow edits by maintainers".
- Please do not submit untested code. - Please do not submit untested code.
- If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and community members can help test your changes. - If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes.
- If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord
## 🤝 Attestations ## 🤝 Attestations
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@v6
with: with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
id: gpg id: gpg
@@ -64,7 +64,7 @@ jobs:
PKG_VERSION: ${{ steps.version.outputs.deb }} PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
overwrite: true overwrite: true
+2 -2
View File
@@ -111,7 +111,7 @@ jobs:
echo "</details>" >> $GITHUB_STEP_SUMMARY echo "</details>" >> $GITHUB_STEP_SUMMARY
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
id: upload-firmware id: upload-firmware
with: with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
@@ -127,7 +127,7 @@ jobs:
release/device-*.bat release/device-*.bat
- name: Store manifests as an artifact - name: Store manifests as an artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
id: upload-manifest id: upload-manifest
with: with:
name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }} name: manifest-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}
+4 -4
View File
@@ -98,7 +98,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-*-* pattern: firmware-*-*
@@ -111,7 +111,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat 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@v7 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }} name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -127,7 +127,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-*-${{ needs.version.outputs.long }} pattern: firmware-*-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -146,7 +146,7 @@ jobs:
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
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true overwrite: true
+1 -1
View File
@@ -35,8 +35,8 @@ jobs:
series: series:
- jammy # 22.04 LTS - jammy # 22.04 LTS
- noble # 24.04 LTS - noble # 24.04 LTS
- plucky # 25.04
- questing # 25.10 - questing # 25.10
- resolute # 26.04 LTS
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
with: with:
ppa_repo: ppa:meshtastic/daily ppa_repo: ppa:meshtastic/daily
+5 -5
View File
@@ -60,16 +60,16 @@ jobs:
- name: Docker login - name: Docker login
if: ${{ inputs.push }} if: ${{ inputs.push }}
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: meshtastic username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Docker setup - name: Docker setup
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- name: Sanitize platform string - name: Sanitize platform string
id: sanitize_platform id: sanitize_platform
@@ -78,7 +78,7 @@ jobs:
- name: Docker tag - name: Docker tag
id: meta id: meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: meshtastic/meshtasticd images: meshtastic/meshtasticd
tags: | tags: |
@@ -86,7 +86,7 @@ jobs:
flavor: latest=false flavor: latest=false
- name: Docker build and push - name: Docker build and push
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
id: docker_variant id: docker_variant
with: with:
context: . context: .
+3 -3
View File
@@ -139,14 +139,14 @@ jobs:
id: tags id: tags
- name: Docker login - name: Docker login
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
username: meshtastic username: meshtastic
password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }} password: ${{ secrets.DOCKER_FIRMWARE_TOKEN }}
- name: Docker meta (Debian) - name: Docker meta (Debian)
id: meta_debian id: meta_debian
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: meshtastic/meshtasticd images: meshtastic/meshtasticd
tags: | tags: |
@@ -167,7 +167,7 @@ jobs:
- name: Docker meta (Alpine) - name: Docker meta (Alpine)
id: meta_alpine id: meta_alpine
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: meshtastic/meshtasticd images: meshtastic/meshtasticd
tags: | tags: |
+13 -13
View File
@@ -177,7 +177,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-${{matrix.arch}}-* pattern: firmware-${{matrix.arch}}-*
@@ -187,7 +187,7 @@ jobs:
run: ls -R run: ls -R
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -205,7 +205,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -224,7 +224,7 @@ jobs:
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
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -250,13 +250,13 @@ jobs:
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head) filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
fetch-depth: 0 fetch-depth: 0
- name: Download the current manifests - name: Download the current manifests
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
path: ./manifests-new/ path: ./manifests-new/
pattern: manifest-* pattern: manifest-*
merge-multiple: true merge-multiple: true
- name: Upload combined manifests for later commit and global stats crunching. - name: Upload combined manifests for later commit and global stats crunching.
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
id: upload-manifest id: upload-manifest
with: with:
name: manifests-${{ github.sha }} name: manifests-${{ github.sha }}
@@ -324,14 +324,14 @@ jobs:
body: ${{ steps.release_notes.outputs.notes }} body: ${{ steps.release_notes.outputs.notes }}
- name: Download source deb - name: Download source deb
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true merge-multiple: true
path: ./output/debian-src path: ./output/debian-src
- name: Download `native-tft` pio deps - name: Download `native-tft` pio deps
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -355,7 +355,7 @@ jobs:
}' > firmware-${{ needs.version.outputs.long }}.json }' > firmware-${{ needs.version.outputs.long }}.json
- name: Save Release manifest artifact - name: Save Release manifest artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: manifest-${{ needs.version.outputs.long }} name: manifest-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -396,7 +396,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -413,7 +413,7 @@ jobs:
- 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@v8 - uses: actions/download-artifact@v7
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -454,14 +454,14 @@ jobs:
python-version: 3.x python-version: 3.x
- name: Get firmware artifacts - name: Get firmware artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
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 - name: Get manifest artifact
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: manifest-${{ needs.version.outputs.long }} pattern: manifest-${{ needs.version.outputs.long }}
path: ./publish path: ./publish
+9 -9
View File
@@ -147,7 +147,7 @@ jobs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
path: ./ path: ./
pattern: firmware-${{matrix.arch}}-* pattern: firmware-${{matrix.arch}}-*
@@ -160,7 +160,7 @@ jobs:
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat 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@v7 uses: actions/upload-artifact@v6
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -176,7 +176,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -195,7 +195,7 @@ jobs:
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
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true overwrite: true
@@ -235,14 +235,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing... Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb - name: Download source deb
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true merge-multiple: true
path: ./output/debian-src path: ./output/debian-src
- name: Download `native-tft` pio deps - name: Download `native-tft` pio deps
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -292,7 +292,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -309,7 +309,7 @@ jobs:
- 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@v8 - uses: actions/download-artifact@v7
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }} name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
@@ -347,7 +347,7 @@ jobs:
with: with:
python-version: 3.x python-version: 3.x
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v7
with: with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true merge-multiple: true
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true
+1 -11
View File
@@ -54,19 +54,9 @@ jobs:
PLATFORMIO_LIBDEPS_DIR: pio/libdeps PLATFORMIO_LIBDEPS_DIR: pio/libdeps
PLATFORMIO_PACKAGES_DIR: pio/packages PLATFORMIO_PACKAGES_DIR: pio/packages
PLATFORMIO_CORE_DIR: pio/core PLATFORMIO_CORE_DIR: pio/core
PLATFORMIO_SETTING_ENABLE_TELEMETRY: 0
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL: 3650
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD: 10240
- name: Mangle platformio cache
# Add "1" to epoch-timestamps of all downloads in the cache.
# This is a hack to prevent internet access at build-time.
run: |
cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak
jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak > pio/core/.cache/downloads/usage.db
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }} name: platformio-deps-${{ inputs.pio_env }}-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
sudo apt-get install -y dput sudo apt-get install -y dput
- name: Import GPG key - name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@v6
with: with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
id: gpg id: gpg
@@ -60,7 +60,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
- name: Download test artifacts - name: Download test artifacts
if: needs.native-tests.result != 'skipped' if: needs.native-tests.result != 'skipped'
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
+1 -1
View File
@@ -23,8 +23,8 @@ jobs:
series: series:
- jammy # 22.04 LTS - jammy # 22.04 LTS
- noble # 24.04 LTS - noble # 24.04 LTS
- plucky # 25.04
- questing # 25.10 - questing # 25.10
- resolute # 26.04 LTS
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
with: with:
ppa_repo: |- ppa_repo: |-
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
# step 3 # step 3
- name: save report as pipeline artifact - name: save report as pipeline artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: report.sarif name: report.sarif
overwrite: true overwrite: true
+7 -7
View File
@@ -59,7 +59,7 @@ jobs:
id: version id: version
- name: Save coverage information - name: Save coverage information
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
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 }}
@@ -94,7 +94,7 @@ jobs:
- name: Save test results - name: Save test results
if: always() # run this step even if previous step failed if: always() # run this step even if previous step failed
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
@@ -108,7 +108,7 @@ jobs:
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative. sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.
- name: Save coverage information - name: Save coverage information
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
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 }}
@@ -137,20 +137,20 @@ jobs:
id: version id: version
- name: Download test artifacts - name: Download test artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }} name: platformio-test-report-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
- name: Test Report - name: Test Report
uses: dorny/test-reporter@v2.6.0 uses: dorny/test-reporter@v2.5.0
with: with:
name: PlatformIO Tests name: PlatformIO Tests
path: testreport.xml path: testreport.xml
reporter: java-junit reporter: java-junit
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v8 uses: actions/download-artifact@v7
with: with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }} pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}
path: code-coverage-report path: code-coverage-report
@@ -163,7 +163,7 @@ jobs:
genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report genhtml --quiet --legend --prefix "${PWD}" code-coverage-report/coverage_src.info --output-directory code-coverage-report
- name: Save Code Coverage Report - name: Save Code Coverage Report
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: code-coverage-report-${{ steps.version.outputs.long }} name: code-coverage-report-${{ steps.version.outputs.long }}
path: code-coverage-report path: code-coverage-report
+10 -10
View File
@@ -4,29 +4,29 @@ cli:
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.6 ref: v1.7.4
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.508 - checkov@3.2.506
- renovate@43.76.3 - renovate@43.43.2
- prettier@3.8.1 - prettier@3.8.1
- trufflehog@3.93.8 - trufflehog@3.93.5
- yamllint@1.38.0 - yamllint@1.38.0
- bandit@1.9.4 - bandit@1.9.4
- trivy@0.69.3 - trivy@0.69.1
- taplo@0.10.0 - taplo@0.10.0
- ruff@0.15.6 - ruff@0.15.4
- isort@8.0.1 - isort@8.0.0
- markdownlint@0.48.0 - markdownlint@0.47.0
- oxipng@10.1.0 - oxipng@10.1.0
- svgo@4.0.1 - svgo@4.0.0
- actionlint@1.7.11 - actionlint@1.7.11
- 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@26.3.1 - black@26.1.0
- git-diff-check - git-diff-check
- gitleaks@8.30.0 - gitleaks@8.30.0
- clang-format@16.0.3 - clang-format@16.0.3
-2
View File
@@ -187,7 +187,6 @@ 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 # JSONFile: /packets.json # File location for JSON output of decoded packets
# JSONFileRotate: 60 # Rotate JSON file every N minutes, or 0 for no rotation
# JSONFilter: position # filter for packets to save to JSON file # 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
@@ -215,4 +214,3 @@ General:
AvailableDirectory: /etc/meshtasticd/available.d/ AvailableDirectory: /etc/meshtasticd/available.d/
# MACAddress: AA:BB:CC:DD:EE:FF # MACAddress: AA:BB:CC:DD:EE:FF
# MACAddressSource: eth0 # MACAddressSource: eth0
# APIPort: 4403
@@ -6,9 +6,6 @@ Lora:
Reset: 16 # IO4 Reset: 16 # IO4
Busy: 24 # IO5 Busy: 24 # IO5
# Ant_sw: 13 # IO3 # Ant_sw: 13 # IO3
Enable_Pins:
- 12
- 13
DIO3_TCXO_VOLTAGE: true DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true DIO2_AS_RF_SWITCH: true
spidev: spidev0.0 spidev: spidev0.0
@@ -4,8 +4,5 @@ Lora:
Reset: 24 # IO4 Reset: 24 # IO4
Busy: 19 # IO5 Busy: 19 # IO5
# Ant_sw: 23 # IO3 # Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
spidev: spidev0.1 spidev: spidev0.1
# CS: 7 # CS: 7
@@ -1,16 +0,0 @@
Lora:
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
Busy: 24 # IO5
# Ant_sw: 13 # IO3
Enable_Pins:
- 12
- 13
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# CS: 8
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
@@ -1,12 +0,0 @@
Lora:
### RAK13300in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
spidev: spidev0.1
# CS: 7
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
+3 -5
View File
@@ -5,11 +5,9 @@ Lora:
IRQ: 22 #IO6 IRQ: 22 #IO6
Reset: 16 # IO4 Reset: 16 # IO4
Busy: 24 # IO5 Busy: 24 # IO5
Enable_Pins: # Ant_sw: 13 # IO3
- 12
- 13
DIO3_TCXO_VOLTAGE: true DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true DIO2_AS_RF_SWITCH: true
spidev: spidev0.0 spidev: spidev0.0
# GPIO_DETECT_PA: 13 GPIO_DETECT_PA: 13
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8] TX_GAIN_LORA: [8, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 8, 8, 7]
@@ -87,9 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.21" date="2026-03-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.21</url>
</release>
<release version="2.7.20" date="2026-02-11"> <release version="2.7.20" date="2026-02-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
</release> </release>
-60
View File
@@ -1,60 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v7.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_TRACKER_T2000_C -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"],
["0x2886", "0x0057"]
],
"usb_product": "T2000-BOOT",
"mcu": "nrf52840",
"variant": "Seeed_T2000-C",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "7.3.0",
"sd_fwid": "0x0123"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Seeed T2000-C",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": [
"jlink",
"nrfjprog",
"nrfutil",
"stlink",
"cmsis-dap",
"blackmagic"
],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://www.seeedstudio.com/",
"vendor": "Seeed Studio"
}
+1 -1
View File
@@ -12,6 +12,6 @@ Ex:
- `logo_320x480.png` - `logo_320x480.png`
- `logo_320x240.png` - `logo_320x240.png`
This file is copied to `data/boot/logo.png` before filesystem image compilation. This file is copied to `data/boot/logo.png` before filesytem image compilation.
For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33). For additional examples see the [`event/defcon33` branch](https://github.com/meshtastic/firmware/tree/event/defcon33).
-6
View File
@@ -1,9 +1,3 @@
meshtasticd (2.7.21.0) unstable; urgency=medium
* Version 2.7.21
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 11 Mar 2026 11:45:36 +0000
meshtasticd (2.7.20.0) unstable; urgency=medium meshtasticd (2.7.20.0) unstable; urgency=medium
* Version 2.7.20 * Version 2.7.20
-7
View File
@@ -3,17 +3,10 @@ export DEBEMAIL="jbennett@incomsystems.biz"
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
export PLATFORMIO_PACKAGES_DIR=pio/packages export PLATFORMIO_PACKAGES_DIR=pio/packages
export PLATFORMIO_CORE_DIR=pio/core export PLATFORMIO_CORE_DIR=pio/core
export PLATFORMIO_SETTING_ENABLE_TELEMETRY=0
export PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650
export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
# Download libraries to `pio` # Download libraries to `pio`
platformio pkg install -e native-tft platformio pkg install -e native-tft
platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0 platformio pkg install -e native-tft -t platformio/tool-scons@4.40502.0
# Mangle PlatformIO cache to prevent internet access at build-time
# Simply adds 1 to all expiry (epoch) timestamps, adding ~500 years to expiry date
cp pio/core/.cache/downloads/usage.db pio/core/.cache/downloads/usage.db.bak
jq -c 'with_entries(.value |= (. | tostring + "1" | tonumber))' pio/core/.cache/downloads/usage.db.bak >pio/core/.cache/downloads/usage.db
# Compress `pio` directory to prevent dh_clean from sanitizing it # Compress `pio` directory to prevent dh_clean from sanitizing it
tar -cf pio.tar pio/ tar -cf pio.tar pio/
rm -rf pio rm -rf pio
+1 -4
View File
@@ -9,10 +9,7 @@
PIO_ENV:=\ PIO_ENV:=\
PLATFORMIO_CORE_DIR=pio/core \ PLATFORMIO_CORE_DIR=pio/core \
PLATFORMIO_LIBDEPS_DIR=pio/libdeps \ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
PLATFORMIO_PACKAGES_DIR=pio/packages \ PLATFORMIO_PACKAGES_DIR=pio/packages
PLATFORMIO_SETTING_ENABLE_TELEMETRY=0 \
PLATFORMIO_SETTING_CHECK_PLATFORMIO_INTERVAL=3650 \
PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
# Raspbian armhf builds should be compatible with armv6-hardfloat # Raspbian armhf builds should be compatible with armv6-hardfloat
# https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags # https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/#rpi1-compiler-flags
+1 -5
View File
@@ -97,11 +97,7 @@ 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.4.0
build_unflags =
-std=c++11
-std=gnu++11
build_flags = ${env.build_flags} -Os build_flags = ${env.build_flags} -Os
-std=gnu++17
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/> build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
; Common libs for communicating over TCP/IP networks such as MQTT ; Common libs for communicating over TCP/IP networks such as MQTT
@@ -126,7 +122,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/622b034d8791153de9d16a473723cb8625d35839.zip https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
+6 -8
View File
@@ -38,10 +38,7 @@ static bool isPowered()
return true; return true;
#endif #endif
bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0);
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
? 1
: 0);
// If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON // If we are not a router and we already have AC power go to POWER state after init, otherwise go to ON
// We assume routers might be powered all the time, but from a low current (solar) source // We assume routers might be powered all the time, but from a low current (solar) source
@@ -185,7 +182,11 @@ static void darkEnter()
static void serialEnter() static void serialEnter()
{ {
LOG_POWERFSM("State: serialEnter"); LOG_POWERFSM("State: serialEnter");
#ifndef ARCH_NRF52
// nRF52 runs BLE on SoftDevice independently of USB serial — no need to disable it.
// (Same rationale as nbEnter() which already guards this with #ifdef ARCH_ESP32)
setBluetoothEnable(false); setBluetoothEnable(false);
#endif
if (screen) { if (screen) {
screen->setOn(true); screen->setOn(true);
} }
@@ -265,10 +266,7 @@ Fsm powerFSM(&stateBOOT);
void PowerFSM_setup() void PowerFSM_setup()
{ {
bool isRouter = ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || bool isRouter = (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ? 1 : 0);
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
? 1
: 0);
bool hasPower = isPowered(); bool hasPower = isPowered();
LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0); LOG_INFO("PowerFSM init, USB power=%d", hasPower ? 1 : 0);
+1 -1
View File
@@ -345,7 +345,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
return; return;
} }
void RedirectablePrint::hexDump(const char *logLevel, const unsigned char *buf, uint16_t len) void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16_t len)
{ {
const char alphabet[17] = "0123456789abcdef"; const char alphabet[17] = "0123456789abcdef";
log(logLevel, " +------------------------------------------------+ +----------------+"); log(logLevel, " +------------------------------------------------+ +----------------+");
+1 -1
View File
@@ -44,7 +44,7 @@ class RedirectablePrint : public Print
/** like printf but va_list based */ /** like printf but va_list based */
size_t vprintf(const char *logLevel, const char *format, va_list arg); size_t vprintf(const char *logLevel, const char *format, va_list arg);
void hexDump(const char *logLevel, const unsigned char *buf, uint16_t len); void hexDump(const char *logLevel, unsigned char *buf, uint16_t len);
std::string mt_sprintf(const std::string fmt_str, ...); std::string mt_sprintf(const std::string fmt_str, ...);
+64 -18
View File
@@ -78,11 +78,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Configuration // Configuration
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Pre-hop drop handling (compile-time flag).
#ifndef MESHTASTIC_PREHOP_DROP
#define MESHTASTIC_PREHOP_DROP 0
#endif
/// Convert a preprocessor name into a quoted string /// Convert a preprocessor name into a quoted string
#define xstr(s) ystr(s) #define xstr(s) ystr(s)
#define ystr(s) #s #define ystr(s) #s
@@ -154,12 +149,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
#endif #endif
#ifdef USE_KCT8103L_PA
// Power Amps are often non-linear, so we can use an array of values for the power curve
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 12, 11, 10, 9, 8, 7
#endif
#ifdef RAK13302 #ifdef RAK13302
#define NUM_PA_POINTS 22 #define NUM_PA_POINTS 22
#define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 #define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8
@@ -174,10 +163,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define TX_GAIN_LORA 0 #define TX_GAIN_LORA 0
#endif #endif
#ifndef HAS_LORA_FEM
#define HAS_LORA_FEM 0
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Feature toggles // Feature toggles
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -276,8 +261,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BHI260AP_ADDR 0x28 #define BHI260AP_ADDR 0x28
#define BMM150_ADDR 0x13 #define BMM150_ADDR 0x13
#define DA217_ADDR 0x26 #define DA217_ADDR 0x26
#define BMI270_ADDR 0x68
#define BMI270_ADDR_ALT 0x69
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// LED // LED
@@ -504,7 +487,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1 #define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1 #define MESHTASTIC_EXCLUDE_STOREFORWARD 1
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1 #define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1
#define MESHTASTIC_EXCLUDE_ATAK 1 #define MESHTASTIC_EXCLUDE_ATAK 1
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1 #define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1 #define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
@@ -546,5 +528,69 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define HAS_SCREEN 0 #define HAS_SCREEN 0
#endif #endif
// -----------------------------------------------------------------------------
// MESHTASTIC_LOCKDOWN — combined hardened build flag
//
// All targets:
// DEBUG_MUTE — suppress serial/USB log output
// MESHTASTIC_EXCLUDE_* — strip non-essential modules
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL — redact keys from unauthenticated BLE/USB/TCP clients
// HARDENED_DEFAULTS — set is_managed, RANDOM_PIN, TAK role at first boot
//
// nRF52 (CC310 hardware crypto):
// MESHTASTIC_ENCRYPTED_STORAGE — AES-128-CTR + HMAC-SHA256 at-rest encryption
// MESHTASTIC_ENABLE_APPROTECT — write UICR APPROTECT to block debug probe
//
// ESP32 (stub — mbedTLS crypto TODO):
// MESHTASTIC_ENCRYPTED_STORAGE — same API, plaintext pass-through until ported
// APPROTECT equivalent — burn JTAG_DISABLE + flash encryption via espsecure.py
// -----------------------------------------------------------------------------
#ifdef MESHTASTIC_LOCKDOWN
// Suppress all serial/USB-CDC log output — prevents info leakage via physical access
#define DEBUG_MUTE
// Strip non-essential modules to reduce attack surface
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_POWER_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_RANGETEST 1
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
#define MESHTASTIC_EXCLUDE_SERIAL 1
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1
#define MESHTASTIC_EXCLUDE_DROPZONE 1
#define MESHTASTIC_EXCLUDE_REPLYBOT 1
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
#define MESHTASTIC_EXCLUDE_AUDIO 1
#define MESHTASTIC_EXCLUDE_STATUS 1
#define MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE 1
#define MESHTASTIC_EXCLUDE_POWERMON 1
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
// Access control and hardened defaults are pure C++ — no platform-specific deps.
#define MESHTASTIC_PHONEAPI_ACCESS_CONTROL 1
#define HARDENED_DEFAULTS 1
// Platform-specific security features:
#if defined(ARCH_NRF52)
// Full encrypted storage via CC310 hardware AES-128-CTR + HMAC-SHA256.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
// Hardware JTAG/SWD lockout: firmware writes NRF_UICR->APPROTECT = 0x00 once.
#define MESHTASTIC_ENABLE_APPROTECT 1
#elif defined(ARCH_ESP32)
// Encrypted storage stub: plaintext pass-through, same API, crypto TODO (mbedTLS).
// APPROTECT equivalent: burn JTAG_DISABLE eFuse and enable flash encryption
// via espsecure.py at provisioning time — not firmware-controlled.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
#else
#warning "MESHTASTIC_LOCKDOWN: encrypted storage not available on this platform; access control and hardened defaults still active"
#endif
#endif // MESHTASTIC_LOCKDOWN
#include "DebugConfiguration.h" #include "DebugConfiguration.h"
#include "RF95Configuration.h" #include "RF95Configuration.h"
+2 -3
View File
@@ -37,9 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{ {
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, LSM6DSOX, BMX160, ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150};
STK8BAXX, ICM20948, QMA6100P, BMM150, BMI270}; return firstOfOrNONE(9, types);
return firstOfOrNONE(11, types);
} }
ScanI2C::FoundDevice ScanI2C::firstAQI() const ScanI2C::FoundDevice ScanI2C::firstAQI() const
+1 -4
View File
@@ -47,7 +47,6 @@ class ScanI2C
BMA423, BMA423,
BQ24295, BQ24295,
LSM6DS3, LSM6DS3,
LSM6DSOX,
TCA9535, TCA9535,
TCA9555, TCA9555,
VEML7700, VEML7700,
@@ -90,12 +89,10 @@ class ScanI2C
DA217, DA217,
CHSC6X, CHSC6X,
CST226SE, CST226SE,
BMI270,
SEN5X, SEN5X,
SFA30, SFA30,
CW2015, CW2015,
SCD30, SCD30
ADS1115
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;
+8 -39
View File
@@ -341,9 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = DPS310; type = DPS310;
break; break;
} }
if (type == DPS310) { break;
break;
}
default: default:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
switch (registerValue) { switch (registerValue) {
@@ -511,9 +509,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (registerValue == 0x6A) { if (registerValue == 0x6A) {
type = LSM6DS3; type = LSM6DS3;
logFoundDevice("LSM6DS3", (uint8_t)addr.address); logFoundDevice("LSM6DS3", (uint8_t)addr.address);
} else if (registerValue == 0x6C) {
type = LSM6DSOX;
logFoundDevice("LSM6DSOX", (uint8_t)addr.address);
} else { } else {
type = QMI8658; type = QMI8658;
logFoundDevice("QMI8658", (uint8_t)addr.address); logFoundDevice("QMI8658", (uint8_t)addr.address);
@@ -551,19 +546,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
break; break;
case LSM6DS3_ADDR: SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1);
if (registerValue == 0x6A) {
type = LSM6DS3;
logFoundDevice("LSM6DS3", (uint8_t)addr.address);
} else if (registerValue == 0x6C) {
type = LSM6DSOX;
logFoundDevice("LSM6DSOX", (uint8_t)addr.address);
} else {
type = QMI8658;
logFoundDevice("QMI8658", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
case TCA9555_ADDR: case TCA9555_ADDR:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1); registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 1);
@@ -669,8 +652,9 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
} }
break; break;
case ICM20948_ADDR: // same as BMX160_ADDR, BMI270_ADDR_ALT, and SEN5X_ADDR case ICM20948_ADDR: // same as BMX160_ADDR and SEN5X_ADDR
case ICM20948_ADDR_ALT: // same as MPU6050_ADDR, BMI270_ADDR case ICM20948_ADDR_ALT: // same as MPU6050_ADDR
// ICM20948 Register check
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1);
#ifdef HAS_ICM20948 #ifdef HAS_ICM20948
type = ICM20948; type = ICM20948;
@@ -681,14 +665,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = ICM20948; type = ICM20948;
logFoundDevice("ICM20948", (uint8_t)addr.address); logFoundDevice("ICM20948", (uint8_t)addr.address);
break; break;
} else if (registerValue == 0x24) {
type = BMI270;
logFoundDevice("BMI270", (uint8_t)addr.address);
break;
} else if (registerValue == 0xD8) { // BMX160 chip ID at register 0x00
type = BMX160;
logFoundDevice("BMX160", (uint8_t)addr.address);
break;
} else { } else {
String prod = ""; String prod = "";
prod = readSEN5xProductName(i2cBus, addr.address); prod = readSEN5xProductName(i2cBus, addr.address);
@@ -742,18 +718,11 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (len == 5 && memcmp(expectedInfo, info, len) == 0) { if (len == 5 && memcmp(expectedInfo, info, len) == 0) {
LOG_INFO("NXP SE050 crypto chip found"); LOG_INFO("NXP SE050 crypto chip found");
type = NXP_SE050; type = NXP_SE050;
break;
}
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x01), 2); } else {
if (registerValue == 0x8583 || registerValue == 0x8580) { LOG_INFO("FT6336U touchscreen found");
type = ADS1115; type = FT6336U;
logFoundDevice("ADS1115 ADC", (uint8_t)addr.address);
break;
} }
LOG_INFO("FT6336U touchscreen found");
type = FT6336U;
break; break;
} }
+6 -6
View File
@@ -93,7 +93,7 @@ static const char *getGPSPowerStateString(GPSPowerState state)
#ifdef PIN_GPS_SWITCH #ifdef PIN_GPS_SWITCH
// If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping // If we have a hardware switch, define a periodic watcher outside of the GPS runOnce thread, since this can be sleeping
// indefinitely // idefinitely
int lastState = LOW; int lastState = LOW;
bool firstrun = true; bool firstrun = true;
@@ -586,14 +586,14 @@ bool GPS::setup()
_serial_gps->write("$PMTK301,2*2E\r\n"); _serial_gps->write("$PMTK301,2*2E\r\n");
delay(250); delay(250);
} else if (gnssModel == GNSS_MODEL_ATGM336H) { } else if (gnssModel == GNSS_MODEL_ATGM336H) {
// Set the initial configuration of the device - these _should_ work for most AT6558 devices // Set the intial configuration of the device - these _should_ work for most AT6558 devices
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF); msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
_serial_gps->write(UBXscratch, msglen); _serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) { if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
LOG_WARN("ATGM336H: Could not set Config"); LOG_WARN("ATGM336H: Could not set Config");
} }
// Set the update frequency to 1Hz // Set the update frequence to 1Hz
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ); msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
_serial_gps->write(UBXscratch, msglen); _serial_gps->write(UBXscratch, msglen);
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) { if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
@@ -700,7 +700,7 @@ bool GPS::setup()
} else { // 8,9 } else { // 8,9
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured"); LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
} }
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next
// commands for the M8 it tends to be more... 1 sec should be enough ;>) // commands for the M8 it tends to be more... 1 sec should be enough ;>)
delay(1000); delay(1000);
} }
@@ -733,7 +733,7 @@ bool GPS::setup()
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500); SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500); SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
// For M8 we want to enable NMEA version 4.10 so we can see the additional sats. // For M8 we want to enable NMEA vserion 4.10 so we can see the additional sats.
if (gnssModel == GNSS_MODEL_UBLOX8) { if (gnssModel == GNSS_MODEL_UBLOX8) {
clearBuffer(); clearBuffer();
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500); SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
@@ -1211,7 +1211,7 @@ int32_t GPS::runOnce()
return disable(); // This should trigger when we have a fixed position, and get that first position return disable(); // This should trigger when we have a fixed position, and get that first position
// 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms // 9600bps is approx 1 byte per msec, so considering our buffer size we never need to wake more often than 200ms
// if not awake we can run super infrequently (once every 5 secs?) to see if we need to wake. // if not awake we can run super infrquently (once every 5 secs?) to see if we need to wake.
return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000; return (powerState == GPS_ACTIVE) ? GPS_THREAD_INTERVAL : 5000;
} }
+5 -5
View File
@@ -12,7 +12,7 @@ GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _lon
GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt)
{ {
// Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000
_latitude = int32_t(lat * 1e+7); _latitude = int32_t(lat * 1e+7);
_longitude = int32_t(lon * 1e+7); _longitude = int32_t(lon * 1e+7);
GeoCoord::setCoords(); GeoCoord::setCoords();
@@ -20,7 +20,7 @@ GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt)
GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt)
{ {
// Change decimal representation to int32_t. I.e., 12.345 becomes 123450000 // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000
_latitude = int32_t(lat * 1e+7); _latitude = int32_t(lat * 1e+7);
_longitude = int32_t(lon * 1e+7); _longitude = int32_t(lon * 1e+7);
GeoCoord::setCoords(); GeoCoord::setCoords();
@@ -467,10 +467,10 @@ int32_t GeoCoord::bearingTo(const GeoCoord &pointB)
} }
/** /**
* Create a new point based on the passed-in point * Create a new point bassed on the passed in poin
* Ported from http://www.edwilliams.org/avform147.htm#LL * Ported from http://www.edwilliams.org/avform147.htm#LL
* @param bearing * @param bearing
* The bearing in radians * The bearing in raidans
* @param range_meters * @param range_meters
* range in meters * range in meters
* @return GeoCoord object of point at bearing and range from initial point * @return GeoCoord object of point at bearing and range from initial point
@@ -593,4 +593,4 @@ double GeoCoord::toRadians(double deg)
double GeoCoord::toDegrees(double r) double GeoCoord::toDegrees(double r)
{ {
return r * 180 / PI; return r * 180 / PI;
} }
+2 -2
View File
@@ -223,7 +223,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
// This delta value works on all platforms // This delta value works on all platforms
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv->tv_sec; zeroOffsetSecs = tv->tv_sec;
// If this platform has a settable RTC, set it // If this platform has a setable RTC, set it
#ifdef RV3028_RTC #ifdef RV3028_RTC
if (rtc_found.address == RV3028_RTC) { if (rtc_found.address == RV3028_RTC) {
Melopero_RV3028 rtc; Melopero_RV3028 rtc;
@@ -402,7 +402,7 @@ time_t gm_mktime(const struct tm *tm)
#if !MESHTASTIC_EXCLUDE_TZ #if !MESHTASTIC_EXCLUDE_TZ
time_t result = 0; time_t result = 0;
// First, get us to the start of tm->year, by calculating the number of days since the Unix epoch. // First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch.
int year = 1900 + tm->tm_year; // tm_year is years since 1900 int year = 1900 + tm->tm_year; // tm_year is years since 1900
int year_minus_one = year - 1; int year_minus_one = year - 1;
int days_before_this_year = 0; int days_before_this_year = 0;
+1 -1
View File
@@ -37,7 +37,7 @@ static const uint8_t _message_CAS_CFG_RATE_1HZ[] = {
// CFG-NAVX (0x06, 0x07) // CFG-NAVX (0x06, 0x07)
// Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system // Initial ATGM33H-5N configuration, Updates for Dynamic Mode, Fix Mode, and SV system
// Quirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable // Qwirk: The ATGM33H-5N-31 should only support GPS+BDS, however it will happily enable
// and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used. // and use GPS+BDS+GLONASS iff the correct CFG_NAVX command is used.
static const uint8_t _message_CAS_CFG_NAVX_CONF[] = { static const uint8_t _message_CAS_CFG_NAVX_CONF[] = {
0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings 0x03, 0x01, 0x00, 0x00, // Update Mask: Dynamic Mode, Fix Mode, Nav Settings
+6 -6
View File
@@ -57,7 +57,7 @@ static const uint8_t _message_CFG_PM2[] PROGMEM = {
0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11 0x00, 0x00, 0x00, 0x00 // 0x64, 0x40, 0x01, 0x00 // reserved 11
}; };
// Constellation setup, none required for Neo-6 // Constallation setup, none required for Neo-6
// For Neo-7 GPS & SBAS // For Neo-7 GPS & SBAS
static const uint8_t _message_GNSS_7[] = { static const uint8_t _message_GNSS_7[] = {
@@ -157,7 +157,7 @@ static const uint8_t _message_NAVX5[] = {
0x00, 0x00, 0x00, 0x00, // Reserved 9 0x00, 0x00, 0x00, 0x00, // Reserved 9
0x00, // Reserved 10 0x00, // Reserved 10
0x00, // Reserved 11 0x00, // Reserved 11
0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true)
0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled) 0x01, // useAOP (AssistNow Autonomous configuration) = 1 (enabled)
0x00, // Reserved 12 0x00, // Reserved 12
0x00, // Reserved 13 0x00, // Reserved 13
@@ -185,7 +185,7 @@ static const uint8_t _message_NAVX5_8[] = {
0x00, // Reserved 4 0x00, // Reserved 4
0x00, 0x00, // Reserved 5 0x00, 0x00, // Reserved 5
0x00, 0x00, // Reserved 6 0x00, 0x00, // Reserved 6
0x00, // usePPP (Precise Point Positioning) (0 = false, 1 = true) 0x00, // usePPP (Precice Point Positioning) (0 = false, 1 = true)
0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled) 0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled)
0x00, 0x00, // Reserved 7 0x00, 0x00, // Reserved 7
0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default 0x00, 0x00, // aopOrbMaxErr = 0 to reset to firmware default
@@ -314,7 +314,7 @@ static const uint8_t _message_DISABLE_TXT_INFO[] = {
// This command applies to M8 products // This command applies to M8 products
static const uint8_t _message_PMS[] = { static const uint8_t _message_PMS[] = {
0x00, // Version (0) 0x00, // Version (0)
0x03, // Power setup value 3 = Agressive 1Hz 0x03, // Power setup value 3 = Agresssive 1Hz
0x00, 0x00, // period: not applicable, set to 0 0x00, 0x00, // period: not applicable, set to 0
0x00, 0x00, // onTime: not applicable, set to 0 0x00, 0x00, // onTime: not applicable, set to 0
0x00, 0x00 // reserved, generated by u-center 0x00, 0x00 // reserved, generated by u-center
@@ -337,7 +337,7 @@ static const uint8_t _message_SAVE_10[] = {
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR. // As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
// BBR will survive a restart, and power off for a while, but modules with small backup // BBR will survive a restart, and power off for a while, but modules with small backup
// batteries or super caps will not retain the config for a long power off time. // batteries or super caps will not retain the config for a long power off time.
// for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast acquisition after // for all configurations using sleep / low power modes, V_BCKP needs to be hooked to permanent power for fast aquisition after
// sleep // sleep
// VALSET Commands for M10 // VALSET Commands for M10
@@ -462,7 +462,7 @@ Default GNSS configuration is: GPS, Galileo, BDS B1l, with QZSS and SBAS enabled
The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need The PMREQ puts the receiver to sleep and wakeup re-acquires really fast and seems to not need
the PM config. Lets try without it. the PM config. Lets try without it.
PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats. PMREQ sort of works with SBAS, but the awake time is too short to re-acquire any SBAS sats.
The definition of "Got Fix" doesn't seem to include SBAS. Much more too this... The defination of "Got Fix" doesn't seem to include SBAS. Much more too this...
Even if it was, it can take minutes (up to 12.5), Even if it was, it can take minutes (up to 12.5),
even under good sat visibility conditions to re-acquire the SBAS data. even under good sat visibility conditions to re-acquire the SBAS data.
+13 -11
View File
@@ -101,7 +101,7 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
return true; return true;
} }
// End the update process - virtual method, overridden in derived class // End the update process - virtual method, overriden in derived class
void EInkDisplay::endUpdate() void EInkDisplay::endUpdate()
{ {
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep) // Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
@@ -143,10 +143,6 @@ bool EInkDisplay::connect()
#ifdef ELECROW_ThinkNode_M1 #ifdef ELECROW_ThinkNode_M1
// ThinkNode M1 has a hardware dimmable backlight. Start enabled // ThinkNode M1 has a hardware dimmable backlight. Start enabled
digitalWrite(PIN_EINK_EN, HIGH); digitalWrite(PIN_EINK_EN, HIGH);
#elif defined(MINI_EPAPER_S3)
// T-Mini Epaper S3 requires panel power rail enabled before SPI transfer.
digitalWrite(PIN_EINK_EN, HIGH);
delay(10);
#else #else
digitalWrite(PIN_EINK_EN, LOW); digitalWrite(PIN_EINK_EN, LOW);
#endif #endif
@@ -206,8 +202,7 @@ bool EInkDisplay::connect()
} }
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ #elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \ defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
defined(MINI_EPAPER_S3)
{ {
// Start HSPI // Start HSPI
hspi = new SPIClass(HSPI); hspi = new SPIClass(HSPI);
@@ -221,13 +216,9 @@ bool EInkDisplay::connect()
// Init GxEPD2 // Init GxEPD2
adafruitDisplay->init(); adafruitDisplay->init();
#if defined(MINI_EPAPER_S3)
adafruitDisplay->setRotation(3);
#else
adafruitDisplay->setRotation(3); adafruitDisplay->setRotation(3);
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) #if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER)
adafruitDisplay->setRotation(0); adafruitDisplay->setRotation(0);
#endif
#endif #endif
} }
#elif defined(PCA10059) || defined(ME25LS01) #elif defined(PCA10059) || defined(ME25LS01)
@@ -268,6 +259,17 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(3); adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(MINI_EPAPER_S3)
spi1 = new SPIClass(HSPI);
spi1->begin(PIN_SPI1_SCK, PIN_SPI1_MISO, PIN_SPI1_MOSI, PIN_EINK_CS);
// Create GxEPD2 objects
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(1);
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
// Detect display model, before starting SPI // Detect display model, before starting SPI
+3 -3
View File
@@ -89,12 +89,12 @@ class EInkDisplay : public OLEDDisplay
// If display uses HSPI // If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) || \ defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5)
defined(MINI_EPAPER_S3)
SPIClass *hspi = NULL; SPIClass *hspi = NULL;
#endif #endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) #if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) || \
defined(MINI_EPAPER_S3)
SPIClass *spi1 = NULL; SPIClass *spi1 = NULL;
#endif #endif
+3 -3
View File
@@ -95,7 +95,7 @@ void EInkDynamicDisplay::adjustRefreshCounters()
// Trigger the display update by calling base class // Trigger the display update by calling base class
bool EInkDynamicDisplay::update() bool EInkDynamicDisplay::update()
{ {
// Determine the refresh mode to use, and start the update // Detemine the refresh mode to use, and start the update
bool refreshApproved = determineMode(); bool refreshApproved = determineMode();
if (refreshApproved) { if (refreshApproved) {
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
@@ -317,7 +317,7 @@ void EInkDynamicDisplay::checkFrameMatchesPrevious()
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags); LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags);
} }
// Have too many fast-refreshes occurred consecutively, since last full refresh? // Have too many fast-refreshes occured consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes() void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{ {
// If a decision was already reached, don't run the check // If a decision was already reached, don't run the check
@@ -561,4 +561,4 @@ void EInkDynamicDisplay::awaitRefresh()
} }
#endif // HAS_EINK_ASYNCFULL #endif // HAS_EINK_ASYNCFULL
#endif // USE_EINK_DYNAMICDISPLAY #endif // USE_EINK_DYNAMICDISPLAY
-434
View File
@@ -1,434 +0,0 @@
#include "configuration.h"
#if HAS_SCREEN
#include "graphics/EmoteRenderer.h"
#include <algorithm>
#include <cstring>
namespace graphics
{
namespace EmoteRenderer
{
static inline int getStringWidth(OLEDDisplay *display, const char *text, size_t len)
{
#if defined(OLED_UA) || defined(OLED_RU)
return display->getStringWidth(text, len, true);
#else
(void)len;
return display->getStringWidth(text);
#endif
}
size_t utf8CharLen(uint8_t c)
{
if ((c & 0xE0) == 0xC0)
return 2;
if ((c & 0xF0) == 0xE0)
return 3;
if ((c & 0xF8) == 0xF0)
return 4;
return 1;
}
static inline bool isPossibleEmoteLead(uint8_t c)
{
// All supported emoji labels in emotes.cpp are currently in these UTF-8 lead ranges.
return c == 0xE2 || c == 0xF0;
}
static inline int getUtf8ChunkWidth(OLEDDisplay *display, const char *text, size_t len)
{
char chunk[5] = {0, 0, 0, 0, 0};
if (len > 4)
len = 4;
memcpy(chunk, text, len);
return getStringWidth(display, chunk, len);
}
static inline bool isFE0FAt(const char *s, size_t pos, size_t len)
{
return pos + 2 < len && static_cast<uint8_t>(s[pos]) == 0xEF && static_cast<uint8_t>(s[pos + 1]) == 0xB8 &&
static_cast<uint8_t>(s[pos + 2]) == 0x8F;
}
static inline bool isSkinToneAt(const char *s, size_t pos, size_t len)
{
return pos + 3 < len && static_cast<uint8_t>(s[pos]) == 0xF0 && static_cast<uint8_t>(s[pos + 1]) == 0x9F &&
static_cast<uint8_t>(s[pos + 2]) == 0x8F &&
(static_cast<uint8_t>(s[pos + 3]) >= 0xBB && static_cast<uint8_t>(s[pos + 3]) <= 0xBF);
}
static inline size_t ignorableModifierLenAt(const char *s, size_t pos, size_t len)
{
// Skip modifiers that do not change which bitmap we render.
if (isFE0FAt(s, pos, len))
return 3;
if (isSkinToneAt(s, pos, len))
return 4;
return 0;
}
const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet, int emoteCount)
{
if (!label || !*label)
return nullptr;
for (int i = 0; i < emoteCount; ++i) {
if (emoteSet[i].label && strcmp(label, emoteSet[i].label) == 0)
return &emoteSet[i];
}
return nullptr;
}
static bool matchAtIgnoringModifiers(const char *text, size_t textLen, size_t pos, const char *label, size_t &textConsumed,
size_t &matchScore)
{
// Treat FE0F and skin-tone modifiers as optional while matching.
textConsumed = 0;
matchScore = 0;
if (!label || !*label || pos >= textLen)
return false;
const size_t labelLen = strlen(label);
size_t ti = pos;
size_t li = 0;
while (true) {
while (ti < textLen) {
const size_t skipLen = ignorableModifierLenAt(text, ti, textLen);
if (!skipLen)
break;
ti += skipLen;
}
while (li < labelLen) {
const size_t skipLen = ignorableModifierLenAt(label, li, labelLen);
if (!skipLen)
break;
li += skipLen;
}
if (li >= labelLen) {
while (ti < textLen) {
const size_t skipLen = ignorableModifierLenAt(text, ti, textLen);
if (!skipLen)
break;
ti += skipLen;
}
textConsumed = ti - pos;
return textConsumed > 0;
}
if (ti >= textLen)
return false;
const uint8_t tc = static_cast<uint8_t>(text[ti]);
const uint8_t lc = static_cast<uint8_t>(label[li]);
const size_t tlen = utf8CharLen(tc);
const size_t llen = utf8CharLen(lc);
if (tlen != llen || ti + tlen > textLen || li + llen > labelLen)
return false;
if (memcmp(text + ti, label + li, tlen) != 0)
return false;
ti += tlen;
li += llen;
matchScore += llen;
}
}
const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet, int emoteCount)
{
// Prefer the longest matching label at this byte offset.
const Emote *matched = nullptr;
matchLen = 0;
size_t bestScore = 0;
if (!text || pos >= textLen)
return nullptr;
if (!isPossibleEmoteLead(static_cast<uint8_t>(text[pos])))
return nullptr;
for (int i = 0; i < emoteCount; ++i) {
const char *label = emoteSet[i].label;
if (!label || !*label)
continue;
if (static_cast<uint8_t>(label[0]) != static_cast<uint8_t>(text[pos]))
continue;
const size_t labelLen = strlen(label);
if (labelLen == 0)
continue;
size_t candidateLen = 0;
size_t candidateScore = 0;
if (pos + labelLen <= textLen && memcmp(text + pos, label, labelLen) == 0) {
candidateLen = labelLen;
candidateScore = labelLen;
} else if (matchAtIgnoringModifiers(text, textLen, pos, label, candidateLen, candidateScore)) {
// Matched with FE0F/skin tone modifiers treated as optional.
} else {
continue;
}
if (candidateScore > bestScore) {
matched = &emoteSet[i];
matchLen = candidateLen;
bestScore = candidateScore;
}
}
return matched;
}
static LineMetrics analyzeLineInternal(OLEDDisplay *display, const char *line, size_t lineLen, int fallbackHeight,
const Emote *emoteSet, int emoteCount, int emoteSpacing)
{
// Scan once to collect width and tallest emote for this line.
LineMetrics metrics{0, fallbackHeight, false};
if (!line)
return metrics;
for (size_t i = 0; i < lineLen;) {
size_t matchLen = 0;
const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount);
if (matched) {
metrics.hasEmote = true;
metrics.tallestHeight = std::max(metrics.tallestHeight, matched->height);
if (display)
metrics.width += matched->width + emoteSpacing;
i += matchLen;
continue;
}
const size_t skipLen = ignorableModifierLenAt(line, i, lineLen);
if (skipLen) {
i += skipLen;
continue;
}
const size_t charLen = utf8CharLen(static_cast<uint8_t>(line[i]));
if (display)
metrics.width += getUtf8ChunkWidth(display, line + i, charLen);
i += charLen;
}
return metrics;
}
LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight, const Emote *emoteSet, int emoteCount,
int emoteSpacing)
{
return analyzeLineInternal(display, line, line ? strlen(line) : 0, fallbackHeight, emoteSet, emoteCount, emoteSpacing);
}
int maxEmoteHeight(const Emote *emoteSet, int emoteCount)
{
int tallest = 0;
for (int i = 0; i < emoteCount; ++i) {
if (emoteSet[i].label && *emoteSet[i].label)
tallest = std::max(tallest, emoteSet[i].height);
}
return tallest;
}
int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet, int emoteCount, int emoteSpacing)
{
if (!display)
return 0;
if (!line || !*line)
return 0;
return analyzeLine(display, line, 0, emoteSet, emoteCount, emoteSpacing).width;
}
static int appendTextSpanAndMeasure(OLEDDisplay *display, int cursorX, int fontY, const char *text, size_t len, bool draw,
bool fauxBold)
{
// Draw plain-text runs in chunks so UTF-8 stays intact.
if (!text || len == 0)
return cursorX;
char chunk[33];
size_t pos = 0;
while (pos < len) {
size_t chunkLen = 0;
while (pos + chunkLen < len) {
const size_t charLen = utf8CharLen(static_cast<uint8_t>(text[pos + chunkLen]));
if (chunkLen + charLen >= sizeof(chunk))
break;
chunkLen += charLen;
}
if (chunkLen == 0) {
chunkLen = std::min(len - pos, sizeof(chunk) - 1);
}
memcpy(chunk, text + pos, chunkLen);
chunk[chunkLen] = '\0';
if (draw) {
if (fauxBold)
display->drawString(cursorX + 1, fontY, chunk);
display->drawString(cursorX, fontY, chunk);
}
cursorX += getStringWidth(display, chunk, chunkLen);
pos += chunkLen;
}
return cursorX;
}
size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth, const char *ellipsis,
const Emote *emoteSet, int emoteCount, int emoteSpacing)
{
if (!out || outSize == 0)
return 0;
out[0] = '\0';
if (!display || !line || maxWidth <= 0)
return 0;
const size_t lineLen = strlen(line);
const int suffixWidth =
(ellipsis && *ellipsis) ? measureStringWithEmotes(display, ellipsis, emoteSet, emoteCount, emoteSpacing) : 0;
const char *suffix = (ellipsis && suffixWidth <= maxWidth) ? ellipsis : "";
const size_t suffixLen = strlen(suffix);
const int availableWidth = maxWidth - (*suffix ? suffixWidth : 0);
if (measureStringWithEmotes(display, line, emoteSet, emoteCount, emoteSpacing) <= maxWidth) {
strncpy(out, line, outSize - 1);
out[outSize - 1] = '\0';
return strlen(out);
}
int used = 0;
size_t cut = 0;
for (size_t i = 0; i < lineLen;) {
// Keep whole emotes together when deciding where to cut.
int tokenWidth = 0;
size_t advance = 0;
if (isPossibleEmoteLead(static_cast<uint8_t>(line[i]))) {
size_t matchLen = 0;
const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount);
if (matched) {
tokenWidth = matched->width + emoteSpacing;
advance = matchLen;
}
}
if (advance == 0) {
const size_t skipLen = ignorableModifierLenAt(line, i, lineLen);
if (skipLen) {
i += skipLen;
cut = i;
continue;
}
const size_t charLen = utf8CharLen(static_cast<uint8_t>(line[i]));
tokenWidth = getUtf8ChunkWidth(display, line + i, charLen);
advance = charLen;
}
if (used + tokenWidth > availableWidth)
break;
used += tokenWidth;
i += advance;
cut = i;
}
if (cut == 0) {
strncpy(out, suffix, outSize - 1);
out[outSize - 1] = '\0';
return strlen(out);
}
size_t copyLen = cut;
if (copyLen > outSize - 1)
copyLen = outSize - 1;
if (suffixLen > 0 && copyLen + suffixLen > outSize - 1) {
copyLen = (outSize - 1 > suffixLen) ? (outSize - 1 - suffixLen) : 0;
}
memcpy(out, line, copyLen);
size_t totalLen = copyLen;
if (suffixLen > 0 && totalLen < outSize - 1) {
memcpy(out + totalLen, suffix, std::min(suffixLen, outSize - 1 - totalLen));
totalLen += std::min(suffixLen, outSize - 1 - totalLen);
}
out[totalLen] = '\0';
return totalLen;
}
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet,
int emoteCount, int emoteSpacing, bool fauxBold)
{
if (!line)
return;
const size_t lineLen = strlen(line);
// Center text vertically when any emote is taller than the font.
const int maxIconHeight =
analyzeLineInternal(nullptr, line, lineLen, fontHeight, emoteSet, emoteCount, emoteSpacing).tallestHeight;
const int lineHeight = std::max(fontHeight, maxIconHeight);
const int fontY = y + (lineHeight - fontHeight) / 2;
int cursorX = x;
bool inBold = false;
for (size_t i = 0; i < lineLen;) {
// Toggle faux bold.
if (fauxBold && i + 1 < lineLen && line[i] == '*' && line[i + 1] == '*') {
inBold = !inBold;
i += 2;
continue;
}
const size_t skipLen = ignorableModifierLenAt(line, i, lineLen);
if (skipLen) {
i += skipLen;
continue;
}
size_t matchLen = 0;
const Emote *matched = findEmoteAt(line, lineLen, i, matchLen, emoteSet, emoteCount);
if (matched) {
const int iconY = y + (lineHeight - matched->height) / 2;
display->drawXbm(cursorX, iconY, matched->width, matched->height, matched->bitmap);
cursorX += matched->width + emoteSpacing;
i += matchLen;
continue;
}
size_t next = i;
while (next < lineLen) {
// Stop the text run before the next emote or bold marker.
if (fauxBold && next + 1 < lineLen && line[next] == '*' && line[next + 1] == '*')
break;
if (ignorableModifierLenAt(line, next, lineLen))
break;
size_t nextMatchLen = 0;
if (findEmoteAt(line, lineLen, next, nextMatchLen, emoteSet, emoteCount) != nullptr)
break;
next += utf8CharLen(static_cast<uint8_t>(line[next]));
}
if (next == i)
next += utf8CharLen(static_cast<uint8_t>(line[i]));
cursorX = appendTextSpanAndMeasure(display, cursorX, fontY, line + i, next - i, true, fauxBold && inBold);
i = next;
}
}
} // namespace EmoteRenderer
} // namespace graphics
#endif // HAS_SCREEN
-79
View File
@@ -1,79 +0,0 @@
#pragma once
#include "configuration.h"
#if HAS_SCREEN
#include "graphics/emotes.h"
#include <Arduino.h>
#include <OLEDDisplay.h>
#include <string>
#include <vector>
namespace graphics
{
namespace EmoteRenderer
{
struct LineMetrics {
int width;
int tallestHeight;
bool hasEmote;
};
size_t utf8CharLen(uint8_t c);
const Emote *findEmoteByLabel(const char *label, const Emote *emoteSet = emotes, int emoteCount = numEmotes);
const Emote *findEmoteAt(const char *text, size_t textLen, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes,
int emoteCount = numEmotes);
inline const Emote *findEmoteAt(const std::string &text, size_t pos, size_t &matchLen, const Emote *emoteSet = emotes,
int emoteCount = numEmotes)
{
return findEmoteAt(text.c_str(), text.length(), pos, matchLen, emoteSet, emoteCount);
}
LineMetrics analyzeLine(OLEDDisplay *display, const char *line, int fallbackHeight = 0, const Emote *emoteSet = emotes,
int emoteCount = numEmotes, int emoteSpacing = 1);
inline LineMetrics analyzeLine(OLEDDisplay *display, const std::string &line, int fallbackHeight = 0,
const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1)
{
return analyzeLine(display, line.c_str(), fallbackHeight, emoteSet, emoteCount, emoteSpacing);
}
int maxEmoteHeight(const Emote *emoteSet = emotes, int emoteCount = numEmotes);
int measureStringWithEmotes(OLEDDisplay *display, const char *line, const Emote *emoteSet = emotes, int emoteCount = numEmotes,
int emoteSpacing = 1);
inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, const Emote *emoteSet = emotes,
int emoteCount = numEmotes, int emoteSpacing = 1)
{
return measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing);
}
size_t truncateToWidth(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth,
const char *ellipsis = "...", const Emote *emoteSet = emotes, int emoteCount = numEmotes,
int emoteSpacing = 1);
inline std::string truncateToWidth(OLEDDisplay *display, const std::string &line, int maxWidth,
const std::string &ellipsis = "...", const Emote *emoteSet = emotes,
int emoteCount = numEmotes, int emoteSpacing = 1)
{
if (!display || maxWidth <= 0)
return "";
if (measureStringWithEmotes(display, line.c_str(), emoteSet, emoteCount, emoteSpacing) <= maxWidth)
return line;
std::vector<char> out(line.length() + ellipsis.length() + 1, '\0');
truncateToWidth(display, line.c_str(), out.data(), out.size(), maxWidth, ellipsis.c_str(), emoteSet, emoteCount,
emoteSpacing);
return std::string(out.data());
}
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, const Emote *emoteSet = emotes,
int emoteCount = numEmotes, int emoteSpacing = 1, bool fauxBold = true);
inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight,
const Emote *emoteSet = emotes, int emoteCount = numEmotes, int emoteSpacing = 1,
bool fauxBold = true)
{
drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSet, emoteCount, emoteSpacing, fauxBold);
}
} // namespace EmoteRenderer
} // namespace graphics
#endif // HAS_SCREEN
+2 -11
View File
@@ -433,15 +433,12 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
PMU->enablePowerOutput(XPOWERS_ALDO2); PMU->enablePowerOutput(XPOWERS_ALDO2);
#endif #endif
// some screens seem to need a kick in the pants to turn back on #if defined(MUZI_BASE)
#if defined(MUZI_BASE) || defined(M5STACK_CARDPUTER_ADV)
dispdev->init(); dispdev->init();
dispdev->setBrightness(brightness); dispdev->setBrightness(brightness);
dispdev->flipScreenVertically(); dispdev->flipScreenVertically();
dispdev->resetDisplay(); dispdev->resetDisplay();
#ifdef SCREEN_12V_ENABLE
digitalWrite(SCREEN_12V_ENABLE, HIGH); digitalWrite(SCREEN_12V_ENABLE, HIGH);
#endif
delay(100); delay(100);
#endif #endif
#if !ARCH_PORTDUINO #if !ARCH_PORTDUINO
@@ -465,11 +462,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) #if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
ui->init(); ui->init();
#endif #endif
#if defined(USE_ST7789) && defined(VTFT_LEDA) #ifdef USE_ST7789
#ifdef VTFT_CTRL
pinMode(VTFT_CTRL, OUTPUT); pinMode(VTFT_CTRL, OUTPUT);
digitalWrite(VTFT_CTRL, LOW); digitalWrite(VTFT_CTRL, LOW);
#endif
ui->init(); ui->init();
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT); analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
@@ -511,12 +506,8 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#ifdef USE_ST7789 #ifdef USE_ST7789
SPI1.end(); SPI1.end();
#if defined(ARCH_ESP32) #if defined(ARCH_ESP32)
#ifdef VTFT_LEDA
pinMode(VTFT_LEDA, ANALOG); pinMode(VTFT_LEDA, ANALOG);
#endif
#ifdef VTFT_CTRL
pinMode(VTFT_CTRL, ANALOG); pinMode(VTFT_CTRL, ANALOG);
#endif
pinMode(ST7789_RESET, ANALOG); pinMode(ST7789_RESET, ANALOG);
pinMode(ST7789_RS, ANALOG); pinMode(ST7789_RS, ANALOG);
pinMode(ST7789_NSS, ANALOG); pinMode(ST7789_NSS, ANALOG);
-4
View File
@@ -765,11 +765,7 @@ class Screen : public concurrency::OSThread
DebugInfo debugInfo; DebugInfo debugInfo;
/// Display device /// Display device
#ifdef USE_ST7789
ST7789Spi *dispdev;
#else
OLEDDisplay *dispdev; OLEDDisplay *dispdev;
#endif
/// UI helper for rendering to frames and switching between them /// UI helper for rendering to frames and switching between them
OLEDDisplayUi *ui; OLEDDisplayUi *ui;
+6 -5
View File
@@ -121,10 +121,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
// === Screen Title === // === Screen Title ===
const char *headerTitle = titleStr ? titleStr : ""; display->setTextAlignment(TEXT_ALIGN_CENTER);
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle); display->drawString(SCREEN_WIDTH / 2, y, titleStr);
const int titleX = (SCREEN_WIDTH - titleWidth) / 2; if (config.display.heading_bold) {
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold); display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
}
} }
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -514,4 +515,4 @@ std::string sanitizeString(const std::string &input)
} }
} // namespace graphics } // namespace graphics
#endif #endif
+253 -75
View File
@@ -7,7 +7,6 @@
#include "NodeDB.h" #include "NodeDB.h"
#include "UIRenderer.h" #include "UIRenderer.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/EmoteRenderer.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
@@ -35,6 +34,44 @@ static std::vector<std::string> cachedLines;
static std::vector<int> cachedHeights; static std::vector<int> cachedHeights;
static bool manualScrolling = false; static bool manualScrolling = false;
// UTF-8 skip helper
static inline size_t utf8CharLen(uint8_t c)
{
if ((c & 0xE0) == 0xC0)
return 2;
if ((c & 0xF0) == 0xE0)
return 3;
if ((c & 0xF8) == 0xF0)
return 4;
return 1;
}
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
static std::string normalizeEmoji(const std::string &s)
{
std::string out;
for (size_t i = 0; i < s.size();) {
uint8_t c = static_cast<uint8_t>(s[i]);
size_t len = utf8CharLen(c);
if (c == 0xEF && i + 2 < s.size() && (uint8_t)s[i + 1] == 0xB8 && (uint8_t)s[i + 2] == 0x8F) {
i += 3;
continue;
}
// Skip skin tone modifiers
if (c == 0xF0 && i + 3 < s.size() && (uint8_t)s[i + 1] == 0x9F && (uint8_t)s[i + 2] == 0x8F &&
((uint8_t)s[i + 3] >= 0xBB && (uint8_t)s[i + 3] <= 0xBF)) {
i += 4;
continue;
}
out.append(s, i, len);
i += len;
}
return out;
}
// Scroll state (file scope so we can reset on new message) // Scroll state (file scope so we can reset on new message)
float scrollY = 0.0f; float scrollY = 0.0f;
uint32_t lastTime = 0; uint32_t lastTime = 0;
@@ -73,7 +110,102 @@ void scrollDown()
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)
{ {
graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, FONT_HEIGHT_SMALL, emotes, emoteCount); int cursorX = x;
const int fontHeight = FONT_HEIGHT_SMALL;
// Step 1: Find tallest emote in the line
int maxIconHeight = fontHeight;
for (size_t i = 0; i < line.length();) {
bool matched = false;
for (int e = 0; e < emoteCount; ++e) {
size_t emojiLen = strlen(emotes[e].label);
if (line.compare(i, emojiLen, emotes[e].label) == 0) {
if (emotes[e].height > maxIconHeight)
maxIconHeight = emotes[e].height;
i += emojiLen;
matched = true;
break;
}
}
if (!matched) {
i += utf8CharLen(static_cast<uint8_t>(line[i]));
}
}
// Step 2: Baseline alignment
int lineHeight = std::max(fontHeight, maxIconHeight);
int baselineOffset = (lineHeight - fontHeight) / 2;
int fontY = y + baselineOffset;
// Step 3: Render line in segments
size_t i = 0;
bool inBold = false;
while (i < line.length()) {
// Check for ** start/end for faux bold
if (line.compare(i, 2, "**") == 0) {
inBold = !inBold;
i += 2;
continue;
}
// Look ahead for the next emote match
size_t nextEmotePos = std::string::npos;
const Emote *matchedEmote = nullptr;
size_t emojiLen = 0;
for (int e = 0; e < emoteCount; ++e) {
size_t pos = line.find(emotes[e].label, i);
if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) {
nextEmotePos = pos;
matchedEmote = &emotes[e];
emojiLen = strlen(emotes[e].label);
}
}
// Render normal text segment up to the emote or bold toggle
size_t nextControl = std::min(nextEmotePos, line.find("**", i));
if (nextControl == std::string::npos)
nextControl = line.length();
if (nextControl > i) {
std::string textChunk = line.substr(i, nextControl - i);
if (inBold) {
// Faux bold: draw twice, offset by 1px
display->drawString(cursorX + 1, fontY, textChunk.c_str());
}
display->drawString(cursorX, fontY, textChunk.c_str());
#if defined(OLED_UA) || defined(OLED_RU)
cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true);
#else
cursorX += display->getStringWidth(textChunk.c_str());
#endif
i = nextControl;
continue;
}
// Render the emote (if found)
if (matchedEmote && i == nextEmotePos) {
int iconY = y + (lineHeight - matchedEmote->height) / 2;
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
cursorX += matchedEmote->width + 1;
i += emojiLen;
continue;
} else {
// No more emotes — render the rest of the line
std::string remaining = line.substr(i);
if (inBold) {
display->drawString(cursorX + 1, fontY, remaining.c_str());
}
display->drawString(cursorX, fontY, remaining.c_str());
#if defined(OLED_UA) || defined(OLED_RU)
cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true);
#else
cursorX += display->getStringWidth(remaining.c_str());
#endif
break;
}
}
} }
// Reset scroll state when new messages arrive // Reset scroll state when new messages arrive
@@ -245,7 +377,32 @@ static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8)
static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount)
{ {
return graphics::EmoteRenderer::analyzeLine(display, line, 0, emotes, emoteCount).width; std::string normalized = normalizeEmoji(line);
int totalWidth = 0;
size_t i = 0;
while (i < normalized.length()) {
bool matched = false;
for (int e = 0; e < emoteCount; ++e) {
size_t emojiLen = strlen(emotes[e].label);
if (normalized.compare(i, emojiLen, emotes[e].label) == 0) {
totalWidth += emotes[e].width + 1; // +1 spacing
i += emojiLen;
matched = true;
break;
}
}
if (!matched) {
size_t charLen = utf8CharLen(static_cast<uint8_t>(normalized[i]));
#if defined(OLED_UA) || defined(OLED_RU)
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true);
#else
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str());
#endif
i += charLen;
}
}
return totalWidth;
} }
struct MessageBlock { struct MessageBlock {
@@ -260,7 +417,13 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i
return lineTopY + (FONT_HEIGHT_SMALL - 1); return lineTopY + (FONT_HEIGHT_SMALL - 1);
} }
const int tallest = graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes).tallestHeight; int tallest = FONT_HEIGHT_SMALL;
for (int e = 0; e < numEmotes; ++e) {
if (line.find(emotes[e].label) != std::string::npos) {
if (emotes[e].height > tallest)
tallest = emotes[e].height;
}
}
const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest); const int lineHeight = std::max(FONT_HEIGHT_SMALL, tallest);
const int iconTop = lineTopY + (lineHeight - tallest) / 2; const int iconTop = lineTopY + (lineHeight - tallest) / 2;
@@ -373,28 +536,30 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH;
// Title string depending on mode // Title string depending on mode
char titleStr[48]; static char titleBuf[32];
snprintf(titleStr, sizeof(titleStr), "Messages"); const char *titleStr = "Messages";
switch (currentMode) { switch (currentMode) {
case ThreadMode::ALL: case ThreadMode::ALL:
snprintf(titleStr, sizeof(titleStr), "Messages"); titleStr = "Messages";
break; break;
case ThreadMode::CHANNEL: { case ThreadMode::CHANNEL: {
const char *cname = channels.getName(currentChannel); const char *cname = channels.getName(currentChannel);
if (cname && cname[0]) { if (cname && cname[0]) {
snprintf(titleStr, sizeof(titleStr), "#%s", cname); snprintf(titleBuf, sizeof(titleBuf), "#%s", cname);
} else { } else {
snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel); snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel);
} }
titleStr = titleBuf;
break; break;
} }
case ThreadMode::DIRECT: { case ThreadMode::DIRECT: {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
if (node && node->has_user && node->user.short_name[0]) { if (node && node->has_user) {
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name); snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name);
} else { } else {
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer); snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer);
} }
titleStr = titleBuf;
break; break;
} }
} }
@@ -501,50 +666,44 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender);
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest); meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
char senderName[64] = ""; char senderBuf[48] = "";
if (node && node->has_user) { if (node && node->has_user) {
if (node->user.long_name[0]) { // Use long name if present
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1); strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1);
} else if (node->user.short_name[0]) { senderBuf[sizeof(senderBuf) - 1] = '\0';
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1); } else {
} // No long/short name → show NodeID in parentheses
senderName[sizeof(senderName) - 1] = '\0'; snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender);
}
if (!senderName[0]) {
snprintf(senderName, sizeof(senderName), "(%08x)", m.sender);
} }
// If this is *our own* message, override senderName to who the recipient was // If this is *our own* message, override senderBuf to who the recipient was
bool mine = (m.sender == nodeDB->getNodeNum()); bool mine = (m.sender == nodeDB->getNodeNum());
if (mine && node_recipient && node_recipient->has_user) { if (mine && node_recipient && node_recipient->has_user) {
if (node_recipient->user.long_name[0]) { strcpy(senderBuf, node_recipient->user.long_name);
strncpy(senderName, node_recipient->user.long_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0';
} else if (node_recipient->user.short_name[0]) {
strncpy(senderName, node_recipient->user.short_name, sizeof(senderName) - 1);
senderName[sizeof(senderName) - 1] = '\0';
}
}
// If recipient info is missing/empty, prefer a recipient identifier for outbound messages.
if (mine && (!node_recipient || !node_recipient->has_user ||
(!node_recipient->user.long_name[0] && !node_recipient->user.short_name[0]))) {
snprintf(senderName, sizeof(senderName), "(%08x)", m.dest);
} }
// Shrink Sender name if needed // Shrink Sender name if needed
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) - int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @..."); display->getStringWidth(chanType) - display->getStringWidth(" @...");
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
char truncatedSender[64];
graphics::UIRenderer::truncateStringWithEmotes(display, senderName, truncatedSender, sizeof(truncatedSender), availWidth); size_t origLen = strlen(senderBuf);
while (senderBuf[0] && display->getStringWidth(senderBuf) > availWidth) {
senderBuf[strlen(senderBuf) - 1] = '\0';
}
// If we actually truncated, append "..."
if (strlen(senderBuf) < origLen) {
strcat(senderBuf, "...");
}
// Final header line // Final header line
char headerStr[128]; char headerStr[96];
if (mine) { if (mine) {
if (currentMode == ThreadMode::ALL) { if (currentMode == ThreadMode::ALL) {
if (strcmp(chanType, "(DM)") == 0) { if (strcmp(chanType, "(DM)") == 0) {
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender); snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf);
} else { } else {
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType); snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, chanType);
} }
@@ -552,11 +711,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
snprintf(headerStr, sizeof(headerStr), "%s", timeBuf); snprintf(headerStr, sizeof(headerStr), "%s", timeBuf);
} }
} else { } else {
snprintf(headerStr, sizeof(headerStr), chanType[0] ? "%s @%s %s" : "%s @%s", timeBuf, truncatedSender, chanType); snprintf(headerStr, sizeof(headerStr), "%s @%s %s", timeBuf, senderBuf, chanType);
} }
// Push header line // Push header line
allLines.push_back(headerStr); allLines.push_back(std::string(headerStr));
isMine.push_back(mine); isMine.push_back(mine);
isHeader.push_back(true); isHeader.push_back(true);
ackForLine.push_back(m.ackStatus); ackForLine.push_back(m.ackStatus);
@@ -657,8 +816,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
topY = visualTop - BUBBLE_PAD_TOP_HEADER; topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else { } else {
// Body start // Body start
const bool thisLineHasEmote = bool thisLineHasEmote = false;
graphics::EmoteRenderer::analyzeLine(nullptr, cachedLines[b.start].c_str(), 0, emotes, numEmotes).hasEmote; for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
}
}
if (thisLineHasEmote) { if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4; constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE; visualTop -= EMOTE_PADDING_ABOVE;
@@ -687,7 +851,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
for (size_t i = b.start; i <= b.end; ++i) { for (size_t i = b.start; i <= b.end; ++i) {
int w = 0; int w = 0;
if (isHeader[i]) { if (isHeader[i]) {
w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine) if (b.mine)
w += 12; // room for ACK/NACK/relay mark w += 12; // room for ACK/NACK/relay mark
} else { } else {
@@ -743,7 +907,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (lineY > -cachedHeights[i] && lineY < scrollBottom) { if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
if (isHeader[i]) { if (isHeader[i]) {
int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str()); int w = display->getStringWidth(cachedLines[i].c_str());
int headerX; int headerX;
if (isMine[i]) { if (isMine[i]) {
// push header left to avoid overlap with scrollbar // push header left to avoid overlap with scrollbar
@@ -753,8 +917,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} else { } else {
headerX = x + textIndent; headerX = x + textIndent;
} }
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1, display->drawString(headerX, lineY, cachedLines[i].c_str());
false);
// Draw underline just under header text // Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL; int underlineY = lineY + FONT_HEIGHT_SMALL;
@@ -842,7 +1005,11 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
} else { } else {
word += ch; word += ch;
std::string test = line + word; std::string test = line + word;
uint16_t strWidth = graphics::UIRenderer::measureStringWithEmotes(display, test.c_str()); #if defined(OLED_UA) || defined(OLED_RU)
uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
#else
uint16_t strWidth = display->getStringWidth(test.c_str());
#endif
if (strWidth > textWidth) { if (strWidth > textWidth) {
if (!line.empty()) if (!line.empty())
lines.push_back(line); lines.push_back(line);
@@ -871,20 +1038,31 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
std::vector<int> rowHeights; std::vector<int> rowHeights;
rowHeights.reserve(lines.size()); rowHeights.reserve(lines.size());
std::vector<graphics::EmoteRenderer::LineMetrics> lineMetrics;
lineMetrics.reserve(lines.size());
for (const auto &line : lines) {
lineMetrics.push_back(graphics::EmoteRenderer::analyzeLine(nullptr, line, FONT_HEIGHT_SMALL, emotes, numEmotes));
}
for (size_t idx = 0; idx < lines.size(); ++idx) { for (size_t idx = 0; idx < lines.size(); ++idx) {
const auto &line = lines[idx];
const int baseHeight = FONT_HEIGHT_SMALL; const int baseHeight = FONT_HEIGHT_SMALL;
int lineHeight = baseHeight; int lineHeight = baseHeight;
const int tallestEmote = lineMetrics[idx].tallestHeight; // Detect if THIS line or NEXT line contains an emote
const bool hasEmote = lineMetrics[idx].hasEmote; bool hasEmote = false;
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote; int tallestEmote = baseHeight;
for (int i = 0; i < numEmotes; ++i) {
if (line.find(emotes[i].label) != std::string::npos) {
hasEmote = true;
tallestEmote = std::max(tallestEmote, emotes[i].height);
}
}
bool nextHasEmote = false;
if (idx + 1 < lines.size()) {
for (int i = 0; i < numEmotes; ++i) {
if (lines[idx + 1].find(emotes[i].label) != std::string::npos) {
nextHasEmote = true;
break;
}
}
}
if (isHeaderVec[idx]) { if (isHeaderVec[idx]) {
// Header line spacing // Header line spacing
@@ -934,22 +1112,22 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
// Banner logic // Banner logic
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
char longName[64] = "?"; char longName[48] = "?";
if (node && node->has_user) { if (node && node->user.long_name) {
if (node->user.long_name[0]) { strncpy(longName, node->user.long_name, sizeof(longName) - 1);
strncpy(longName, node->user.long_name, sizeof(longName) - 1); longName[sizeof(longName) - 1] = '\0';
longName[sizeof(longName) - 1] = '\0';
} else if (node->user.short_name[0]) {
strncpy(longName, node->user.short_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0';
}
} }
int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20); int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20);
if (availWidth < 0) if (availWidth < 0)
availWidth = 0; availWidth = 0;
char truncatedLongName[64];
graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName), size_t origLen = strlen(longName);
availWidth); while (longName[0] && display->getStringWidth(longName) > availWidth) {
longName[strlen(longName) - 1] = '\0';
}
if (strlen(longName) < origLen) {
strcat(longName, "...");
}
const char *msgRaw = reinterpret_cast<const char *>(packet.decoded.payload.bytes); const char *msgRaw = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
char banner[256]; char banner[256];
@@ -967,8 +1145,8 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
} }
if (isAlert) { if (isAlert) {
if (truncatedLongName[0]) if (longName && longName[0])
snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName); snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
else else
strcpy(banner, "Alert Received"); strcpy(banner, "Alert Received");
} else { } else {
@@ -976,11 +1154,11 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
if (isChannelMuted) if (isChannelMuted)
return; return;
if (truncatedLongName[0]) { if (longName && longName[0]) {
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
strcpy(banner, "New Message"); strcpy(banner, "New Message");
} else { } else {
snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName); snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
} }
} else } else
strcpy(banner, "New Message"); strcpy(banner, "New Message");
@@ -1043,4 +1221,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet)
} // namespace MessageRenderer } // namespace MessageRenderer
} // namespace graphics } // namespace graphics
#endif #endif
+54 -47
View File
@@ -79,15 +79,13 @@ void scrollDown()
// Utility Functions // Utility Functions
// ============================= // =============================
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth) const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
{ {
(void)display; static char nodeName[25]; // single static buffer we return
(void)columnWidth; nodeName[0] = '\0';
auto fallbackId = [&] { auto writeFallbackId = [&] {
char id[12]; std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
std::snprintf(id, sizeof(id), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
return std::string(id);
}; };
// 1) Choose target candidate (long vs short) only if present // 1) Choose target candidate (long vs short) only if present
@@ -96,10 +94,42 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name; raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
} }
// 2) Preserve UTF-8 names so emotes can be detected and rendered. // 2) Sanitize (empty if raw is null/empty)
std::string nodeName = (raw && *raw) ? std::string(raw) : std::string{}; std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
if (nodeName.empty()) {
nodeName = fallbackId(); // 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
writeFallbackId();
} else {
// %.*s ensures null-termination and safe truncation to buffer size - 1
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
}
// 4) Width-based truncation + ellipsis (long-name mode only)
if (config.display.use_long_node_name && display) {
int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
if (availWidth < 0)
availWidth = 0;
const size_t beforeLen = std::strlen(nodeName);
// Trim from the end until it fits or is empty
size_t len = beforeLen;
while (len && display->getStringWidth(nodeName) > availWidth) {
nodeName[--len] = '\0';
}
// If truncated, append "..." (respect buffer size)
if (len < beforeLen) {
// Make sure there's room for "..." and '\0'
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
const size_t needed = 3; // "..."
if (len > capForText - needed) {
len = capForText - needed;
nodeName[len] = '\0';
}
std::strcat(nodeName, "...");
}
} }
return nodeName; return nodeName;
@@ -133,15 +163,6 @@ const char *getCurrentModeTitle_Location(int screenWidth)
} }
} }
static int getNodeNameMaxWidth(int columnWidth, int baseWidth)
{
if (!config.display.use_long_node_name)
return baseWidth;
const int legacyLongNameWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
return std::max(0, std::min(baseWidth, legacyLongNameWidth));
}
// Use dynamic timing based on mode // Use dynamic timing based on mode
unsigned long getModeCycleIntervalMs() unsigned long getModeCycleIntervalMs()
{ {
@@ -184,13 +205,10 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
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)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int nameMaxWidth = columnWidth - 25;
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); const char *nodeName = getSafeNodeName(display, node, columnWidth);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char timeStr[10]; char timeStr[10];
@@ -210,7 +228,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
@@ -237,22 +255,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25); int nameMaxWidth = columnWidth - 25;
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); const char *nodeName = getSafeNodeName(display, node, columnWidth);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
@@ -297,13 +312,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = int nameMaxWidth =
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
: (isLeftCol ? 20 : 22)));
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); const char *nodeName = getSafeNodeName(display, node, columnWidth);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char distStr[10] = ""; char distStr[10] = "";
@@ -357,7 +368,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
@@ -403,18 +414,14 @@ 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 = int nameMaxWidth =
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
: (isLeftCol ? 20 : 22)));
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3); const char *nodeName = getSafeNodeName(display, node, columnWidth);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0; bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
UIRenderer::drawStringWithEmotes(display, nameX, y, nodeName, FONT_HEIGHT_SMALL, 1, false); display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
if (node->is_favorite) { if (node->is_favorite) {
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
@@ -821,4 +828,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields
} // namespace NodeListRenderer } // namespace NodeListRenderer
} // namespace graphics } // namespace graphics
#endif #endif
+1 -2
View File
@@ -4,7 +4,6 @@
#include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h"
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
#include <OLEDDisplayUi.h> #include <OLEDDisplayUi.h>
#include <string>
namespace graphics namespace graphics
{ {
@@ -57,7 +56,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
// Utility functions // Utility functions
const char *getCurrentModeTitle_Nodes(int screenWidth); const char *getCurrentModeTitle_Nodes(int screenWidth);
const char *getCurrentModeTitle_Location(int screenWidth); const char *getCurrentModeTitle_Location(int screenWidth);
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth); 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 // Scrolling controls
+21 -59
View File
@@ -4,7 +4,6 @@
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "NotificationRenderer.h" #include "NotificationRenderer.h"
#include "UIRenderer.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/images.h" #include "graphics/images.h"
@@ -44,7 +43,7 @@ InputEvent NotificationRenderer::inEvent;
int8_t NotificationRenderer::curSelected = 0; int8_t NotificationRenderer::curSelected = 0;
char NotificationRenderer::alertBannerMessage[256] = {0}; char NotificationRenderer::alertBannerMessage[256] = {0};
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are selectable options uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
const char **NotificationRenderer::optionsArrayPtr = nullptr; const char **NotificationRenderer::optionsArrayPtr = nullptr;
const int *NotificationRenderer::optionsEnumPtr = nullptr; const int *NotificationRenderer::optionsEnumPtr = nullptr;
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL; std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
@@ -96,7 +95,7 @@ void NotificationRenderer::resetBanner()
inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.inputEvent = INPUT_BROKER_NONE;
inEvent.kbchar = 0; inEvent.kbchar = 0;
curSelected = 0; curSelected = 0;
alertBannerOptions = 0; // last x lines are selectable options alertBannerOptions = 0; // last x lines are seelctable options
optionsArrayPtr = nullptr; optionsArrayPtr = nullptr;
optionsEnumPtr = nullptr; optionsEnumPtr = nullptr;
alertBannerCallback = NULL; alertBannerCallback = NULL;
@@ -300,7 +299,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
for (int i = 0; i < lineCount; i++) { for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i]; linePointers[i] = lineStarts[i];
} }
char scratchLineBuffer[visibleTotalLines - lineCount][64]; char scratchLineBuffer[visibleTotalLines - lineCount][40];
uint8_t firstOptionToShow = 0; uint8_t firstOptionToShow = 0;
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) { if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
@@ -313,47 +312,28 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
int scratchLineNum = 0; int scratchLineNum = 0;
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char tempName[48] = {0}; char temp_name[16] = {0};
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1); if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
if (node && node->has_user) { std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
const char *rawName = nullptr; strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
if (node->user.long_name[0]) {
rawName = node->user.long_name;
} else if (node->user.short_name[0]) {
rawName = node->user.short_name;
}
if (rawName) {
const int arrowWidth = (currentResolution == ScreenResolution::High)
? UIRenderer::measureStringWithEmotes(display, "> <")
: UIRenderer::measureStringWithEmotes(display, "><");
const int maxTextWidth = std::max(0, display->getWidth() - 28 - arrowWidth);
UIRenderer::truncateStringWithEmotes(display, rawName, tempName, sizeof(tempName), maxTextWidth);
}
} else { } else {
snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0)); snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
}
if (!tempName[0]) {
snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0));
} }
if (i == curSelected) { if (i == curSelected) {
selectedNodenum = node ? node->num : 0; selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (currentResolution == ScreenResolution::High) { if (currentResolution == ScreenResolution::High) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3); strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 3); strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1);
strncpy(scratchLineBuffer[scratchLineNum] + used, " <", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1);
} else { } else {
strncpy(scratchLineBuffer[scratchLineNum], ">", 2); strncpy(scratchLineBuffer[scratchLineNum], ">", 2);
strncpy(scratchLineBuffer[scratchLineNum] + 1, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 2); strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2);
const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1);
strncpy(scratchLineBuffer[scratchLineNum] + used, "<", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1);
} }
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; scratchLineBuffer[scratchLineNum][39] = '\0';
} else { } else {
strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1); strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0'; scratchLineBuffer[scratchLineNum][39] = '\0';
} }
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
} }
@@ -521,13 +501,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
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]);
if (current_notification_type == notificationTypeEnum::node_picker) { lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
char measureBuffer[64] = {0};
strncpy(measureBuffer, lines[lineCount], std::min<size_t>(lineLengths[lineCount], sizeof(measureBuffer) - 1));
lineWidths[lineCount] = UIRenderer::measureStringWithEmotes(display, measureBuffer);
} else {
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
}
// Consider extra width for signal bars on lines that contain "Signal:" // Consider extra width for signal bars on lines that contain "Signal:"
uint16_t potentialWidth = lineWidths[lineCount]; uint16_t potentialWidth = lineWidths[lineCount];
@@ -633,11 +607,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
display->setColor(BLACK); display->setColor(BLACK);
int yOffset = 3; int yOffset = 3;
if (current_notification_type == notificationTypeEnum::node_picker) { display->drawString(textX, lineY - yOffset, lineBuffer);
UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(textX, lineY - yOffset, lineBuffer);
}
display->setColor(WHITE); display->setColor(WHITE);
lineY += (effectiveLineHeight - 2 - background_yOffset); lineY += (effectiveLineHeight - 2 - background_yOffset);
} else { } else {
@@ -656,11 +626,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
int totalWidth = textWidth + barsWidth; int totalWidth = textWidth + barsWidth;
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2; int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
if (current_notification_type == notificationTypeEnum::node_picker) { display->drawString(groupStartX, lineY, lineBuffer);
UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(groupStartX, lineY, lineBuffer);
}
int baseX = groupStartX + textWidth + gap; int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1; int baseY = lineY + effectiveLineHeight - 1;
@@ -676,11 +642,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
} }
} }
} else { } else {
if (current_notification_type == notificationTypeEnum::node_picker) { display->drawString(textX, lineY, lineBuffer);
UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(textX, lineY, lineBuffer);
}
} }
lineY += (effectiveLineHeight); lineY += (effectiveLineHeight);
} }
@@ -819,4 +781,4 @@ void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title,
} }
} // namespace graphics } // namespace graphics
#endif #endif
+1 -1
View File
@@ -22,7 +22,7 @@ class NotificationRenderer
static uint32_t alertBannerUntil; // 0 is a special case meaning forever static uint32_t alertBannerUntil; // 0 is a special case meaning forever
static const char **optionsArrayPtr; static const char **optionsArrayPtr;
static const int *optionsEnumPtr; static const int *optionsEnumPtr;
static uint8_t alertBannerOptions; // last x lines are selectable options static uint8_t alertBannerOptions; // last x lines are seelctable options
static std::function<void(int)> alertBannerCallback; static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits; static uint32_t numDigits;
static uint32_t currentNumber; static uint32_t currentNumber;
+51 -79
View File
@@ -8,7 +8,6 @@
#include "UIRenderer.h" #include "UIRenderer.h"
#include "airtime.h" #include "airtime.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "graphics/EmoteRenderer.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"
@@ -314,8 +313,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
#endif #endif
currentFavoriteNodeNum = node->num; currentFavoriteNodeNum = node->num;
// === Create the shortName and title string === // === Create the shortName and title string ===
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node"; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
char titlestr[40]; char titlestr[32] = {0};
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName); snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
// === Draw battery/time/mail header (common across screens) === // === Draw battery/time/mail header (common across screens) ===
@@ -329,6 +328,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
// List of available macro Y positions in order, from top to bottom. // List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) === // === 1. Long Name (always try to show first) ===
const char *username; const char *username;
if (currentResolution == ScreenResolution::UltraLow) { if (currentResolution == ScreenResolution::UltraLow) {
@@ -338,8 +338,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
} }
if (username) { if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// Print node's long name (e.g. "Backpack Node") // Print node's long name (e.g. "Backpack Node")
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false); display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
} }
// === 2. Signal and Hops (combined on one line, if available) === // === 2. Signal and Hops (combined on one line, if available) ===
@@ -801,12 +802,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Node Identity === // === Node Identity ===
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
const char *shortName = owner.short_name ? owner.short_name : ""; char shortnameble[35];
snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
// === ShortName Centered === // === ShortName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, shortName); textWidth = display->getStringWidth(shortnameble);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, false); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
#else #else
if (powerStatus->getHasBattery()) { if (powerStatus->getHasBattery()) {
char batStr[20]; char batStr[20];
@@ -901,36 +904,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5; int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : ""; std::string longNameStr;
const char *shortName = owner.short_name ? owner.short_name : "";
char combinedName[96]; if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
if (longName[0] && shortName[0]) { longNameStr = sanitizeString(ourNode->user.long_name);
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortName);
} else if (longName[0]) {
strncpy(combinedName, longName, sizeof(combinedName) - 1);
combinedName[sizeof(combinedName) - 1] = '\0';
} else {
strncpy(combinedName, shortName, sizeof(combinedName) - 1);
combinedName[sizeof(combinedName) - 1] = '\0';
} }
if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) { char shortnameble[35];
textWidth = UIRenderer::measureStringWithEmotes(display, combinedName); snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
char combinedName[50];
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble);
if (SCREEN_WIDTH - (display->getStringWidth(combinedName)) > 10) {
size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
combinedName[len - 3] = '\0'; // Remove the last three characters
}
textWidth = display->getStringWidth(combinedName);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes( display->drawString(
display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName);
combinedName, FONT_HEIGHT_SMALL, 1, false);
} else { } else {
// === LongName Centered === // === LongName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, longName); textWidth = display->getStringWidth(longNameStr.c_str());
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1, display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
false);
// === ShortName Centered === // === ShortName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, shortName); textWidth = display->getStringWidth(shortnameble);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], shortName, FONT_HEIGHT_SMALL, 1, display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
false);
} }
#endif #endif
graphics::drawCommonFooter(display, x, y); graphics::drawCommonFooter(display, x, y);
@@ -1042,12 +1045,12 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused"; const char *pauseText = "Screen Paused";
const char *idText = owner.short_name; const char *idText = owner.short_name;
const bool useId = (idText && idText[0]); const bool useId = haveGlyphs(idText);
constexpr uint8_t padding = 2; constexpr uint8_t padding = 2;
constexpr uint8_t dividerGap = 1; constexpr uint8_t dividerGap = 1;
// Text widths // Text widths
const uint16_t idTextWidth = useId ? UIRenderer::measureStringWithEmotes(display, idText) : 0; const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
@@ -1072,7 +1075,7 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
// Draw: text // Draw: text
if (useId) if (useId)
UIRenderer::drawStringWithEmotes(display, idTextLeft, idTextTop, idText, FONT_HEIGHT_SMALL, 1, false); display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText); display->drawString(pauseTextLeft, pauseTextTop, pauseText);
display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold
@@ -1105,16 +1108,11 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
display->drawString(msgX, msgY, upperMsg); display->drawString(msgX, msgY, upperMsg);
} }
// Draw version and short name in bottom middle // Draw version and short name in bottom middle
char footer[64]; char buf[25];
if (owner.short_name && owner.short_name[0]) { snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT),
snprintf(footer, sizeof(footer), "%s %s", xstr(APP_VERSION_SHORT), owner.short_name); graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
} else {
snprintf(footer, sizeof(footer), "%s", xstr(APP_VERSION_SHORT)); display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf);
}
int footerW = UIRenderer::measureStringWithEmotes(display, footer);
int footerX = x + ((SCREEN_WIDTH - footerW) / 2);
UIRenderer::drawStringWithEmotes(display, footerX, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, footer, FONT_HEIGHT_SMALL, 1,
false);
screen->forceDisplay(); screen->forceDisplay();
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
@@ -1132,15 +1130,12 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
display->drawString(x + 0, y + 0, upperMsg); display->drawString(x + 0, y + 0, upperMsg);
// Draw version and short name in upper right // Draw version and short name in upper right
const char *version = xstr(APP_VERSION_SHORT); char buf[25];
int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT),
display->drawString(versionX, y + 0, version); graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
if (owner.short_name && owner.short_name[0]) {
const char *shortName = owner.short_name; display->setTextAlignment(TEXT_ALIGN_RIGHT);
int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName); display->drawString(x + SCREEN_WIDTH, y + 0, buf);
int shortNameX = x + SCREEN_WIDTH - shortNameW;
UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false);
}
screen->forceDisplay(); screen->forceDisplay();
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
@@ -1370,15 +1365,11 @@ void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, O
display->drawString(x + 0, y + 0, upperMsg); display->drawString(x + 0, y + 0, upperMsg);
// Draw version and shortname in upper right // Draw version and shortname in upper right
const char *version = xstr(APP_VERSION_SHORT); char buf[25];
int versionX = x + SCREEN_WIDTH - display->getStringWidth(version); snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : "");
display->drawString(versionX, y + 0, version);
if (owner.short_name && owner.short_name[0]) { display->setTextAlignment(TEXT_ALIGN_RIGHT);
const char *shortName = owner.short_name; display->drawString(x + SCREEN_WIDTH, y + 0, buf);
int shortNameW = UIRenderer::measureStringWithEmotes(display, shortName);
int shortNameX = x + SCREEN_WIDTH - shortNameW;
UIRenderer::drawStringWithEmotes(display, shortNameX, y + FONT_HEIGHT_SMALL, shortName, FONT_HEIGHT_SMALL, 1, false);
}
screen->forceDisplay(); screen->forceDisplay();
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
@@ -1567,25 +1558,6 @@ std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t mi
return uptime; return uptime;
} }
int UIRenderer::measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing)
{
return graphics::EmoteRenderer::measureStringWithEmotes(display, line, graphics::emotes, graphics::numEmotes, emoteSpacing);
}
size_t UIRenderer::truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth,
const char *ellipsis, int emoteSpacing)
{
return graphics::EmoteRenderer::truncateToWidth(display, line, out, outSize, maxWidth, ellipsis, graphics::emotes,
graphics::numEmotes, emoteSpacing);
}
void UIRenderer::drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing,
bool fauxBold)
{
graphics::EmoteRenderer::drawStringWithEmotes(display, x, y, line, fontHeight, graphics::emotes, graphics::numEmotes,
emoteSpacing, fauxBold);
}
} // namespace graphics } // namespace graphics
#endif // HAS_SCREEN #endif // HAS_SCREEN
-23
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "NodeDB.h" #include "NodeDB.h"
#include "graphics/EmoteRenderer.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
@@ -81,28 +80,6 @@ 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);
// Shared BaseUI emote helpers.
static int measureStringWithEmotes(OLEDDisplay *display, const char *line, int emoteSpacing = 1);
static inline int measureStringWithEmotes(OLEDDisplay *display, const std::string &line, int emoteSpacing = 1)
{
return measureStringWithEmotes(display, line.c_str(), emoteSpacing);
}
static size_t truncateStringWithEmotes(OLEDDisplay *display, const char *line, char *out, size_t outSize, int maxWidth,
const char *ellipsis = "...", int emoteSpacing = 1);
static inline std::string truncateStringWithEmotes(OLEDDisplay *display, const std::string &line, int maxWidth,
const std::string &ellipsis = "...", int emoteSpacing = 1)
{
return graphics::EmoteRenderer::truncateToWidth(display, line, maxWidth, ellipsis, graphics::emotes, graphics::numEmotes,
emoteSpacing);
}
static void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const char *line, int fontHeight, int emoteSpacing = 1,
bool fauxBold = true);
static inline void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, int fontHeight,
int emoteSpacing = 1, bool fauxBold = true)
{
drawStringWithEmotes(display, x, y, line.c_str(), fontHeight, emoteSpacing, fauxBold);
}
// Check if the display can render a string (detect special chars; emoji) // 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
@@ -37,8 +37,8 @@ class DEPG0213BNS800 : public SSD16XX
void configWaveform() override; void configWaveform() override;
void configUpdateSequence() override; void configUpdateSequence() override;
void detachFromUpdate() override; void detachFromUpdate() override;
void finalizeUpdate() override; // Only overridden for a slight optimization void finalizeUpdate() override; // Only overriden for a slight optimization
}; };
} // namespace NicheGraphics::Drivers } // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -35,8 +35,8 @@ class DEPG0290BNS800 : public SSD16XX
void configWaveform() override; void configWaveform() override;
void configUpdateSequence() override; void configUpdateSequence() override;
void detachFromUpdate() override; void detachFromUpdate() override;
void finalizeUpdate() override; // Only overridden for a slight optimization void finalizeUpdate() override; // Only overriden for a slight optimization
}; };
} // namespace NicheGraphics::Drivers } // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -1,178 +0,0 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./GDEW0102T4.h"
#include <cstring>
using namespace NicheGraphics::Drivers;
// LUTs from GxEPD2_102.cpp (GDEW0102T4 / UC8175).
static const uint8_t LUT_W_FULL[] = {
0x60, 0x5A, 0x5A, 0x00, 0x00, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
static const uint8_t LUT_B_FULL[] = {
0x90, 0x5A, 0x5A, 0x00, 0x00, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
static const uint8_t LUT_W_FAST[] = {
0x60, 0x01, 0x01, 0x00, 0x00, 0x01, //
0x80, 0x12, 0x00, 0x00, 0x00, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
static const uint8_t LUT_B_FAST[] = {
0x90, 0x01, 0x01, 0x00, 0x00, 0x01, //
0x40, 0x14, 0x00, 0x00, 0x00, 0x01, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
};
GDEW0102T4::GDEW0102T4() : UC8175(width, height, supported) {}
void GDEW0102T4::setFastConfig(FastConfig cfg)
{
// Clamp out only clearly invalid PLL settings.
if (cfg.reg30 < 0x05)
cfg.reg30 = 0x05;
fastConfig = cfg;
}
GDEW0102T4::FastConfig GDEW0102T4::getFastConfig() const
{
return fastConfig;
}
void GDEW0102T4::configCommon()
{
// Init path aligned with GxEPD2_GDEW0102T4 (UC8175 family).
sendCommand(0xD2);
sendData(0x3F);
sendCommand(0x00);
sendData(0x6F);
sendCommand(0x01);
sendData(0x03);
sendData(0x00);
sendData(0x2B);
sendData(0x2B);
sendCommand(0x06);
sendData(0x3F);
sendCommand(0x2A);
sendData(0x00);
sendData(0x00);
sendCommand(0x30); // PLL / drive clock
sendData(0x13);
sendCommand(0x50); // Last border/data interval; subtle but can affect artifacts
sendData(0x57);
sendCommand(0x60);
sendData(0x22);
sendCommand(0x61);
sendData(width);
sendData(height);
sendCommand(0x82); // VCOM DC setting
sendData(0x12);
sendCommand(0xE3);
sendData(0x33);
}
void GDEW0102T4::configFull()
{
sendCommand(0x23);
sendData(LUT_W_FULL, sizeof(LUT_W_FULL));
sendCommand(0x24);
sendData(LUT_B_FULL, sizeof(LUT_B_FULL));
powerOn();
}
void GDEW0102T4::configFast()
{
uint8_t lutW[sizeof(LUT_W_FAST)];
uint8_t lutB[sizeof(LUT_B_FAST)];
memcpy(lutW, LUT_W_FAST, sizeof(LUT_W_FAST));
memcpy(lutB, LUT_B_FAST, sizeof(LUT_B_FAST));
// Second stage duration bytes are the main "darkness vs ghosting" control for this panel.
lutW[7] = fastConfig.lutW2;
lutB[7] = fastConfig.lutB2;
sendCommand(0x30);
sendData(fastConfig.reg30);
sendCommand(0x50);
sendData(fastConfig.reg50);
sendCommand(0x82);
sendData(fastConfig.reg82);
sendCommand(0x23);
sendData(lutW, sizeof(lutW));
sendCommand(0x24);
sendData(lutB, sizeof(lutB));
powerOn();
}
void GDEW0102T4::writeOldImage()
{
// On this panel, FULL refresh is most reliable when "old image" is all white.
if (updateType == FULL) {
sendCommand(0x10);
// Use buffered writes of 0xFF to avoid per-byte SPI transactions.
const uint16_t chunkSize = 64;
uint8_t ffBuf[chunkSize];
memset(ffBuf, 0xFF, sizeof(ffBuf));
uint32_t remaining = bufferSize;
while (remaining > 0) {
uint16_t toSend = remaining > chunkSize ? chunkSize : static_cast<uint16_t>(remaining);
sendData(ffBuf, toSend);
remaining -= toSend;
}
return;
}
// FAST refresh uses differential data (previous frame as old image).
if (previousBuffer) {
writeImage(0x10, previousBuffer);
} else {
writeImage(0x10, buffer);
}
}
void GDEW0102T4::finalizeUpdate()
{
// Keep panel out of deep-sleep between updates for better reliability of repeated FAST refresh.
powerOff();
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
@@ -1,55 +0,0 @@
/*
E-Ink display driver
- GDEW0102T4
- Controller: UC8175
- Size: 1.02 inch
- Resolution: 80px x 128px
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./UC8175.h"
namespace NicheGraphics::Drivers
{
class GDEW0102T4 : public UC8175
{
private:
static constexpr uint16_t width = 80;
static constexpr uint16_t height = 128;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
struct FastConfig {
uint8_t reg30;
uint8_t reg50;
uint8_t reg82;
uint8_t lutW2;
uint8_t lutB2;
};
GDEW0102T4();
void setFastConfig(FastConfig cfg);
FastConfig getFastConfig() const;
protected:
void configCommon() override;
void configFull() override;
void configFast() override;
void writeOldImage() override;
void finalizeUpdate() override;
private:
FastConfig fastConfig = {0x13, 0xF2, 0x12, 0x0E, 0x14};
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+1 -1
View File
@@ -19,7 +19,7 @@ void setupNicheGraphics()
SPIClass *hspi = new SPIClass(HSPI); SPIClass *hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
// Setup EInk driver // Setup Enk driver
Drivers::EInk *driver = new Drivers::DEPG0290BNS800; Drivers::EInk *driver = new Drivers::DEPG0290BNS800;
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY); driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY);
-203
View File
@@ -1,203 +0,0 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./UC8175.h"
#include <cstring>
#include "SPILock.h"
using namespace NicheGraphics::Drivers;
UC8175::UC8175(uint16_t width, uint16_t height, UpdateTypes supported) : EInk(width, height, supported)
{
bufferRowSize = ((width - 1) / 8) + 1;
bufferSize = bufferRowSize * height;
}
void UC8175::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
{
this->spi = spi;
this->pin_dc = pin_dc;
this->pin_cs = pin_cs;
this->pin_busy = pin_busy;
this->pin_rst = pin_rst;
pinMode(pin_dc, OUTPUT);
pinMode(pin_cs, OUTPUT);
pinMode(pin_busy, INPUT);
// Reset is active LOW, hold HIGH when idle.
if (pin_rst != (uint8_t)-1) {
pinMode(pin_rst, OUTPUT);
digitalWrite(pin_rst, HIGH);
}
if (!previousBuffer) {
previousBuffer = new uint8_t[bufferSize];
if (previousBuffer)
memset(previousBuffer, 0xFF, bufferSize);
}
}
void UC8175::update(uint8_t *imageData, UpdateTypes type)
{
buffer = imageData;
updateType = (type == UpdateTypes::UNSPECIFIED) ? UpdateTypes::FULL : type;
if (updateType == FAST && hasPreviousBuffer && previousBuffer && memcmp(previousBuffer, buffer, bufferSize) == 0)
return;
reset();
configCommon();
if (updateType == FAST)
configFast();
else
configFull();
writeOldImage();
writeNewImage();
sendCommand(0x12); // Display refresh.
if (previousBuffer) {
memcpy(previousBuffer, buffer, bufferSize);
hasPreviousBuffer = true;
}
detachFromUpdate();
}
void UC8175::wait(uint32_t timeoutMs)
{
if (failed)
return;
uint32_t started = millis();
while (digitalRead(pin_busy) == BUSY_ACTIVE) {
if ((millis() - started) > timeoutMs) {
failed = true;
break;
}
yield();
}
}
void UC8175::reset()
{
if (pin_rst != (uint8_t)-1) {
digitalWrite(pin_rst, LOW);
delay(20);
digitalWrite(pin_rst, HIGH);
delay(20);
} else {
sendCommand(0x12); // Software reset.
delay(10);
}
wait(3000);
}
void UC8175::sendCommand(uint8_t command)
{
if (failed)
return;
spiLock->lock();
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, LOW);
digitalWrite(pin_cs, LOW);
spi->transfer(command);
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
spiLock->unlock();
}
void UC8175::sendData(uint8_t data)
{
sendData(&data, 1);
}
void UC8175::sendData(const uint8_t *data, uint32_t size)
{
if (failed)
return;
spiLock->lock();
spi->beginTransaction(spiSettings);
digitalWrite(pin_dc, HIGH);
digitalWrite(pin_cs, LOW);
#if defined(ARCH_ESP32)
spi->transferBytes(data, NULL, size);
#elif defined(ARCH_NRF52)
spi->transfer(data, NULL, size);
#else
for (uint32_t i = 0; i < size; ++i)
spi->transfer(data[i]);
#endif
digitalWrite(pin_cs, HIGH);
digitalWrite(pin_dc, HIGH);
spi->endTransaction();
spiLock->unlock();
}
void UC8175::powerOn()
{
sendCommand(0x04);
wait(2000);
}
void UC8175::powerOff()
{
sendCommand(0x02); // Power off.
wait(1500);
}
void UC8175::writeImage(uint8_t command, const uint8_t *image)
{
sendCommand(command);
sendData(image, bufferSize);
}
void UC8175::writeOldImage()
{
if (updateType == FAST && previousBuffer)
writeImage(0x10, previousBuffer);
else
writeImage(0x10, buffer);
}
void UC8175::writeNewImage()
{
writeImage(0x13, buffer);
}
void UC8175::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 400);
case FULL:
default:
return beginPolling(100, 2000);
}
}
bool UC8175::isUpdateDone()
{
return digitalRead(pin_busy) != BUSY_ACTIVE;
}
void UC8175::finalizeUpdate()
{
powerOff();
if (pin_rst != (uint8_t)-1) {
sendCommand(0x07); // Deep sleep.
sendData(0xA5);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
-62
View File
@@ -1,62 +0,0 @@
// E-Ink base class for displays based on UC8175 / UC8176 style controller ICs.
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./EInk.h"
namespace NicheGraphics::Drivers
{
class UC8175 : public EInk
{
public:
UC8175(uint16_t width, uint16_t height, UpdateTypes supported);
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) override;
void update(uint8_t *imageData, UpdateTypes type) override;
protected:
virtual void wait(uint32_t timeoutMs = 1000);
virtual void reset();
virtual void sendCommand(uint8_t command);
virtual void sendData(uint8_t data);
virtual void sendData(const uint8_t *data, uint32_t size);
virtual void configCommon() = 0; // Always run
virtual void configFull() = 0; // Run when updateType == FULL
virtual void configFast() = 0; // Run when updateType == FAST
virtual void powerOn();
virtual void powerOff();
virtual void writeOldImage();
virtual void writeNewImage();
virtual void writeImage(uint8_t command, const uint8_t *image);
virtual void detachFromUpdate();
virtual bool isUpdateDone() override;
virtual void finalizeUpdate() override;
protected:
static constexpr uint8_t BUSY_ACTIVE = LOW;
uint16_t bufferRowSize = 0;
uint32_t bufferSize = 0;
uint8_t *buffer = nullptr;
uint8_t *previousBuffer = nullptr;
bool hasPreviousBuffer = false;
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
uint8_t pin_dc = (uint8_t)-1;
uint8_t pin_cs = (uint8_t)-1;
uint8_t pin_busy = (uint8_t)-1;
uint8_t pin_rst = (uint8_t)-1;
SPIClass *spi = nullptr;
SPISettings spiSettings = SPISettings(8000000, MSBFIRST, SPI_MODE0);
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+2 -2
View File
@@ -3,7 +3,7 @@
/* /*
Base class for InkHUD applets Base class for InkHUD applets
Must be overridden Must be overriden
An applet is one "program" which may show info on the display. An applet is one "program" which may show info on the display.
@@ -208,4 +208,4 @@ class Applet : public GFX
}; // namespace NicheGraphics::InkHUD }; // namespace NicheGraphics::InkHUD
#endif #endif
@@ -525,7 +525,7 @@ void InkHUD::MapApplet::calculateAllMarkers()
} }
// Determine the conversion factor between metres, and pixels on screen // Determine the conversion factor between metres, and pixels on screen
// May be overridden by derived applet, if custom scale required (fixed map size?) // May be overriden by derived applet, if custom scale required (fixed map size?)
void InkHUD::MapApplet::calculateMapScale() void InkHUD::MapApplet::calculateMapScale()
{ {
// Aspect ratio of map and screen // Aspect ratio of map and screen
@@ -555,4 +555,4 @@ void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
drawLine(x0, y1, x1, y0, BLACK); drawLine(x0, y1, x1, y0, BLACK);
} }
#endif #endif
@@ -120,26 +120,9 @@ void InkHUD::NodeListApplet::onRender(bool full)
// Draw the main node list // Draw the main node list
// ======================== // ========================
// Leave a small gutter between long-name text and right-side card content // Imaginary vertical line dividing left-side and right-side info
constexpr uint8_t rightContentGap = 2; // Long-name will crop here
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
// Truncate with trailing "...", sized using the current font.
auto ellipsizeToWidth = [this](std::string text, uint16_t maxWidth) {
constexpr const char *ellipsis = "...";
const uint16_t ellipsisW = getTextWidth(ellipsis);
uint16_t textW = getTextWidth(text);
if (maxWidth == 0)
return std::string();
if (textW <= maxWidth)
return text;
if (ellipsisW > maxWidth)
return std::string();
while (!text.empty() && (textW + ellipsisW > maxWidth)) {
text.pop_back();
textW = getTextWidth(text);
}
return text + ellipsis;
};
// Y value (top) of the current card. Increases as we draw. // Y value (top) of the current card. Increases as we draw.
uint16_t cardTopY = headerDivY + padDivH; uint16_t cardTopY = headerDivY + padDivH;
@@ -158,7 +141,7 @@ void InkHUD::NodeListApplet::onRender(bool full)
SignalStrength &signal = card->signal; SignalStrength &signal = card->signal;
std::string longName; // handled below std::string longName; // handled below
std::string shortName; // handled below std::string shortName; // handled below
std::string distance; // handled below std::string distance; // handled below;
const uint8_t &hopsAway = card->hopsAway; const uint8_t &hopsAway = card->hopsAway;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
@@ -202,49 +185,41 @@ void InkHUD::NodeListApplet::onRender(bool full)
setFont(fontMedium); setFont(fontMedium);
printAt(0, lineAY, shortName, LEFT, MIDDLE); printAt(0, lineAY, shortName, LEFT, MIDDLE);
// Right-side labels and long name are rendered in small font. // Print the distance
setFont(fontSmall); setFont(fontSmall);
uint16_t rightContentW = 0; printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
// Bottom row right: distance. // If we have a direct connection to the node, draw the signal indicator
if (!distance.empty()) {
rightContentW = std::max(rightContentW, getTextWidth(distance));
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
}
// Top row right: direct-link signal only.
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) { if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
uint16_t signalW = getTextWidth("Xkm"); // Indicator width tuned to a short right-side label uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
uint16_t signalH = fontMedium.lineHeight() * 0.75; uint16_t signalH = fontMedium.lineHeight() * 0.75;
int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75); int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75);
int16_t signalX = width() - signalW; int16_t signalX = width() - signalW;
rightContentW = std::max(rightContentW, signalW);
drawSignalIndicator(signalX, signalY, signalW, signalH, signal); drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
} else if (hopsAway != CardInfo::HOPS_UNKNOWN) { }
std::string hopString = to_string(hopsAway) + (hopsAway == 1 ? " Hop" : " Hops"); // Otherwise, print "hops away" info, if available
rightContentW = std::max(rightContentW, getTextWidth(hopString)); else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
std::string hopString = to_string(node->hops_away);
hopString += " Hop";
if (node->hops_away != 1)
hopString += "s"; // Append s for "Hops", rather than "Hop"
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE); printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
} }
// Give long names as much room as possible while still avoiding right side signal and hop space // Print the long name, cropping to prevent overflow onto the right-side info
const uint16_t longNameMaxW = setCrop(0, 0, dividerX - 1, height());
(rightContentW + rightContentGap < width()) ? (width() - rightContentW - rightContentGap) : 0; printAt(0, lineBY, longName, LEFT, MIDDLE);
const std::string longNameShown = ellipsizeToWidth(longName, longNameMaxW);
// Safety crop // GFX effect: "hatch" the right edge of longName area
setCrop(0, cardTopY, longNameMaxW, cardH); // If a longName has been cropped, it will appear to fade out,
printAt(0, lineBY, longNameShown, LEFT, MIDDLE); // creating a soft barrier with the right-side info
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
resetCrop(); const int16_t hatchWidth = fontSmall.lineHeight();
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
// Draw separator between cards
const int16_t separatorY = cardTopY + cardH - 1;
if (separatorY < height() - 1 && (card + 1) != cards.end()) {
for (int16_t xSep = 0; xSep < width(); xSep += 2)
drawPixel(xSep, separatorY, BLACK);
}
// Prepare to draw the next card // Prepare to draw the next card
resetCrop();
cardTopY += cardH; cardTopY += cardH;
// Once we've run out of screen, stop drawing cards // Once we've run out of screen, stop drawing cards
@@ -313,4 +288,4 @@ void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t
} }
} }
#endif #endif
@@ -65,10 +65,10 @@ class NodeListApplet : public Applet, public MeshModule
// Card Dimensions // Card Dimensions
// - for rendering and for maxCards calc // - for rendering and for maxCards calc
uint8_t cardMarginH = 1; // Gap between cards (minimal to fit more rows) uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
}; };
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD
#endif #endif
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
InkHUD::BatteryIconApplet::BatteryIconApplet() InkHUD::BatteryIconApplet::BatteryIconApplet()
{ {
alwaysRender = true; // render every time the screen is updated alwaysRender = true; // render everytime the screen is updated
// Show at boot, if user has previously enabled the feature // Show at boot, if user has previously enabled the feature
if (settings->optionalFeatures.batteryIcon) if (settings->optionalFeatures.batteryIcon)
@@ -91,4 +91,4 @@ void InkHUD::BatteryIconApplet::onRender(bool full)
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK); drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
} }
#endif #endif
@@ -2,7 +2,7 @@
/* /*
System Applet to render an on-screen keyboard System Applet to render an on-screeen keyboard
*/ */
@@ -45,7 +45,7 @@ void InkHUD::LogoApplet::onRender(bool full)
int16_t logoCY = Y(0.5 - 0.05); int16_t logoCY = Y(0.5 - 0.05);
// Invert colors if black-on-white // Invert colors if black-on-white
// Used during shutdown, to report display health // Used during shutdown, to resport display health
// Todo: handle this in InkHUD::Renderer instead // Todo: handle this in InkHUD::Renderer instead
if (inverted) { if (inverted) {
fillScreen(BLACK); fillScreen(BLACK);
@@ -186,4 +186,4 @@ int32_t InkHUD::LogoApplet::runOnce()
return OSThread::disable(); return OSThread::disable();
} }
#endif #endif
@@ -349,13 +349,13 @@ void InkHUD::MenuApplet::execute(MenuItem item)
handleFreeText = true; handleFreeText = true;
cm.freeTextItem.rawText.erase(); // clear the previous freetext message cm.freeTextItem.rawText.erase(); // clear the previous freetext message
freeTextMode = true; // render input field instead of normal menu freeTextMode = true; // render input field instead of normal menu
// Open the on-screen keyboard only for full joystick devices // Open the on-screen keyboard if the joystick is enabled
if (settings->joystick.enabled && !inkhud->twoWayRocker) if (settings->joystick.enabled)
inkhud->openKeyboard(); inkhud->openKeyboard();
break; break;
case STORE_CANNEDMESSAGE_SELECTION: case STORE_CANNEDMESSAGE_SELECTION:
if (!settings->joystick.enabled || inkhud->twoWayRocker) if (!settings->joystick.enabled)
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
else else
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
@@ -922,7 +922,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
if (settings->userTiles.maxCount > 1) if (settings->userTiles.maxCount > 1)
items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS));
items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS));
if (settings->joystick.enabled && !inkhud->twoWayRocker) if (settings->joystick.enabled)
items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT));
items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS,
&settings->optionalFeatures.notifications)); &settings->optionalFeatures.notifications));
@@ -1751,7 +1751,7 @@ void InkHUD::MenuApplet::populateSendPage()
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
// If joystick is available, include the Free Text option // If joystick is available, include the Free Text option
if (settings->joystick.enabled && !inkhud->twoWayRocker) if (settings->joystick.enabled)
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND)); items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
// One menu item for each canned message // One menu item for each canned message
@@ -2028,7 +2028,7 @@ void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char
service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone
} }
// Free up any heap memory we'd used while selecting / sending canned messages // Free up any heap mmemory we'd used while selecting / sending canned messages
void InkHUD::MenuApplet::freeCannedMessageResources() void InkHUD::MenuApplet::freeCannedMessageResources()
{ {
cm.selectedMessageItem = nullptr; cm.selectedMessageItem = nullptr;
@@ -152,11 +152,6 @@ void InkHUD::TipsApplet::onRender(bool full)
drawBullet("User Button"); drawBullet("User Button");
drawBullet("- short press: next"); drawBullet("- short press: next");
drawBullet("- long press: select or open menu"); drawBullet("- long press: select or open menu");
} else if (inkhud->twoWayRocker) {
drawBullet("Rocker + Button");
drawBullet("- center press: open menu or select");
drawBullet("- left/right: applet nav");
drawBullet("- in menu: up/down");
} else { } else {
drawBullet("Joystick"); drawBullet("Joystick");
drawBullet("- press: open menu or select"); drawBullet("- press: open menu or select");
@@ -7,7 +7,7 @@ Displays a thread-view of incoming and outgoing message for a specific channel
The channel for this applet is set in the constructor, The channel for this applet is set in the constructor,
when the applet is added to WindowManager in the setupNicheGraphics method. when the applet is added to WindowManager in the setupNicheGraphics method.
Several messages are saved to flash at shutdown, to preserve applet between reboots. Several messages are saved to flash at shutdown, to preseve applet between reboots.
This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer. This class has its own internal method for saving and loading to fs, which interacts directly with the FSCommon layer.
If the amount of flash usage is unacceptable, we could keep these in RAM only. If the amount of flash usage is unacceptable, we could keep these in RAM only.
@@ -55,4 +55,4 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD
#endif #endif
+1 -4
View File
@@ -88,9 +88,6 @@ class InkHUD
// Used by TipsApplet to force menu to start on Region selection // Used by TipsApplet to force menu to start on Region selection
bool forceRegionMenu = false; bool forceRegionMenu = false;
// Input mode hint for devices that use a left/right rocker plus center button
bool twoWayRocker = false;
// Updating the display // Updating the display
// - called by various InkHUD components // - called by various InkHUD components
@@ -133,4 +130,4 @@ class InkHUD
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD
#endif #endif
+2 -2
View File
@@ -53,7 +53,7 @@ class Renderer : protected concurrency::OSThread
uint16_t height(); uint16_t height();
private: private:
// Make attempts to render / update, once triggered by requestUpdate or forceUpdate // Make attemps to render / update, once triggered by requestUpdate or forceUpdate
int32_t runOnce() override; int32_t runOnce() override;
// Apply the display rotation to handled pixels // Apply the display rotation to handled pixels
@@ -95,4 +95,4 @@ class Renderer : protected concurrency::OSThread
} // namespace NicheGraphics::InkHUD } // namespace NicheGraphics::InkHUD
#endif #endif
+5 -11
View File
@@ -143,7 +143,7 @@ void InkHUD::WindowManager::openMenu()
// Bring the AlignStick applet to the foreground // Bring the AlignStick applet to the foreground
void InkHUD::WindowManager::openAlignStick() void InkHUD::WindowManager::openAlignStick()
{ {
if (settings->joystick.enabled && !inkhud->twoWayRocker) { if (settings->joystick.enabled) {
AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick");
alignStick->bringToForeground(); alignStick->bringToForeground();
} }
@@ -151,9 +151,6 @@ void InkHUD::WindowManager::openAlignStick()
void InkHUD::WindowManager::openKeyboard() void InkHUD::WindowManager::openKeyboard()
{ {
if (!settings->joystick.enabled || inkhud->twoWayRocker)
return;
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
if (keyboard) { if (keyboard) {
@@ -165,9 +162,6 @@ void InkHUD::WindowManager::openKeyboard()
void InkHUD::WindowManager::closeKeyboard() void InkHUD::WindowManager::closeKeyboard()
{ {
if (!settings->joystick.enabled || inkhud->twoWayRocker)
return;
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard"); KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
if (keyboard) { if (keyboard) {
@@ -483,7 +477,7 @@ void InkHUD::WindowManager::createSystemApplets()
addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Logo", new LogoApplet, new Tile);
addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile);
addSystemApplet("Tips", new TipsApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile);
if (settings->joystick.enabled && !inkhud->twoWayRocker) { if (settings->joystick.enabled) {
addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
addSystemApplet("Keyboard", new KeyboardApplet, new Tile); addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
} }
@@ -509,7 +503,7 @@ void InkHUD::WindowManager::placeSystemTiles()
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
if (settings->joystick.enabled && !inkhud->twoWayRocker) { if (settings->joystick.enabled) {
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight(); const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
inkhud->getSystemApplet("Keyboard") inkhud->getSystemApplet("Keyboard")
@@ -655,7 +649,7 @@ void InkHUD::WindowManager::refocusTile()
} }
} }
// Search for any applets which believe they are foreground, but no longer have a valid tile // Seach for any applets which believe they are foreground, but no longer have a valid tile
// Tidies up after layout changes at runtime // Tidies up after layout changes at runtime
void InkHUD::WindowManager::findOrphanApplets() void InkHUD::WindowManager::findOrphanApplets()
{ {
@@ -685,4 +679,4 @@ void InkHUD::WindowManager::findOrphanApplets()
} }
} }
#endif #endif
+1 -1
View File
@@ -733,7 +733,7 @@ To add support for additional encodings, add to the `AppletFont::Encodings` enum
#### Custom Line Height #### Custom Line Height
Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritics. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor. Some fonts may have a handful of especially tall characters, especially extended-ASCII fonts with diacritcs. Ideally, the font should be modified to help resolve this, but if the problem remains, manual offsets to the automatically determined line height can be specified in the constructor.
```cpp ```cpp
// -2 px of padding above, +1 px of padding below // -2 px of padding above, +1 px of padding below
+2 -2
View File
@@ -59,7 +59,7 @@ void TwoButton::stop()
} }
// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings
// This helper method isn't used by the TwoButton class itself, it could be moved elsewhere. // This helper method isn't used by the TweButton class itself, it could be moved elsewhere.
// Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method. // Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method.
uint8_t TwoButton::getUserButtonPin() uint8_t TwoButton::getUserButtonPin()
{ {
@@ -308,4 +308,4 @@ int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause)
#endif #endif
#endif #endif
@@ -156,24 +156,6 @@ void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lP
pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
} }
// Configures only left/right joystick directions for a two-way rocker
void TwoButtonExtended::setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup)
{
if (leftPin == rightPin) {
LOG_WARN("Attempted reuse of TwoWayRocker GPIO. Ignoring assignment");
return;
}
joystick[Direction::UP].pin = 0xFF;
joystick[Direction::DOWN].pin = 0xFF;
joystick[Direction::LEFT].pin = leftPin;
joystick[Direction::RIGHT].pin = rightPin;
joystickActiveLogic = LOW;
pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT);
pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
}
void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
{ {
assert(whichButton < 2); assert(whichButton < 2);
@@ -247,13 +229,6 @@ void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPres
joystick[Direction::RIGHT].onPress = rPress; joystick[Direction::RIGHT].onPress = rPress;
} }
// Set press handlers for a two-way rocker mapped to left/right directions
void TwoButtonExtended::setTwoWayRockerPressHandlers(Callback lPress, Callback rPress)
{
joystick[Direction::LEFT].onPress = lPress;
joystick[Direction::RIGHT].onPress = rPress;
}
// Handle the start of a press to the primary button // Handle the start of a press to the primary button
// Wakes our button thread // Wakes our button thread
void TwoButtonExtended::isrPrimary() void TwoButtonExtended::isrPrimary()
@@ -45,7 +45,6 @@ class TwoButtonExtended : protected concurrency::OSThread
void stop(); // Stop handling button input (disconnect ISRs for sleep) void stop(); // Stop handling button input (disconnect ISRs for sleep)
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false);
void setTwoWayRockerWiring(uint8_t leftPin, uint8_t rightPin, bool internalPullup = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setJoystickDebounce(uint32_t debounceMs); void setJoystickDebounce(uint32_t debounceMs);
void setHandlerDown(uint8_t whichButton, Callback onDown); void setHandlerDown(uint8_t whichButton, Callback onDown);
@@ -55,7 +54,6 @@ class TwoButtonExtended : protected concurrency::OSThread
void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
void setTwoWayRockerPressHandlers(Callback lPress, Callback rPress);
// Disconnect and reconnect interrupts for light sleep // Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
+3 -3
View File
@@ -271,8 +271,8 @@ int32_t ButtonThread::runOnce()
break; break;
} // end multipress event } // end multipress event
// Do actual shutdown when button released, otherwise the button release // Do actual shutdown when button released, otherwise the button release
// may wake the board immediately. // may wake the board immediatedly.
case BUTTON_EVENT_LONG_RELEASED: { case BUTTON_EVENT_LONG_RELEASED: {
LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime); LOG_INFO("LONG PRESS RELEASE AFTER %u MILLIS", millis() - buttonPressStartTime);
@@ -347,4 +347,4 @@ int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause)
void ButtonThread::storeClickCount() void ButtonThread::storeClickCount()
{ {
multipressClickCount = userButton.getNumberClicks(); multipressClickCount = userButton.getNumberClicks();
} }
-200
View File
@@ -1,200 +0,0 @@
#if defined(M5STACK_CARDPUTER_ADV)
#include "CardputerKeyboard.h"
#include "main.h"
#define _TCA8418_COLS 8
#define _TCA8418_ROWS 7
#define _TCA8418_NUM_KEYS 56
#define _TCA8418_MULTI_TAP_THRESHOLD 1500
using Key = TCA8418KeyboardBase::TCA8418Key;
constexpr uint8_t modifierFnKey = 2;
constexpr uint8_t modifierFn = 0b0010;
constexpr uint8_t modifierCtrlKey = 3;
constexpr uint8_t modifierShiftKey = 6;
constexpr uint8_t modifierShift = 0b0001;
constexpr uint8_t modifierOptKey = 7;
constexpr uint8_t modifierAltKey = 11;
// Num chars per key, Modulus for rotating through characters
static uint8_t CardputerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3};
static unsigned char CardputerTapMap[_TCA8418_NUM_KEYS][3] = {{'`', '~', Key::ESC},
{Key::TAB, 0x00, 0x00},
{0x00, 0x00, 0x00},
{0x00, 0x00, 0x00},
{'1', '!', 0x00},
{'q', 'Q', Key::REBOOT},
{0x00, 0x00, 0x00},
{0x00, 0x00, 0x00},
{'2', '@', 0x00},
{'w', 'W', 0x00},
{'a', 'A', 0x00},
{0x00, 0x00, 0x00},
{'3', '#', 0x00},
{'e', 'E', 0x00},
{'s', 'S', 0x00},
{'z', 'Z', 0x00},
{'4', '$', 0x00},
{'r', 'R', 0x00},
{'d', 'D', 0x00},
{'x', 'X', 0x00},
{'5', '%', 0x00},
{'t', 'T', 0x00},
{'f', 'F', 0x00},
{'c', 'C', 0x00},
{'6', '^', 0x00},
{'y', 'Y', 0x00},
{'g', 'G', Key::GPS_TOGGLE},
{'v', 'V', 0x00},
{'7', '&', 0x00},
{'u', 'U', 0x00},
{'h', 'H', 0x00},
{'b', 'B', Key::BT_TOGGLE},
{'8', '*', 0x00},
{'i', 'I', 0x00},
{'j', 'J', 0x00},
{'n', 'N', 0x00},
{'9', '(', 0x00},
{'o', 'O', 0x00},
{'k', 'K', 0x00},
{'m', 'M', Key::MUTE_TOGGLE},
{'0', ')', 0x00},
{'p', 'P', Key::SEND_PING},
{'l', 'L', 0x00},
{',', '<', Key::LEFT},
{'_', '-', 0x00},
{'[', '{', 0x00},
{';', ':', Key::UP},
{'.', '>', Key::DOWN},
{'=', '+', 0x00},
{']', '}', 0x00},
{'\'', '"', 0x00},
{'/', '?', Key::RIGHT},
{Key::BSP, 0x00, 0x00},
{'\\', '|', 0x00},
{Key::SELECT, 0x00, 0x00},
{' ', ' ', ' '}};
CardputerKeyboard::CardputerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
{
reset();
}
void CardputerKeyboard::reset(void)
{
TCA8418KeyboardBase::reset();
}
// handle multi-key presses (shift and alt)
void CardputerKeyboard::trigger()
{
uint8_t count = keyCount();
if (count == 0)
return;
for (uint8_t i = 0; i < count; ++i) {
uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
uint8_t key = k & 0x7F;
if (k & 0x80) {
pressed(key);
} else {
released();
state = Idle;
}
}
}
void CardputerKeyboard::pressed(uint8_t key)
{
if (state == Init || state == Busy) {
return;
}
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
return; // Invalid key
}
next_key = row * _TCA8418_COLS + col;
state = Held;
uint32_t now = millis();
tap_interval = now - last_tap;
updateModifierFlag(next_key);
if (isModifierKey(next_key)) {
last_modifier_time = now;
}
if (tap_interval < 0) {
last_tap = 0;
state = Busy;
return;
}
if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
char_idx = 0;
} else {
char_idx += 1;
}
last_key = next_key;
last_tap = now;
}
void CardputerKeyboard::released()
{
if (state != Held) {
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
state = Idle;
return;
}
uint32_t now = millis();
last_tap = now;
if (inputBroker->menuMode && modifierFlag == 0) {
if (last_key == 0 || last_key == 43 || last_key == 46 || last_key == 47 ||
last_key == 51) { // esc, left, up, down, right key
modifierFlag = modifierFn;
}
}
queueEvent(CardputerTapMap[last_key][modifierFlag % CardputerTapMod[last_key]]);
if (isModifierKey(last_key) == false)
modifierFlag = 0;
}
void CardputerKeyboard::updateModifierFlag(uint8_t key)
{
if (key == modifierShiftKey) {
modifierFlag ^= modifierShift;
} else if (key == modifierFnKey) {
modifierFlag ^= modifierFn;
}
}
bool CardputerKeyboard::isModifierKey(uint8_t key)
{
return (key == modifierShiftKey || key == modifierFnKey);
}
#endif
-26
View File
@@ -1,26 +0,0 @@
#include "TCA8418KeyboardBase.h"
class CardputerKeyboard : public TCA8418KeyboardBase
{
public:
CardputerKeyboard();
void reset(void);
void trigger(void) override;
virtual ~CardputerKeyboard() {}
protected:
void pressed(uint8_t key) override;
void released(void) override;
void updateModifierFlag(uint8_t key);
bool isModifierKey(uint8_t key);
private:
uint8_t modifierFlag;
uint32_t last_modifier_time;
int8_t last_key;
int8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
};
-1
View File
@@ -70,7 +70,6 @@ class InputBroker : public Observable<const InputEvent *>
public: public:
InputBroker(); InputBroker();
bool menuMode = true;
void registerSource(Observable<const InputEvent *> *source); void registerSource(Observable<const InputEvent *> *source);
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
#if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040) #if defined(HAS_FREE_RTOS) && !defined(ARCH_RP2040)
+2 -2
View File
@@ -177,7 +177,7 @@ void MPR121Keyboard::reset()
delay(20); delay(20);
writeRegister(_MPR121_REG_CONFIG2, 0x21); writeRegister(_MPR121_REG_CONFIG2, 0x21);
delay(20); delay(20);
// Enter run mode by setting partial filter calibration tracking, disable proximity detection, enable 12 channels // Enter run mode by Seting partial filter calibration tracking, disable proximity detection, enable 12 channels
writeRegister(_MPR121_REG_ELECTRODE_CONFIG, writeRegister(_MPR121_REG_ELECTRODE_CONFIG,
ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH); ECR_CALIBRATION_TRACK_FROM_FULL_FILTER | ECR_PROXIMITY_DETECTION_OFF | ECR_TOUCH_DETECTION_12CH);
delay(100); delay(100);
@@ -430,4 +430,4 @@ void MPR121Keyboard::writeRegister(uint8_t reg, uint8_t value)
if (writeCallback) { if (writeCallback) {
writeCallback(m_addr, data[0], &(data[1]), 1); writeCallback(m_addr, data[0], &(data[1]), 1);
} }
} }
+2 -2
View File
@@ -59,7 +59,7 @@ int32_t SeesawRotary::runOnce()
wasPressed = currentlyPressed; wasPressed = currentlyPressed;
int32_t new_position = ss.getEncoderPosition(); int32_t new_position = ss.getEncoderPosition();
// did we move around? // did we move arounde?
if (encoder_position != new_position) { if (encoder_position != new_position) {
if (encoder_position == 0 && new_position != 1) { if (encoder_position == 0 && new_position != 1) {
e.inputEvent = INPUT_BROKER_ALT_PRESS; e.inputEvent = INPUT_BROKER_ALT_PRESS;
@@ -80,4 +80,4 @@ int32_t SeesawRotary::runOnce()
return 50; return 50;
} }
#endif #endif
+1 -1
View File
@@ -88,7 +88,7 @@ void TCA8418Keyboard::pressed(uint8_t key)
// Check if the key is the same as the last one or if the time interval has passed // Check if the key is the same as the last one or if the time interval has passed
if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
char_idx = 0; // Reset char index if new key or long press char_idx = 0; // Reset char index if new key or long press
should_backspace = false; // don't backspace on new key should_backspace = false; // dont backspace on new key
} else { } else {
char_idx += 1; // Cycle through characters if same key pressed char_idx += 1; // Cycle through characters if same key pressed
should_backspace = true; // allow backspace on same key should_backspace = true; // allow backspace on same key
+2 -2
View File
@@ -43,7 +43,7 @@ int32_t TouchScreenBase::runOnce()
// process touch events // process touch events
int16_t x, y; int16_t x, y;
bool touched = getTouch(x, y); bool touched = getTouch(x, y);
if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen
touched = false; touched = false;
if (touched) { if (touched) {
this->setInterval(20); this->setInterval(20);
@@ -123,7 +123,7 @@ int32_t TouchScreenBase::runOnce()
} }
} }
#else #else
// fire TAP event when no 2nd tap occurred within time // fire TAP event when no 2nd tap occured within time
if (_tapped) { if (_tapped) {
_tapped = false; _tapped = false;
e.touchEvent = static_cast<char>(TOUCH_ACTION_TAP); e.touchEvent = static_cast<char>(TOUCH_ACTION_TAP);
+1 -9
View File
@@ -8,14 +8,6 @@ UpDownInterruptImpl1::UpDownInterruptImpl1() : UpDownInterruptBase("upDown1") {}
bool UpDownInterruptImpl1::init() bool UpDownInterruptImpl1::init()
{ {
#if defined(INPUTDRIVER_TWO_WAY_ROCKER) && defined(INPUTDRIVER_TWO_WAY_ROCKER_LEFT) && defined(INPUTDRIVER_TWO_WAY_ROCKER_RIGHT)
moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = INPUTDRIVER_TWO_WAY_ROCKER_LEFT;
moduleConfig.canned_message.inputbroker_pin_b = INPUTDRIVER_TWO_WAY_ROCKER_RIGHT;
#if defined(INPUTDRIVER_TWO_WAY_ROCKER_BTN)
moduleConfig.canned_message.inputbroker_pin_press = INPUTDRIVER_TWO_WAY_ROCKER_BTN;
#endif
#endif
if (!moduleConfig.canned_message.updown1_enabled) { if (!moduleConfig.canned_message.updown1_enabled) {
// Input device is disabled. // Input device is disabled.
@@ -54,4 +46,4 @@ void UpDownInterruptImpl1::handleIntUp()
void UpDownInterruptImpl1::handleIntPressed() void UpDownInterruptImpl1::handleIntPressed()
{ {
upDownInterruptImpl1->intPressHandler(); upDownInterruptImpl1->intPressHandler();
} }
+1 -5
View File
@@ -7,8 +7,6 @@
#include "TDeckProKeyboard.h" #include "TDeckProKeyboard.h"
#elif defined(T_LORA_PAGER) #elif defined(T_LORA_PAGER)
#include "TLoraPagerKeyboard.h" #include "TLoraPagerKeyboard.h"
#elif defined(M5STACK_CARDPUTER_ADV)
#include "CardputerKeyboard.h"
#elif defined(HACKADAY_COMMUNICATOR) #elif defined(HACKADAY_COMMUNICATOR)
#include "HackadayCommunicatorKeyboard.h" #include "HackadayCommunicatorKeyboard.h"
#else #else
@@ -24,8 +22,6 @@ KbI2cBase::KbI2cBase(const char *name)
TCAKeyboard(*(new TDeckProKeyboard())) TCAKeyboard(*(new TDeckProKeyboard()))
#elif defined(T_LORA_PAGER) #elif defined(T_LORA_PAGER)
TCAKeyboard(*(new TLoraPagerKeyboard())) TCAKeyboard(*(new TLoraPagerKeyboard()))
#elif defined(M5STACK_CARDPUTER_ADV)
TCAKeyboard(*(new CardputerKeyboard()))
#elif defined(HACKADAY_COMMUNICATOR) #elif defined(HACKADAY_COMMUNICATOR)
TCAKeyboard(*(new HackadayCommunicatorKeyboard())) TCAKeyboard(*(new HackadayCommunicatorKeyboard()))
#else #else
@@ -491,7 +487,7 @@ int32_t KbI2cBase::runOnce()
e.kbchar = 0; e.kbchar = 0;
break; break;
case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker))
// toggle modifiers button. // toggle moddifiers button.
is_sym = !is_sym; is_sym = !is_sym;
e.inputEvent = INPUT_BROKER_ANYKEY; e.inputEvent = INPUT_BROKER_ANYKEY;
e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the
+22
View File
@@ -59,6 +59,13 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
NRF52Bluetooth *nrf52Bluetooth = nullptr; NRF52Bluetooth *nrf52Bluetooth = nullptr;
#endif #endif
#ifdef MESHTASTIC_ENABLE_APPROTECT
#include "security/APProtect.h"
#endif
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
#include "security/EncryptedStorage.h"
#endif
#if HAS_WIFI || defined(USE_WS5500) #if HAS_WIFI || defined(USE_WS5500)
#include "mesh/api/WiFiServerAPI.h" #include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h" #include "mesh/wifi/WiFiAPClient.h"
@@ -364,6 +371,10 @@ void setup()
consoleInit(); // Set serial baud rate and init our mesh console consoleInit(); // Set serial baud rate and init our mesh console
#endif #endif
#ifdef MESHTASTIC_ENABLE_APPROTECT
enableAPProtect();
#endif
#ifdef UNPHONE #ifdef UNPHONE
unphone.printStore(); unphone.printStore();
#endif #endif
@@ -461,6 +472,17 @@ void setup()
fsInit(); fsInit();
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
EncryptedStorage::initLocked();
if (!EncryptedStorage::isUnlocked()) {
if (!EncryptedStorage::isProvisioned()) {
LOG_WARN("TAK: Device not provisioned — connect and set a passphrase to unlock storage");
} else {
LOG_WARN("TAK: Device locked — connect and provide passphrase to unlock storage");
}
}
#endif
#if !MESHTASTIC_EXCLUDE_I2C #if !MESHTASTIC_EXCLUDE_I2C
#if defined(I2C_SDA1) && defined(ARCH_RP2040) #if defined(I2C_SDA1) && defined(ARCH_RP2040)
Wire1.setSDA(I2C_SDA1); Wire1.setSDA(I2C_SDA1);
+2 -4
View File
@@ -38,13 +38,11 @@ uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultVa
uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes) uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t defaultValue, uint32_t numOnlineNodes)
{ {
// If we are a router, we don't scale the value. It's already significantly higher. // If we are a router, we don't scale the value. It's already significantly higher.
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER)
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
return getConfiguredOrDefaultMs(configured, defaultValue); return getConfiguredOrDefaultMs(configured, defaultValue);
// Additionally if we're a tracker or sensor, we want priority to send position and telemetry // Additionally if we're a tracker or sensor, we want priority to send position and telemetry
if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER, if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_TRACKER))
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
return getConfiguredOrDefaultMs(configured, defaultValue); return getConfiguredOrDefaultMs(configured, defaultValue);
return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes); return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes);
+15 -40
View File
@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <MeshRadio.h>
#include <NodeDB.h> #include <NodeDB.h>
#include <RadioInterface.h>
#include <cmath>
#include <cstdint> #include <cstdint>
#include <meshUtils.h> #include <meshUtils.h>
#define ONE_DAY 24 * 60 * 60 #define ONE_DAY 24 * 60 * 60
@@ -30,11 +27,6 @@
#define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour #define min_node_info_broadcast_secs 60 * 60 // No regular broadcasts of more than once an hour
#define min_neighbor_info_broadcast_secs 4 * 60 * 60 #define min_neighbor_info_broadcast_secs 4 * 60 * 60
#define default_map_publish_interval_secs 60 * 60 #define default_map_publish_interval_secs 60 * 60
// Traffic management defaults
#define default_traffic_mgmt_position_precision_bits 24 // ~10m grid cells
#define default_traffic_mgmt_position_min_interval_secs ONE_DAY // 1 day between identical positions
#ifdef USERPREFS_RINGTONE_NAG_SECS #ifdef USERPREFS_RINGTONE_NAG_SECS
#define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS #define default_ringtone_nag_secs USERPREFS_RINGTONE_NAG_SECS
#else #else
@@ -50,10 +42,7 @@
#define default_mqtt_tls_enabled false #define default_mqtt_tls_enabled false
#define IF_ROUTER(routerVal, normalVal) \ #define IF_ROUTER(routerVal, normalVal) \
((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER || \ ((config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER) ? (routerVal) : (normalVal))
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) \
? (routerVal) \
: (normalVal))
class Default class Default
{ {
@@ -74,39 +63,25 @@ class Default
if (numOnlineNodes <= 40) { if (numOnlineNodes <= 40) {
return 1.0; return 1.0;
} else { } else {
// Resolve SF and BW from preset or manual config float throttlingFactor = 0.075;
// When use_preset is true, config.lora.spread_factor and bandwidth may be 0 if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW)
// because applyModemConfig() sets them on RadioInterface, not on config.lora throttlingFactor = 0.04;
float bwKHz; else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST)
uint8_t sf; throttlingFactor = 0.02;
uint8_t cr; else if (config.lora.use_preset &&
if (config.lora.use_preset) { IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
modemPresetToParams(config.lora.modem_preset, false, bwKHz, sf, cr); meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
} else { meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW))
sf = config.lora.spread_factor; throttlingFactor = 0.01;
bwKHz = bwCodeToKHz(config.lora.bandwidth);
}
// Guard against invalid values
sf = clampSpreadFactor(sf);
bwKHz = clampBandwidthKHz(bwKHz);
// throttlingFactor = 2^SF / (BW_in_kHz * scaling_divisor)
// With scaling_divisor=100:
// In SF11 and BW=250khz (longfast), this gives 0.08192 rather than the original 0.075
// In SF10 and BW=250khz (mediumslow), this gives 0.04096 rather than the original 0.04
// In SF9 and BW=250khz (mediumfast), this gives 0.02048 rather than the original 0.02
// In SF7 and BW=250khz (shortfast), this gives 0.00512 rather than the original 0.01
float throttlingFactor = static_cast<float>(pow_of_2(sf)) / (bwKHz * 100.0f);
#if USERPREFS_EVENT_MODE #if USERPREFS_EVENT_MODE
// If we are in event mode, scale down the throttling factor by 4 // If we are in event mode, scale down the throttling factor
throttlingFactor = static_cast<float>(pow_of_2(sf)) / (bwKHz * 25.0f); throttlingFactor = 0.04;
#endif #endif
// Scaling up traffic based on number of nodes over 40 // Scaling up traffic based on number of nodes over 40
int nodesOverForty = (numOnlineNodes - 40); int nodesOverForty = (numOnlineNodes - 40);
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by throttle factor return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default)
} }
} }
}; };
-228
View File
@@ -1,228 +0,0 @@
#if HAS_LORA_FEM
#include "LoRaFEMInterface.h"
#if defined(ARCH_ESP32)
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#endif
LoRaFEMInterface loraFEMInterface;
void LoRaFEMInterface::init(void)
{
setLnaCanControl(false); // Default is uncontrollable
#ifdef HELTEC_V4
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
delay(1);
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD);
pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used
delay(1);
if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) {
// FEM is KCT8103L
fem_type = KCT8103L_PA;
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX);
pinMode(LORA_KCT8103L_PA_CSD, OUTPUT);
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
pinMode(LORA_KCT8103L_PA_CTX, OUTPUT);
digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default
setLnaCanControl(true);
} else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) {
// FEM is GC1109
fem_type = GC1109_PA;
// LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held.
// rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN);
pinMode(LORA_GC1109_PA_EN, OUTPUT);
digitalWrite(LORA_GC1109_PA_EN, HIGH);
pinMode(LORA_GC1109_PA_TX_EN, OUTPUT);
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
} else {
fem_type = OTHER_FEM_TYPES;
}
#elif defined(USE_GC1109_PA)
fem_type = GC1109_PA;
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
#if defined(ARCH_ESP32)
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN);
rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN);
#endif
delay(1);
pinMode(LORA_GC1109_PA_EN, OUTPUT);
digitalWrite(LORA_GC1109_PA_EN, HIGH);
pinMode(LORA_GC1109_PA_TX_EN, OUTPUT);
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
#elif defined(USE_KCT8103L_PA)
fem_type = KCT8103L_PA;
pinMode(LORA_PA_POWER, OUTPUT);
digitalWrite(LORA_PA_POWER, HIGH);
#if defined(ARCH_ESP32)
rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER);
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD);
rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX);
#endif
delay(1);
pinMode(LORA_KCT8103L_PA_CSD, OUTPUT);
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
pinMode(LORA_KCT8103L_PA_CTX, OUTPUT);
digitalWrite(LORA_KCT8103L_PA_CTX, LOW); // LNA enabled by default
setLnaCanControl(true);
#endif
}
void LoRaFEMInterface::setSleepModeEnable(void)
{
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
/*
* Do not switch the power on and off frequently.
* After turning off LORA_GC1109_PA_EN, the power consumption has dropped to the uA level.
*/
digitalWrite(LORA_GC1109_PA_EN, LOW);
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
} else if (fem_type == KCT8103L_PA) {
// shutdown the PA
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_GC1109_PA_EN, LOW);
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
#elif defined(USE_KCT8103L_PA)
// shutdown the PA
digitalWrite(LORA_KCT8103L_PA_CSD, LOW);
#endif
}
void LoRaFEMInterface::setTxModeEnable(void)
{
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
} else if (fem_type == KCT8103L_PA) {
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care)
#elif defined(USE_KCT8103L_PA)
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
#endif
}
void LoRaFEMInterface::setRxModeEnable(void)
{
#ifdef HELTEC_V4
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
} else if (fem_type == KCT8103L_PA) {
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled
digitalWrite(LORA_GC1109_PA_TX_EN, LOW);
#elif defined(USE_KCT8103L_PA)
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
#endif
}
void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void)
{
#ifdef HELTEC_V4
// Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake.
// Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown),
// then latch with RTC hold so the state survives deep sleep.
digitalWrite(LORA_PA_POWER, HIGH);
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
if (fem_type == GC1109_PA) {
digitalWrite(LORA_GC1109_PA_EN, HIGH);
rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN);
gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN);
} else if (fem_type == KCT8103L_PA) {
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
}
#elif defined(USE_GC1109_PA)
digitalWrite(LORA_PA_POWER, HIGH);
digitalWrite(LORA_GC1109_PA_EN, HIGH);
#if defined(ARCH_ESP32)
rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER);
rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN);
gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN);
#endif
#elif defined(USE_KCT8103L_PA)
digitalWrite(LORA_KCT8103L_PA_CSD, HIGH);
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD);
if (lna_enabled) {
digitalWrite(LORA_KCT8103L_PA_CTX, LOW);
} else {
digitalWrite(LORA_KCT8103L_PA_CTX, HIGH);
}
rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX);
#endif
}
void LoRaFEMInterface::setLNAEnable(bool enabled)
{
lna_enabled = enabled;
}
int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower)
{
#ifdef HELTEC_V4
const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7};
const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7};
const uint16_t *tx_gain;
uint16_t tx_gain_num;
if (fem_type == GC1109_PA) {
tx_gain = gc1109_tx_gain;
tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]);
} else if (fem_type == KCT8103L_PA) {
tx_gain = kct8103l_tx_gain;
tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]);
} else {
return loraOutputPower;
}
#else
#ifdef ARCH_PORTDUINO
const uint16_t *tx_gain = portduino_config.tx_gain_lora;
uint16_t tx_gain_num = portduino_config.num_pa_points;
#else
const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA};
uint16_t tx_gain_num = NUM_PA_POINTS;
#endif
#endif
for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) {
if (((radio_dbm + tx_gain[radio_dbm]) > loraOutputPower) ||
((radio_dbm == (tx_gain_num - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= loraOutputPower))) {
// we've exceeded the power limit, or hit the max we can do
LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", loraOutputPower, tx_gain[radio_dbm]);
loraOutputPower -= tx_gain[radio_dbm];
break;
}
}
return loraOutputPower;
}
#endif
-30
View File
@@ -1,30 +0,0 @@
#pragma once
#if HAS_LORA_FEM
#include "configuration.h"
#include <stdint.h>
typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType;
class LoRaFEMInterface
{
public:
LoRaFEMInterface() : fem_type(OTHER_FEM_TYPES) {}
virtual ~LoRaFEMInterface() {}
void init(void);
void setSleepModeEnable(void);
void setTxModeEnable(void);
void setRxModeEnable(void);
void setRxModeEnableWhenMCUSleep(void);
void setLNAEnable(bool enabled);
int8_t powerConversion(int8_t loraOutputPower);
bool isLnaCanControl(void) { return lna_can_control; }
void setLnaCanControl(bool can_control) { lna_can_control = can_control; }
private:
LoRaFEMType fem_type;
bool lna_enabled = true;
bool lna_can_control = false;
};
extern LoRaFEMInterface loraFEMInterface;
#endif
-39
View File
@@ -24,45 +24,6 @@ extern const RegionInfo *myRegion;
extern void initRegion(); extern void initRegion();
// Valid LoRa spread factor range and defaults
constexpr uint8_t LORA_SF_MIN = 7;
constexpr uint8_t LORA_SF_MAX = 12;
constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default
// Valid LoRa coding rate range and default
constexpr uint8_t LORA_CR_MIN = 4;
constexpr uint8_t LORA_CR_MAX = 8;
constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default
// Default bandwidth in kHz (LONG_FAST)
constexpr float LORA_BW_DEFAULT_KHZ = 250.0f;
/// Clamp spread factor to the valid LoRa range [7, 12].
/// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT.
static inline uint8_t clampSpreadFactor(uint8_t sf)
{
if (sf < LORA_SF_MIN || sf > LORA_SF_MAX)
return LORA_SF_DEFAULT;
return sf;
}
/// Clamp coding rate to the valid LoRa range [4, 8].
/// Out-of-range values return LORA_CR_DEFAULT.
static inline uint8_t clampCodingRate(uint8_t cr)
{
if (cr < LORA_CR_MIN || cr > LORA_CR_MAX)
return LORA_CR_DEFAULT;
return cr;
}
/// Ensure bandwidth is positive. Non-positive values return LORA_BW_DEFAULT_KHZ.
static inline float clampBandwidthKHz(float bwKHz)
{
if (bwKHz <= 0.0f)
return LORA_BW_DEFAULT_KHZ;
return bwKHz;
}
static inline float bwCodeToKHz(uint16_t bwCode) static inline float bwCodeToKHz(uint16_t bwCode)
{ {
if (bwCode == 31) if (bwCode == 31)
+1 -3
View File
@@ -87,9 +87,7 @@ int MeshService::handleFromRadio(const meshtastic_MeshPacket *mp)
powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping powerFSM.trigger(EVENT_PACKET_FOR_PHONE); // Possibly keep the node from sleeping
nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio nodeDB->updateFrom(*mp); // update our DB state based off sniffing every RX packet from the radio
bool isPreferredRebroadcaster = bool isPreferredRebroadcaster = config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER;
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE);
if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag && if (mp->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) { mp->decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && mp->decoded.request_id > 0) {
LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo"); LOG_DEBUG("Received telemetry response. Skip sending our NodeInfo");

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