Compare commits

..
Author SHA1 Message Date
Clive BlackledgeandCopilot 016e68ec53 Traffic Management Module for packet forwarding logic (#9358)
* Add ESP32 Power Management lessons learned document

Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).

Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.

* Addition of traffic management module

* Fixing compile issues, but may still need to update protobufs.

* Fixing log2Floor in cuckoo hash function

* Adding support for traffic management in PhoneAPI.

* Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE.

* Adding station-g2 and portduino varients to be able to use this module.

* Spoofing from address for nodeinfo cache

* Changing name and behavior for zero_hop_telemetry / zero_hop_position

* Name change for exhausting telemetry packets and setting hop_limit to 1 so it will be 0 when sent.

* Updated hop logic, including exhaustRequested flag to bypass some checks later in the code.

* Reducing memory on nrf52 nodes further to 12 bytes per entry, 12KB total using 8 bit hashes with 0.4% collision. Probably ok. Adding portduino to the platforms that don't need to worry about memory as much.

* Fixing hopsAway for nodeinfo responses.

* traffic_management.nodeinfo_direct_response_min_hops -> traffic_management.nodeinfo_direct_response_max_hops

* Removing dry run mode

* Updates to UnifiedCacheEntry to use a common cache, created defaults for some values, reduced a couple bytes per entry by using a resolution-scale time selection based on configuration value.

* Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants.

* Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails.

* Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit.

* Creating consistent log messages

* Remove docs/ESP32_Power_Management.md from traffic_module

* Add unit tests for Traffic Management Module functionality

* Fixing compile issues, but may still need to update protobufs.

* Adding support for traffic management in PhoneAPI.

* Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE.

* Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants.

* Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails.

* Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit.

* Add mock classes and unit tests for Traffic Management Module functionality.

* Refactor setup and loop functions in test_main.cpp to include extern "C" linkage

* Update comment to include reduced  memory requirements

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Re-arranging comments for programmers with the attention span of less than 5 lines of code.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update comments in TrafficManagementModule to reflect changes in timestamp epoch handling and memory optimization details.

* bug: Use node-wide config_ok_to_mqtt setting for cached NodeInfo replies.

* Better way to handle clearing the ok_to_mqtt bit

* Add bucketing to cuckoo hashing, allowing for 95% occupied rate before major eviction problems.

* Extend nodeinfo cache for psram devices.

* Refactor traffic management to make hop exhaustion packet-scoped. Nice catch.

* Implement better position precision sanitization in TrafficManagementModule.

* Added logic in TrafficManagementModule to invalidate stale traffic state. Also, added some tests to avoid future me from creating a regression here.

* Fixing tests for native

* Enhance TrafficManagementModule to improve NodeInfo response handling and position deduplication logic. Added tests to ensure local packets bypass transit filters and that NodeInfo requests correctly update the requester information in the cache. Updated deduplication checks to prevent dropping valid position packets under certain conditions.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 06:12:12 -05:00
Catalin PatuleaandBen Meadors 79f469ce71 pioarduino Heltec v4: fix build due to LED_BUILTIN compile error. (#9875)
Fixes this build error:

  <command-line>: error: expected unqualified-id before '-' token
  /home/<snip>/.platformio/packages/framework-arduinoespressif32/variants/esp32s3/pins_arduino.h:15:22: note: in expansion of macro 'LED_BUILTIN'
     15 | static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT + PIN_RGB_LED;
        |                      ^~~~~~~~~~~

More info: https://github.com/meshtastic/firmware/pull/9122#issuecomment-4028263894

The fix is consistent with variants/esp32s3/heltec_v3/platformio.ini

This commit is intentionally on the develop branch, because it's harmless to
develop branch, and makes us more ready for pioarduino when the time comes.

Heltec v4 introduced in: https://github.com/meshtastic/firmware/pull/7845

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-03-10 06:19:25 -05:00
HarukiToredaandCopilot 0501e177e9 T-mini Eink S3 Support for both InkHUD and BaseUI (#9856)
* Tmini Eink fix

* tuning

* better refresh

* Fix to lora pins to be like the original.

* Update pins_arduino.h

* removed dead flags from previous tests

* Update src/graphics/niche/Drivers/EInk/GDEW0102T4.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 17:37:34 -05:00
212 changed files with 4668 additions and 4114 deletions
+2
View File
@@ -8,6 +8,8 @@ runs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install dependencies
shell: bash
+5 -3
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
@@ -27,6 +28,8 @@ jobs:
with:
submodules: recursive
path: meshtasticd
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install deps
shell: bash
@@ -39,7 +42,6 @@ jobs:
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
- name: Import GPG key
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
@@ -58,7 +60,7 @@ jobs:
run: debian/ci_pack_sdeb.sh
env:
SERIES: ${{ inputs.series }}
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid || '' }}
GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }}
PKG_VERSION: ${{ steps.version.outputs.deb }}
- name: Store binaries as an artifact
+2
View File
@@ -26,6 +26,8 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Build ${{ inputs.platform }}
id: build
+1 -1
View File
@@ -87,7 +87,7 @@ jobs:
gather-artifacts:
permissions:
contents: read
contents: write
pull-requests: write
runs-on: ubuntu-latest
needs: [version, build]
+2 -2
View File
@@ -5,7 +5,7 @@ on:
workflow_dispatch:
push:
branches:
- develop # Default branch, same as 'cron' above
- master
paths:
- debian/**
- "*.rpkg"
@@ -16,7 +16,7 @@ on:
- .github/workflows/hook_copr.yml
permissions:
contents: read
contents: write
packages: write
jobs:
+6 -4
View File
@@ -37,7 +37,7 @@ on:
value: ${{ jobs.docker-build.outputs.digest }}
permissions:
contents: read
contents: write
packages: write
jobs:
@@ -50,6 +50,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
@@ -67,7 +69,7 @@ jobs:
uses: docker/setup-qemu-action@v4
- name: Docker setup
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Sanitize platform string
id: sanitize_platform
@@ -76,7 +78,7 @@ jobs:
- name: Docker tag
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
@@ -84,7 +86,7 @@ jobs:
flavor: latest=false
- name: Docker build and push
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
id: docker_variant
with:
context: .
+5 -3
View File
@@ -12,7 +12,7 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
@@ -86,6 +86,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: |
@@ -144,7 +146,7 @@ jobs:
- name: Docker meta (Debian)
id: meta_debian
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
@@ -165,7 +167,7 @@ jobs:
- name: Docker meta (Alpine)
id: meta_alpine
uses: docker/metadata-action@v6
uses: docker/metadata-action@v5
with:
images: meshtastic/meshtasticd
tags: |
+4 -1
View File
@@ -11,7 +11,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-copr-hook:
@@ -21,6 +22,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{ github.ref }}
repository: ${{ github.repository }}
- name: Trigger COPR build
uses: vidplace7/copr-build@main
+21 -18
View File
@@ -15,7 +15,8 @@ on:
- "**.md"
- version.properties
pull_request:
# Note: This is different from "pull_request". Need to specify ref when doing checkouts.
pull_request_target:
branches:
- master
- develop
@@ -28,8 +29,6 @@ on:
workflow_dispatch:
permissions: read-all
jobs:
setup:
strategy:
@@ -89,6 +88,8 @@ jobs:
- uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check ${{ matrix.check.board }}
uses: meshtastic/gh-action-firmware@main
with:
@@ -125,16 +126,9 @@ jobs:
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
permissions: # Needed for dorny/test-reporter.
contents: read
actions: read
checks: write
uses: ./.github/workflows/test_native.yml
docker:
permissions: # Needed for pushing to GHCR.
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -159,6 +153,9 @@ jobs:
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
if: github.repository == 'meshtastic/firmware'
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
@@ -176,6 +173,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8
with:
@@ -231,6 +231,13 @@ jobs:
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
shame:
if: github.repository == 'meshtastic/firmware'
continue-on-error: true
@@ -238,7 +245,7 @@ jobs:
needs: [build]
steps:
- uses: actions/checkout@v6
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
with:
filter: blob:none # means we download all the git history but none of the commit (except ones with checkout like the head)
fetch-depth: 0
@@ -256,26 +263,24 @@ jobs:
overwrite: true
path: manifests-new/*.mt.json
- name: Find the merge base
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request_target'
run: echo "MERGE_BASE=$(git merge-base "origin/$base" "$head")" >> $GITHUB_ENV
env:
base: ${{ github.base_ref }}
head: ${{ github.sha }}
# Currently broken (for-loop through EVERY artifact -- rate limiting)
# - name: Download the old manifests
# if: github.event_name == 'pull_request'
# if: github.event_name == 'pull_request_target'
# run: gh run download -R "$repo" --name "manifests-$merge_base" --dir manifest-old/
# env:
# GH_TOKEN: ${{ github.token }}
# merge_base: ${{ env.MERGE_BASE }}
# repo: ${{ github.repository }}
# - name: Do scan and post comment
# if: github.event_name == 'pull_request'
# if: github.event_name == 'pull_request_target'
# run: python3 bin/shame.py ${{ github.event.pull_request.number }} manifests-old/ manifests-new/
release-artifacts:
permissions: # Needed for 'gh release upload'.
contents: write
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
outputs:
@@ -367,8 +372,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
permissions: # Needed for 'gh release upload'.
contents: write
strategy:
fail-fast: false
matrix:
+371
View File
@@ -0,0 +1,371 @@
name: Merge Queue
# Not sure how concurrency works in merge_queue, removing for now.
# concurrency:
# group: merge-queue-${{ github.head_ref || github.run_id }}
# cancel-in-progress: true
on:
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
merge_group:
jobs:
setup:
strategy:
fail-fast: true
matrix:
arch:
- all
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
else
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr)
fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
outputs:
all: ${{ steps.jsonStep.outputs.all }}
check: ${{ steps.jsonStep.outputs.check }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
outputs:
long: ${{ steps.version.outputs.long }}
deb: ${{ steps.version.outputs.deb }}
check:
needs: setup
strategy:
fail-fast: true
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v6
- name: Build base
id: base
uses: ./.github/actions/setup-base
- name: Check ${{ matrix.check.board }}
run: bin/check-all.sh ${{ matrix.check.board }}
build:
needs: [setup, version]
strategy:
matrix:
build: ${{ fromJson(needs.setup.outputs.all) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.platform }}
build-debian-src:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') }}
uses: ./.github/workflows/test_native.yml
docker:
strategy:
fail-fast: false
matrix:
distro: [debian, alpine]
platform: [linux/amd64, linux/arm64, linux/arm/v7]
pio_env: [native, native-tft]
exclude:
- distro: alpine
platform: linux/arm/v7
- pio_env: native-tft
platform: linux/arm64
- pio_env: native-tft
platform: linux/arm/v7
uses: ./.github/workflows/docker_build.yml
with:
distro: ${{ matrix.distro }}
platform: ${{ matrix.platform }}
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
pio_env: ${{ matrix.pio_env }}
push: false
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v8
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v7
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v8
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
# For diagnostics
- name: Show artifacts
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v7
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
- version
- gather-artifacts
- build-debian-src
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Create release
uses: softprops/action-gh-release@v2
id: create_release
with:
draft: true
prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }}
body: |
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v8
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v8
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output/pio-deps-native-tft
- name: Zip Linux sources
working-directory: output
run: |
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add Linux sources to GtiHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-firmware:
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v8
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
- name: Display structure of downloaded files
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh || true
chmod +x ./output/device-update.sh || true
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v8
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./elfs
- name: Zip debug elfs
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
# For diagnostics
- name: Display structure of downloaded files
run: ls -lR
- name: Add bins and debug elfs to GitHub Release
# Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }}
run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-firmware:
runs-on: ubuntu-24.04
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-firmware, version]
env:
targets: |-
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v8
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./publish
- name: Publish firmware to meshtastic.github.io
uses: peaceiris/actions-gh-pages@v4
env:
# On event/* branches, use the event name as the destination prefix
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
with:
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
external_repository: meshtastic/meshtastic.github.io
publish_branch: master
publish_dir: ./publish
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
keep_files: true
user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com
commit_message: ${{ needs.version.outputs.long }}
enable_jekyll: true
+2 -1
View File
@@ -18,7 +18,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
+4 -1
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
pkg-pio-libdeps:
@@ -26,6 +27,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Setup Python
uses: actions/setup-python@v6
+4 -2
View File
@@ -16,7 +16,8 @@ on:
type: string
permissions:
contents: read
contents: write
packages: write
jobs:
build-debian-src:
@@ -35,6 +36,8 @@ jobs:
with:
submodules: recursive
path: meshtasticd
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Install deps
shell: bash
@@ -67,6 +70,5 @@ jobs:
- name: Publish with dput
if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
timeout-minutes: 15 # dput is terrible, sometimes runs 'forever'
run: |
dput ${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes
+8 -1
View File
@@ -16,6 +16,8 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
submodules: recursive
- name: Setup native build
@@ -70,6 +72,8 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
submodules: recursive
- name: Setup native build
@@ -124,6 +128,9 @@ jobs:
if: always()
steps:
- uses: actions/checkout@v6
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -136,7 +143,7 @@ jobs:
merge-multiple: true
- name: Test Report
uses: dorny/test-reporter@v3.0.0
uses: dorny/test-reporter@v2.6.0
with:
name: PlatformIO Tests
path: testreport.xml
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
node-version: 24
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@v4
with:
version: latest
+1 -1
View File
@@ -6,7 +6,7 @@ permissions: read-all
jobs:
update-protobufs:
runs-on: ubuntu-latest
permissions: # Needed for peter-evans/create-pull-request.
permissions:
contents: write
pull-requests: write
steps:
+9 -9
View File
@@ -4,31 +4,31 @@ cli:
plugins:
sources:
- id: trunk
ref: v1.7.6
ref: v1.7.5
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.513
- renovate@43.104.1
- checkov@3.2.506
- renovate@43.52.0
- prettier@3.8.1
- trufflehog@3.94.2
- trufflehog@3.93.6
- yamllint@1.38.0
- bandit@1.9.4
- trivy@0.69.3
- taplo@0.10.0
- ruff@0.15.9
- ruff@0.15.4
- isort@8.0.1
- markdownlint@0.48.0
- oxipng@10.1.0
- svgo@4.0.1
- actionlint@1.7.12
- svgo@4.0.0
- actionlint@1.7.11
- flake8@7.3.0
- hadolint@2.14.0
- shfmt@3.6.0
- shellcheck@0.11.0
- black@26.3.1
- black@26.1.0
- git-diff-check
- gitleaks@8.30.1
- gitleaks@8.30.0
- clang-format@16.0.3
ignore:
- linters: [ALL]
@@ -1,9 +1,3 @@
Meta:
name: BananaPi-BPI-R4-sx1262
support: community
compatible:
- bananapi_bpi-r4 # OpenWrt target
Lora:
Module: sx1262 # BananaPi-BPI-R4 SPI via 26p GPIO Header
## CS: 28
@@ -1,10 +1,4 @@
## https://www.mikroe.com/lr-iot-click
Meta:
name: OpenWRT One mikroBUS LR-IOT-CLICK
support: community
compatible:
- openwrt_one # OpenWrt target
Lora:
Module: lr1110 # OpenWRT ONE mikroBUS with LR-IOT-CLICK
# CS: 25
@@ -1,9 +1,3 @@
Meta:
name: OpenWRT One mikroBUS sx1262
support: community
compatible:
- openwrt_one # OpenWrt target
Lora:
Module: sx1262
IRQ: 10
-28
View File
@@ -1,28 +0,0 @@
# meshtasticd configuration files
This directory contains YAML configuration files for meshtasticd. Each file describes a specific hardware configuration, including the LoRa module and pin assignments. These configurations are used by meshtasticd to correctly interface with the hardware.
## Metadata structure
Each configuration file includes a `Meta` section that provides information about the configuration.
This configuration is consumed by configuration-selection tools.
```yaml
Meta:
name: MeshAdv-Pi E22-900M30S # A unique identifier for this configuration.
support: community # community, official, or deprecated; determined by Meshtastic Leads.
compatible: # A list of compatible products or platforms.
- raspberry-pi
```
`name`: A unique identifier for the configuration, typically reflecting the hardware it supports.
`support`: Indicates the level of support for this configuration. It can be one of the following:
- `community`: Supported by the Meshtastic community. Meshtastic Members may not possess, or have not tested this configuration.
- `official`: Fully supported by Meshtastic. Meshtastic Members have tested and verified this configuration.
- `deprecated`: No longer recommended for deployment by Meshtastic.
`compatible`: A list of compatible products or platforms that can use this configuration.
This will vary depending on the intended use case / platform.
Multiple compatible entries can be included. E.g. Armbian `BOARD` value or OpenWrt `TARGET` value.
These tags can be consumed by different configuration-selection tools, filtering based upon their platform/etc.
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare 1.44inch LCD HAT
support: community
compatible:
- raspberry-pi
### Waveshare 1.44inch LCD HAT
Display:
Panel: ST7735S
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare 2.8inch LCD HAT
support: community
compatible:
- raspberry-pi
Display:
### Waveshare 2.8inch RPi LCD
@@ -1,22 +1,20 @@
---
Meta:
name: Femtofox Ebyte E80-900M22S with TCXO
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E80-900M22S
## This is a bit experimental
##
##
Module: lr1121
gpiochip: 1 # subtract 32 from the gpio numbers
DIO3_TCXO_VOLTAGE: 1.8
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
@@ -1,24 +1,21 @@
---
Meta:
name: Femtofox SX1262 TCXO
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup
## HT-RA62 (Has internal switching, but whatever)
## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN)
## Will work with any module with or without RF switching, and with TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E22-900M30S, E22-900M22S with or without external RF switching setup
## HT-RA62 (Has internal switching, but whatever)
## Seeed WIO SX1262 (already has TXEN-DIO2 link, but needs RXEN)
## Will work with any module with or without RF switching, and with TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
@@ -1,24 +1,21 @@
---
Meta:
name: Femtofox SX1262 XTAL
support: community
compatible:
- luckfox-pico-mini # Armbian
Lora:
## Ebyte E22-900MM22S with no external RF switching setup
## Waveshare SX126X XXXM, AI Thinker RA-01SH
## Will work with any module with or without RF switching and no TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: false
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
---
Lora:
## Ebyte E22-900MM22S with no external RF switching setup
## Waveshare SX126X XXXM, AI Thinker RA-01SH
## Will work with any module with or without RF switching and no TCXO
Module: sx1262
gpiochip: 1 # subtract 32 from the gpio numbers
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: false
CS: 16 #pin6 / GPIO48 1C0
IRQ: 23 #pin17 / GPIO55 1C7
Busy: 22 #pin16 / GPIO54 1C6
Reset: 25 #pin13 / GPIO57 1D1
RXen: 24 #pin12 / GPIO56 1D0 # Not strictly needed for auto-switching, but why complicate things?
# TXen: bridge to DIO2 on E22 module
spidev: spidev0.0 #pins are (CS=16, CLK=17, MOSI=18, MISO=19)
spiSpeed: 2000000
General:
MACAddressSource: eth0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Adafruit RFM9x
support: deprecated
compatible:
- raspberry-pi
Lora:
Module: RF95 # Adafruit RFM9x
Reset: 25
-6
View File
@@ -1,11 +1,5 @@
# MeshAdv-Pi E22-900M30S
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
Meta:
name: MeshAdv-Pi E22-900M30S
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262
CS: 21
@@ -1,11 +1,5 @@
# MeshAdv Mini E22-900M22S
# https://github.com/chrismyers2000/MeshAdv-Mini
Meta:
name: MeshAdv Mini E22-900M22S
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Ebyte E22-900M22S
CS: 8
+1 -7
View File
@@ -1,12 +1,6 @@
Meta:
name: RAK6421 + RAK13300 Slot 1
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 1
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
+1 -7
View File
@@ -1,11 +1,5 @@
Meta:
name: RAK6421 + RAK13300 Slot 2
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 2 pins
### RAK13300in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
+2 -8
View File
@@ -1,12 +1,6 @@
Meta:
name: RAK6421 + RAK13302 Slot 1
support: official
compatible:
- raspberry-pi
Lora:
### RAK13302 in Slot 1
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
@@ -19,4 +13,4 @@ Lora:
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]
TX_GAIN_LORA: [8]
+2 -8
View File
@@ -1,11 +1,5 @@
Meta:
name: RAK6421 + RAK13302 Slot 2
support: official
compatible:
- raspberry-pi
Lora:
### RAK13302 in Slot 2 pins
### RAK13300in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
@@ -15,4 +9,4 @@ Lora:
- 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]
TX_GAIN_LORA: [8]
+2 -8
View File
@@ -1,12 +1,6 @@
Meta:
name: RAK6421 + RAK13300 Slot 1 (Autoconf default)
support: official
compatible:
- raspberry-pi
Lora:
### RAK13300 in Slot 1
### RAK13300in Slot 1
Module: sx1262
IRQ: 22 #IO6
Reset: 16 # IO4
@@ -18,4 +12,4 @@ Lora:
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# 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]
@@ -1,9 +1,3 @@
Meta:
name: Luckfox Lyra PicoCalc Wio LoRa SX1262
support: official
compatible:
- luckfox-lyra-plus # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
-7
View File
@@ -2,13 +2,6 @@
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 1 Watt Lyra Ultra hat
Meta:
name: wehooper4 Luckfox Ultra 1W
support: community
compatible:
- luckfox-pico-ultra # Armbian
- luckfox-lyra-ultra # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
-7
View File
@@ -2,13 +2,6 @@
# Enable overlay 'luckfox-lyra-ultra-w-spi0-cs0-spidev' with armbian-config
# https://github.com/wehooper4/Meshtastic-Hardware/tree/main/Luckfox%20Ultra%20Hat
# 2 Watt Lyra Ultra hat
Meta:
name: wehooper4 Luckfox Ultra 2W
support: community
compatible:
- luckfox-pico-ultra # Armbian
- luckfox-lyra-ultra # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
@@ -2,12 +2,6 @@
# Enable overlay 'luckfox-lyra-plus-spi0-cs0_rmio13-spidev' with armbian-config
# Waveshare LoRa HAT for Raspberry Pi Pico
# https://www.waveshare.com/wiki/Pico-LoRa-SX1262
Meta:
name: Waveshare LoRa HAT for Raspberry Pi Pico
support: community
compatible:
- luckfox-lyra-plus # Armbian
Lora:
Module: sx1262
DIO2_AS_RF_SWITCH: true
@@ -1,39 +0,0 @@
# MeshAdv-Pi E22-900M30S
# https://github.com/chrismyers2000/MeshAdv-Pi-Hat
Meta:
name: MeshAdv-Pi E22-900M30S
support: community
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262
CS: # GPIO0_C2 (physical 40)
pin: 18
gpiochip: 0
line: 18
IRQ: # GPIO1_D1 (physical 36)
pin: 57
gpiochip: 1
line: 25
Busy: # GPIO0_C1 (physical 38)
pin: 17
gpiochip: 0
line: 17
Reset: # GPIO0_B6 (physical 12)
pin: 14
gpiochip: 0
line: 14
TXen: # GPIO1_C2 (physical 33)
pin: 50
gpiochip: 1
line: 18
RXen: # GPIO1_D2 (physical 32)
pin: 58
gpiochip: 1
line: 26
DIO3_TCXO_VOLTAGE: true
# Only for E22-900M33S:
# Limit the output power to 8 dBm
# SX126X_MAX_POWER: 8
spidev: spidev0.0
@@ -1,33 +0,0 @@
# MeshAdv Mini E22-900M22S
# https://github.com/chrismyers2000/MeshAdv-Mini
Meta:
name: MeshAdv Mini E22-900M22S
support: community
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262 # Ebyte E22-900M22S
CS: # GPIO0_B2_d (phys 24, RPi CE0)
pin: 10
gpiochip: 0
line: 10
IRQ: # GPIO1_D1_d (phys 36, RPi GPIO16)
pin: 57
gpiochip: 1
line: 25
Busy: # GPIO0_C1_d (phys 38, RPi GPIO20)
pin: 17
gpiochip: 0
line: 17
Reset: # GPIO0_B4_d (phys 18, RPi GPIO24)
pin: 12
gpiochip: 0
line: 12
RXen: # GPIO1_D2_d (phys 32, RPi GPIO12)
pin: 58
gpiochip: 1
line: 26
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
spidev: spidev0.0
@@ -1,38 +0,0 @@
Meta:
name: RAK6421 + RAK13300 Slot 1
support: community # Promote when tested
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262
IRQ: # GPIO0_A5 (IO6)
pin: 5
gpiochip: 0
line: 5
Reset: # GPIO1_D1 (IO4)
pin: 57
gpiochip: 1
line: 25
Busy: # GPIO0_B4 (IO5)
pin: 12
gpiochip: 0
line: 12
# Ant_sw: # GPIO1_C2 (IO3)
# pin: 50
# gpiochip: 1
# line: 18
Enable_Pins:
- pin: 58 # GPIO1_D2
gpiochip: 1
line: 26
- pin: 50 # GPIO1_C2
gpiochip: 1
line: 18
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# CS: # GPIO0_B2
# pin: 10
# gpiochip: 0
# line: 10
@@ -1,36 +0,0 @@
Meta:
name: RAK6421 + RAK13300 Slot 2
support: community # Promote when tested
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262
IRQ: # GPIO0_B6 (IO6)
pin: 14
gpiochip: 0
line: 14
Reset: # GPIO0_B4 (IO4)
pin: 12
gpiochip: 0
line: 12
Busy: # GPIO1_C0 (IO5)
pin: 48
gpiochip: 1
line: 16
# Ant_sw: # GPIO0_B5 (IO3)
# pin: 13
# gpiochip: 0
# line: 13
Enable_Pins:
- pin: 51 # GPIO1_C3
gpiochip: 1
line: 19
- pin: 13 # GPIO0_B5
gpiochip: 0
line: 13
spidev: spidev0.1
# CS: # GPIO0_B1
# pin: 9
# gpiochip: 0
# line: 9
@@ -1,39 +0,0 @@
Meta:
name: RAK6421 + RAK13300 Slot 1
support: community # Promote when tested
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262
IRQ: # GPIO0_A5 (IO6)
pin: 5
gpiochip: 0
line: 5
Reset: # GPIO1_D1 (IO4)
pin: 57
gpiochip: 1
line: 25
Busy: # GPIO0_B4 (IO5)
pin: 12
gpiochip: 0
line: 12
# Ant_sw: # GPIO1_C2 (IO3)
# pin: 50
# gpiochip: 1
# line: 18
Enable_Pins:
- pin: 58 # GPIO1_D2
gpiochip: 1
line: 26
- pin: 50 # GPIO1_C2
gpiochip: 1
line: 18
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.0
# CS: # GPIO0_B2
# pin: 10
# gpiochip: 0
# line: 10
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,37 +0,0 @@
Meta:
name: RAK6421 + RAK13300 Slot 2
support: community # Promote when tested
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262
IRQ: # GPIO0_B6 (IO6)
pin: 14
gpiochip: 0
line: 14
Reset: # GPIO0_B4 (IO4)
pin: 12
gpiochip: 0
line: 12
Busy: # GPIO1_C0 (IO5)
pin: 48
gpiochip: 1
line: 16
# Ant_sw: # GPIO0_B5 (IO3)
# pin: 13
# gpiochip: 0
# line: 13
Enable_Pins:
- pin: 51 # GPIO1_C3
gpiochip: 1
line: 19
- pin: 13 # GPIO0_B5
gpiochip: 0
line: 13
spidev: spidev0.1
# CS: # GPIO0_B1
# pin: 9
# gpiochip: 0
# line: 9
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,30 +0,0 @@
Meta:
name: Waveshare SX1262
support: deprecated
compatible:
- luckfox-lyra-zero-w # Armbian
Lora:
Module: sx1262 # Waveshare SX126X XXXM
DIO2_AS_RF_SWITCH: true
CS: # GPIO0_C2 (physical 40)
pin: 18
gpiochip: 0
line: 18
IRQ: # GPIO1_D1 (physical 36)
pin: 57
gpiochip: 1
line: 25
Busy: # GPIO0_C1 (physical 38)
pin: 17
gpiochip: 0
line: 17
Reset: # GPIO0_B6 (physical 12)
pin: 14
gpiochip: 0
line: 14
SX126X_ANT_SW: # GPIO1_B3 (physical 31)
pin: 43
gpiochip: 1
line: 11
spidev: spidev0.0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Lora Meshstick SX1262
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Lora Meshstick SX1262
support: community
compatible:
- usb
Lora:
Module: lr1121
CS: 0
+1 -7
View File
@@ -1,11 +1,5 @@
Meta:
name: Pinedio USB SX1262
support: deprecated
compatible:
- usb
Lora:
Module: sx1262
CS: 0
IRQ: 10
spidev: ch341
spidev: ch341
@@ -1,9 +1,3 @@
Meta:
name: raxda-rock2f-starter-edition-hat
support: community
compatible:
- rock-2f # Armbian
Lora:
### Raxda Rock 2F running Armbian Linux 6.1.99-vendor-rk35xx
@@ -1,11 +1,5 @@
# https://www.waveshare.com/core1262-868m.htm
# https://github.com/markbirss/lora-starter-edition-sx1262-i2c
Meta:
name: lora-starter-edition-sx1262-i2c
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Starter Edition SX1262 I2C Raspberry Pi HAT
DIO2_AS_RF_SWITCH: true
@@ -1,9 +1,3 @@
Meta:
name: meshstick-1262
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: meshtoad-e22
support: official
compatible:
- usb
Lora:
Module: sx1262
CS: 0
@@ -1,9 +1,3 @@
Meta:
name: umesh-1262-30dbm
support: community
compatible:
- clockwork-uconsole
Lora:
Module: sx1262
CS: 0
@@ -1,9 +1,3 @@
Meta:
name: umesh-1268-30dbm
support: community
compatible:
- clockwork-uconsole
Lora:
Module: sx1268
CS: 0
-6
View File
@@ -1,9 +1,3 @@
Meta:
name: Waveshare SX1262
support: deprecated
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Waveshare SX126X XXXM
DIO2_AS_RF_SWITCH: true
@@ -1,12 +1,5 @@
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
# https://github.com/markbirss/lora-ws-raspberry-pi-pico-to-rpi-adapter
Meta:
name: ws-raspberry-pico-to-rpi-adapter
support: community
compatible:
- raspberry-pi
Lora:
Module: sx1262 # Waveshare Raspberry Pi Pico to Raspberry Pi HAT Adapter
DIO2_AS_RF_SWITCH: true
@@ -15,12 +15,6 @@
# 5 CS 24
# 26 DIO1/IRQ 26
Meta:
name: ws-raspberry-pico-to-orangepi-03
support: community
compatible:
- orange-pi-zero-3 # Armbian
Lora:
Module: sx1262 # Waveshare Raspberry Pico Lora module
DIO2_AS_RF_SWITCH: true
+1 -1
View File
@@ -43,7 +43,7 @@ for pio_env in pio_envs:
env = {
"ci": {"board": pio_env, "platform": env_platform},
"board_level": cfg.get(f"env:{pio_env}", "board_level", default=None),
"board_check": cfg.get(f"env:{pio_env}", "board_check", default="false").strip().lower() == "true",
"board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)),
}
all_envs.append(env)
@@ -87,9 +87,6 @@
</screenshots>
<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">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.20</url>
</release>
-54
View File
@@ -1,54 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x2886", "0x1667"]
],
"usb_product": "HT-n5262G",
"mcu": "nrf52840",
"variant": "heltec_mesh_node_t096",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org/",
"vendor": "Heltec"
}
-38
View File
@@ -1,38 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T5-ePaper-S3",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"url": "https://lilygo.cc/products/t5-e-paper-s3-pro",
"vendor": "LILYGO"
}
-29
View File
@@ -1,29 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32_out.ld"
},
"core": "esp32",
"extra_flags": "-DARDUINO_T_Beam",
"f_cpu": "240000000L",
"f_flash": "40000000L",
"flash_mode": "dio",
"mcu": "esp32",
"variant": "tbeam"
},
"connectivity": ["wifi", "bluetooth", "can", "ethernet"],
"debug": {
"openocd_board": "esp-wroom-32.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "TTGO T-Beam (PSRAM Disabled)",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 1310720,
"maximum_size": 4194304,
"require_upload_port": true,
"speed": 460800
},
"url": "https://github.com/LilyGO/TTGO-T-Beam",
"vendor": "TTGO"
}
-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
* Version 2.7.20
+2 -11
View File
@@ -10,10 +10,6 @@ export PLATFORMIO_SETTING_CHECK_PRUNE_SYSTEM_THRESHOLD=10240
# Download libraries to `pio`
platformio pkg install -e native-tft
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
tar -cf pio.tar pio/
rm -rf pio
@@ -27,10 +23,5 @@ rm -rf debian/changelog
dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \
"GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES"
if [[ -n $GPG_KEY_ID ]]; then
# Build and sign the source deb
debuild -S -nc -k"$GPG_KEY_ID"
else
# Build the source deb without signing (forks)
debuild -S -nc
fi
# Build the source deb
debuild -S -nc -k"$GPG_KEY_ID"
+17 -21
View File
@@ -77,10 +77,10 @@ lib_deps =
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip
# renovate: datasource=github-tags depName=Nanopb packageName=nanopb/nanopb
https://github.com/nanopb/nanopb/archive/refs/tags/nanopb-0.4.9.1.zip
# renovate: datasource=github-tags depName=ErriezCRC32 packageName=Erriez/ErriezCRC32
https://github.com/Erriez/ErriezCRC32/archive/refs/tags/1.0.1.zip
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
nanopb/Nanopb@0.4.91
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
erriez/ErriezCRC32@1.0.1
; Used for the code analysis in PIO Home / Inspect
check_tool = cppcheck
@@ -95,38 +95,34 @@ check_flags =
framework = arduino
lib_deps =
${env.lib_deps}
# renovate: datasource=github-tags depName=NonBlockingRTTTL packageName=end2endzone/NonBlockingRTTTL
https://github.com/end2endzone/NonBlockingRTTTL/archive/refs/tags/1.4.0.zip
build_unflags =
-std=c++11
-std=gnu++11
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL
end2endzone/NonBlockingRTTTL@1.4.0
build_flags = ${env.build_flags} -Os
-std=gnu++17
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
; Common libs for communicating over TCP/IP networks such as MQTT
[networking_base]
lib_deps =
# renovate: datasource=github-tags depName=TBPubSubClient packageName=thingsboard/pubsubclient
https://github.com/thingsboard/pubsubclient/archive/refs/tags/v2.12.1.zip
# renovate: datasource=github-tags depName=NTPClient packageName=arduino-libraries/NTPClient
https://github.com/arduino-libraries/NTPClient/archive/refs/tags/3.2.1.zip
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
; Extra TCP/IP networking libs for supported devices
[networking_extra]
lib_deps =
# renovate: datasource=github-tags depName=Syslog packageName=arcao/Syslog
https://github.com/arcao/Syslog/archive/refs/tags/v2.0.zip
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0
[radiolib_base]
lib_deps =
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.6.0
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/1897dd17fceb1f101bb1a3245680aa3439edcfdd.zip
https://github.com/meshtastic/device-ui/archive/6c75195e9987b7a49563232234f2f868dd343cae.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
@@ -233,5 +229,5 @@ lib_deps =
[environmental_extra_no_bsec]
lib_deps =
${environmental_extra_common.lib_deps}
# renovate: datasource=custom.pio depName=Adafruit_BME680 packageName=adafruit/library/Adafruit BME680 Library
adafruit/Adafruit BME680 Library@2.0.6
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
adafruit/Adafruit BME680 Library@^2.0.5
-2
View File
@@ -4,8 +4,6 @@
":dependencyDashboard",
":semanticCommitTypeAll(chore)",
":ignoreModulesAndTests",
":noUnscheduledUpdates",
"schedule:daily",
"group:recommended",
"replacements:all",
"workarounds:all"
+2 -10
View File
@@ -98,6 +98,7 @@ Syslog &Syslog::logMask(uint8_t priMask)
void Syslog::enable()
{
this->_client->begin(this->_port);
this->_enabled = true;
}
@@ -165,21 +166,14 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
if ((pri & LOG_FACMASK) == 0)
pri = LOG_MAKEPRI(LOG_FAC(this->_priDefault), pri);
// W5100S: acquire UDP socket on-demand to avoid permanent socket consumption
if (!this->_client->begin(this->_port)) {
return false;
}
if (this->_server != NULL) {
result = this->_client->beginPacket(this->_server, this->_port);
} else {
result = this->_client->beginPacket(this->_ip, this->_port);
}
if (result != 1) {
this->_client->stop();
if (result != 1)
return false;
}
this->_client->print('<');
this->_client->print(pri);
@@ -199,8 +193,6 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
this->_client->print(message);
this->_client->endPacket();
this->_client->stop(); // W5100S: release UDP socket for other services
return true;
}
-8
View File
@@ -35,11 +35,6 @@
#include "nrfx_power.h"
#endif
#if defined(ARCH_NRF52)
#include "Nrf52SaadcLock.h"
#include "concurrency/LockGuard.h"
#endif
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#include "target_specific.h"
@@ -333,9 +328,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
scaled *= operativeAdcMultiplier;
#else // block for all other platforms
#ifdef ARCH_NRF52
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
#endif
for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
raw += analogRead(BATTERY_PIN);
}
+1 -1
View File
@@ -345,7 +345,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
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";
log(logLevel, " +------------------------------------------------+ +----------------+");
+1 -1
View File
@@ -44,7 +44,7 @@ class RedirectablePrint : public Print
/** like printf but va_list based */
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, ...);
+1 -56
View File
@@ -6,11 +6,6 @@
#include "Tone.h"
#endif
#if defined(HAS_I2S)
#include "main.h"
#include <unordered_map>
#endif
#if !defined(ARCH_PORTDUINO)
extern "C" void delay(uint32_t dwMs);
#endif
@@ -55,50 +50,6 @@ const int DURATION_1_2 = 500; // 1/2 note
const int DURATION_3_4 = 750; // 3/4 note
const int DURATION_1_1 = 1000; // 1/1 note
#ifdef HAS_I2S
void playTonesRTTTL(const ToneDuration *tone_durations, int size)
{
// translate ToneDuration[] to RTTTL string and play using audioThread
static std::unordered_map<int, std::string> freqToNote = {
{NOTE_C3, "c4"}, {NOTE_CS3, "c#4"}, {NOTE_D3, "d4"}, {NOTE_DS3, "d#4"}, {NOTE_E3, "e4"}, {NOTE_F3, "f4"},
{NOTE_FS3, "f#4"}, {NOTE_G3, "g4"}, {NOTE_GS3, "g#4"}, {NOTE_A3, "a4"}, {NOTE_AS3, "a#4"}, {NOTE_B3, "b4"},
{NOTE_C4, "c5"}, {NOTE_E4, "e5"}, {NOTE_G4, "g5"}, {NOTE_A4, "a5"}, {NOTE_C5, "c6"}, {NOTE_E5, "e6"},
{NOTE_G5, "g6"}, {NOTE_F5, "f6"}, {NOTE_G6, "g7"}, {NOTE_E7, "e8"}};
char rtttl[128] = "tone:d=32,o=4,b=200:"; // default duration and octave
for (int i = 0; i < size; i++) {
const auto &td = tone_durations[i];
std::string note = "b4";
if (freqToNote.find(td.frequency_khz) != freqToNote.end()) {
note = freqToNote[td.frequency_khz];
}
int dur = 32; // default duration
if (td.duration_ms >= 1000)
dur = 1;
else if (td.duration_ms >= 500)
dur = 2;
else if (td.duration_ms >= 250)
dur = 4;
else if (td.duration_ms >= 125)
dur = 8;
else if (td.duration_ms >= 62)
dur = 16;
else
dur = 32;
char noteStr[64];
snprintf(noteStr, sizeof(noteStr), "%s,%d", note.c_str(), dur);
strncat(rtttl, noteStr, sizeof(rtttl) - strlen(rtttl) - 1);
audioThread->beginRttl(rtttl, strlen(rtttl));
while (audioThread->isPlaying()) {
delay(10);
}
return;
}
}
#endif
void playTones(const ToneDuration *tone_durations, int size)
{
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
@@ -106,13 +57,7 @@ void playTones(const ToneDuration *tone_durations, int size)
// Buzzer is disabled or not set to system tones
return;
}
#ifdef HAS_I2S
if (moduleConfig.external_notification.use_i2s_as_buzzer && audioThread) {
playTonesRTTTL(tone_durations, size);
return;
}
#endif
#if defined(PIN_BUZZER)
#ifdef PIN_BUZZER
if (!config.device.buzzer_gpio)
config.device.buzzer_gpio = PIN_BUZZER;
#endif
+1 -11
View File
@@ -151,19 +151,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#ifdef USE_KCT8103L_PA
// Power Amps are often non-linear, so we can use an array of values for the power curve
#if defined(HELTEC_WIRELESS_TRACKER_V2)
#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
#elif defined(HELTEC_MESH_NODE_T096)
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7
#else
// If a board enables USE_KCT8103L_PA but does not match a known variant and has
// not already provided a PA curve, fail at compile time to avoid unsafe defaults.
#if !defined(NUM_PA_POINTS) || !defined(TX_GAIN_LORA)
#error "USE_KCT8103L_PA is defined, but no PA gain curve (NUM_PA_POINTS / TX_GAIN_LORA) is configured for this board."
#endif
#endif
#endif
#ifdef RAK13302
@@ -510,6 +499,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
#define MESHTASTIC_EXCLUDE_TEXTMESSAGE 1
#define MESHTASTIC_EXCLUDE_TRAFFIC_MANAGEMENT 1
#define MESHTASTIC_EXCLUDE_ATAK 1
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
#define MESHTASTIC_EXCLUDE_NEIGHBORINFO 1
+1 -3
View File
@@ -341,9 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = DPS310;
break;
}
if (type == DPS310) {
break;
}
break;
default:
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // GET_ID
switch (registerValue) {
-28
View File
@@ -2,7 +2,6 @@
#include "configuration.h"
#include "detect/ScanI2C.h"
#include "main.h"
#include "modules/NodeInfoModule.h"
#include <Throttle.h>
#include <sys/time.h>
#include <time.h>
@@ -13,14 +12,6 @@ uint32_t lastSetFromPhoneNtpOrGps = 0;
static uint32_t lastTimeValidationWarning = 0;
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
static void triggerNodeInfoCheckOnTimeSource(RTCQuality oldQuality, RTCQuality newQuality)
{
if (oldQuality == RTCQualityNone && newQuality > RTCQualityNone && nodeInfoModule) {
LOG_DEBUG("Time source acquired (%s -> %s), triggering NodeInfo recheck", RtcName(oldQuality), RtcName(newQuality));
nodeInfoModule->triggerImmediateNodeInfoCheck();
}
}
RTCQuality getRTCQuality()
{
return currentQuality;
@@ -70,11 +61,9 @@ RTCSetResult readFromRTC()
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
RTCQuality oldQuality = currentQuality;
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
}
return RTCSetResultSuccess;
} else {
@@ -116,11 +105,9 @@ RTCSetResult readFromRTC()
LOG_DEBUG("Read RTC time from %s getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t.tm_year + 1900,
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
RTCQuality oldQuality = currentQuality;
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
}
return RTCSetResultSuccess;
} else {
@@ -152,11 +139,9 @@ RTCSetResult readFromRTC()
}
#endif
if (currentQuality == RTCQualityNone) {
RTCQuality oldQuality = currentQuality;
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
}
return RTCSetResultSuccess;
}
@@ -229,7 +214,6 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
}
if (shouldSet) {
RTCQuality oldQuality = currentQuality;
currentQuality = q;
lastSetMsec = now;
if (currentQuality >= RTCQualityNTP) {
@@ -297,7 +281,6 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
#endif
readFromRTC();
triggerNodeInfoCheckOnTimeSource(oldQuality, currentQuality);
return RTCSetResultSuccess;
} else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time
@@ -414,17 +397,6 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
return (currentQuality >= minQuality) ? getTime(local) : 0;
}
#ifdef PIO_UNIT_TESTING
void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot)
{
currentQuality = RTCQualityNone;
zeroOffsetSecs = 0;
timeStartMsec = millis() - (secondsSinceBoot * 1000);
lastSetFromPhoneNtpOrGps = 0;
lastTimeValidationWarning = 0;
}
#endif
time_t gm_mktime(const struct tm *tm)
{
#if !MESHTASTIC_EXCLUDE_TZ
-4
View File
@@ -54,10 +54,6 @@ uint32_t getValidTime(RTCQuality minQuality, bool local = false);
RTCSetResult readFromRTC();
#ifdef PIO_UNIT_TESTING
void setBootRelativeTimeForUnitTest(uint32_t secondsSinceBoot);
#endif
time_t gm_mktime(const struct tm *tm);
#define SEC_PER_DAY 86400
+1 -1
View File
@@ -1,6 +1,6 @@
#include "configuration.h"
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#ifdef USE_EINK
#include "EInkDisplay2.h"
#include "SPILock.h"
#include "main.h"
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#ifdef USE_EINK
#include "GxEPD2_BW.h"
#include <OLEDDisplay.h>
-427
View File
@@ -1,427 +0,0 @@
#include "EInkParallelDisplay.h"
#ifdef USE_EINK_PARALLELDISPLAY
#include "Wire.h"
#include "variant.h"
#include <Arduino.h>
#include <atomic>
#include <stdlib.h>
#include <string.h>
#include "FastEPD.h"
// Thresholds for choosing partial vs full update
#ifndef EPD_PARTIAL_THRESHOLD_ROWS
#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial
#endif
#ifndef EPD_FULLSLOW_PERIOD
#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh
#endif
#ifndef EPD_RESPONSIVE_MIN_MS
#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates
#endif
EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot)
{
LOG_INFO("init EInkParallelDisplay");
// Set dimensions in OLEDDisplay base class
this->geometry = GEOMETRY_RAWMODE;
this->displayWidth = width;
this->displayHeight = height;
// Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer
uint16_t shortSide = min(width, height);
uint16_t longSide = max(width, height);
if (shortSide % 8 != 0)
shortSide = (shortSide | 7) + 1;
this->displayBufferSize = longSide * (shortSide / 8);
#ifdef EINK_LIMIT_GHOSTING_PX
// allocate dirty pixel buffer same size as epaper buffers (rowBytes * height)
size_t rowBytes = (this->displayWidth + 7) / 8;
dirtyPixelsSize = rowBytes * this->displayHeight;
dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1);
ghostPixelCount = 0;
#endif
}
EInkParallelDisplay::~EInkParallelDisplay()
{
#ifdef EINK_LIMIT_GHOSTING_PX
if (dirtyPixels) {
free(dirtyPixels);
dirtyPixels = nullptr;
}
#endif
// If an async full update is running, wait for it to finish
if (asyncFullRunning.load()) {
// wait a short while for task to finish
for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) {
delay(50);
}
if (asyncTaskHandle) {
// Let it finish or delete it
vTaskDelete(asyncTaskHandle);
asyncTaskHandle = nullptr;
}
}
delete epaper;
}
/*
* Called by the OLEDDisplay::init() path.
*/
bool EInkParallelDisplay::connect()
{
LOG_INFO("Do EPD init");
if (!epaper) {
epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
// initialize all port 0 pins (0-7) as outputs / HIGH
for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT);
epaper->ioWrite(i, HIGH);
}
#else
#error "unsupported EPD device!"
#endif
}
// epaper->setRotation(rotation); // does not work, messes up width/height
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
epaper->fullUpdate(true);
#ifdef EINK_LIMIT_GHOSTING_PX
// After a full/clear the dirty tracking should be reset
resetGhostPixelTracking();
#endif
return true;
}
/*
* sendCommand - simple passthrough (not required for epd_driver-based path)
*/
void EInkParallelDisplay::sendCommand(uint8_t com)
{
LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com);
}
/*
* Start a background task that will perform a blocking fullUpdate(). This lets
* display() return quickly while the heavy refresh runs in the background.
*/
void EInkParallelDisplay::startAsyncFullUpdate(int clearMode)
{
if (asyncFullRunning.load())
return; // already running
asyncFullRunning.store(true);
// pass 'this' as parameter
BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t),
this, 2, &asyncTaskHandle,
#if CONFIG_FREERTOS_UNICORE
0
#else
1
#endif
);
if (rc != pdPASS) {
LOG_WARN("Failed to create async full-update task, falling back to blocking update");
epaper->fullUpdate(clearMode, false);
epaper->backupPlane();
asyncFullRunning.store(false);
asyncTaskHandle = nullptr;
}
}
/*
* FreeRTOS task entry: runs the full update and then backs up plane.
*/
void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
{
EInkParallelDisplay *self = static_cast<EInkParallelDisplay *>(pvParameters);
if (!self) {
vTaskDelete(nullptr);
return;
}
// choose CLEAR_SLOW occasionally
int clearMode = CLEAR_FAST;
if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) {
clearMode = CLEAR_SLOW;
self->fastRefreshCount = 0;
} else {
// when running async full, treat it as a full so reset fast count
self->fastRefreshCount = 0;
}
self->epaper->fullUpdate(clearMode, false);
self->epaper->backupPlane();
#ifdef EINK_LIMIT_GHOSTING_PX
// A full refresh clears ghosting state
self->resetGhostPixelTracking();
#endif
self->asyncFullRunning.store(false);
self->asyncTaskHandle = nullptr;
// delete this task
vTaskDelete(nullptr);
}
/*
* Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes
* buffer used by the FASTEPD library. For performance we write directly into FASTEPD's
* currentBuffer() while comparing against previousBuffer() to detect changed rows.
* After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according
* to a heuristic so only the minimal region is refreshed.
*/
void EInkParallelDisplay::display(void)
{
const uint16_t w = this->displayWidth;
const uint16_t h = this->displayHeight;
// Simple rate limiting: avoid very-frequent responsive updates
uint32_t nowMs = millis();
if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) {
LOG_DEBUG("rate-limited, skipping update");
return;
}
// bytes per row in epd format (one byte = 8 horizontal pixels)
const uint32_t rowBytes = (w + 7) / 8;
// Get pointers to internal buffers
uint8_t *cur = epaper->currentBuffer();
uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init
// Track changed row range while converting
int newTop = h; // min changed row (initialized to out-of-range)
int newBottom = -1; // max changed row
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// Track changed byte column range (for clipped fullUpdate fallback)
int newLeftByte = (int)rowBytes;
int newRightByte = -1;
#endif
// Compute a quick hash of the incoming OLED buffer (so we can skip identical frames)
uint32_t imageHash = 0;
uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size
for (uint32_t bi = 0; bi < bufBytes; ++bi) {
imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31);
}
if (imageHash == previousImageHash) {
// LOG_DEBUG("image identical to previous, skipping update");
return;
}
#ifdef EINK_LIMIT_GHOSTING_PX
// reset ghost count for this conversion pass; we'll mark bits that change
ghostPixelCount = 0;
#endif
// Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur,
// comparing against prev when available to detect changes.
for (uint32_t y = 0; y < h; ++y) {
const uint32_t base = (y >> 3) * w; // (y/8) * width
const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout
const uint32_t rowBase = y * rowBytes;
// process full 8-pixel bytes
for (uint32_t xb = 0; xb < rowBytes; ++xb) {
uint32_t x0 = xb * 8;
// read up to 8 source bytes (vertical-byte per column)
uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0;
uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0;
uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0;
uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0;
uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0;
uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0;
uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0;
uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0;
// build output byte: MSB = leftmost pixel
uint8_t out = 0;
out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00);
out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00);
out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00);
out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00);
out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00);
out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00);
out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00);
out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00);
// handle partial byte at end of row by masking off invalid bits
uint8_t mask = 0xFF;
uint32_t bitsRemain = (w > x0) ? (w - x0) : 0;
if (bitsRemain > 0 && bitsRemain < 8) {
mask = (uint8_t)(0xFF << (8 - bitsRemain));
out &= mask;
}
// invert to FASTEPD polarity
out = (~out) & mask;
uint32_t pos = rowBase + xb;
uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00;
// Consider this byte changed if previous buffer differs (or prev is null)
bool changed = (prev == nullptr) || (prevVal != out);
#ifdef EINK_LIMIT_GHOSTING_PX
if (changed && prev)
markDirtyBits(prev, pos, mask, out);
#endif
// mark row changed only if the previous buffer differs
if (changed) {
if (y < (uint32_t)newTop)
newTop = y;
if ((int)y > newBottom)
newBottom = y;
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// record changed column bytes
if ((int)xb < newLeftByte)
newLeftByte = (int)xb;
if ((int)xb > newRightByte)
newRightByte = (int)xb;
#endif
}
// Always write the computed value into the current buffer (avoid leaving stale bytes)
cur[pos] = (cur[pos] & ~mask) | out;
}
}
// If nothing changed, avoid any panel update
if (newBottom < 0) {
LOG_DEBUG("no pixel changes detected, skipping update (conv)");
previousImageHash = imageHash; // still remember that frame
return;
}
// Choose partial vs full update using heuristic
// Decide if we should force a full update after many fast updates
bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD);
#ifdef EINK_LIMIT_GHOSTING_PX
// If ghost pixels exceed limit, force a full update to clear ghosting
if (ghostPixelCount > ghostPixelLimit) {
LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit);
forceFull = true;
}
#endif
// Compute pixel bounds from newTop/newBottom
int startRow = (newTop / 8) * 8;
int endRow = (newBottom / 8) * 8 + 7;
LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes);
if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) {
// Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback.
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// Workaround for FastEPD partial update bug: use clipped fullUpdate instead
// Build a pixel rectangle for a clipped fullUpdate using the changed columns
int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0;
int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1);
BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1};
// LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h);
epaper->fullUpdate(CLEAR_FAST, false, &rect);
#else
// Use rows for partial update
LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow);
epaper->partialUpdate(true, startRow, endRow);
#endif
epaper->backupPlane();
fastRefreshCount++;
} else {
// Full update: run async if possible (startAsyncFullUpdate will fall back to blocking)
startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST);
}
lastUpdateMs = millis();
previousImageHash = imageHash;
// Keep same behavior as before
lastDrawMsec = millis();
}
#ifdef EINK_LIMIT_GHOSTING_PX
// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount
void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out)
{
// defensive: need dirtyPixels allocated and prevBuf valid
if (!dirtyPixels || !prevBuf)
return;
// 'out' is in FASTEPD polarity (1 = black, 0 = white)
uint8_t newBlack = out & mask; // bits that will be black now
uint8_t newWhite = (~out) & mask; // bits that will be white now
// previously recorded dirty bits for this byte
uint8_t before = dirtyPixels[pos];
// Ghost bits: bits that were previously marked dirty and are now being driven white
uint8_t ghostBits = before & newWhite;
if (ghostBits) {
ghostPixelCount += __builtin_popcount((unsigned)ghostBits);
}
// Only mark bits dirty when they turn black now (accumulate until a full refresh)
uint8_t newlyDirty = newBlack & (~before);
if (newlyDirty) {
dirtyPixels[pos] |= newlyDirty;
}
}
// reset ghost tracking (call after a full refresh)
void EInkParallelDisplay::resetGhostPixelTracking()
{
if (!dirtyPixels)
return;
memset(dirtyPixels, 0, dirtyPixelsSize);
ghostPixelCount = 0;
}
#endif
/*
* forceDisplay: use lastDrawMsec
*/
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
{
uint32_t now = millis();
if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
display();
return true;
}
return false;
}
void EInkParallelDisplay::endUpdate()
{
{
// ensure any async full update is started/completed
if (asyncFullRunning.load()) {
// nothing to do; background task will run and call backupPlane when done
} else {
epaper->fullUpdate(CLEAR_FAST, false);
epaper->backupPlane();
#ifdef EINK_LIMIT_GHOSTING_PX
resetGhostPixelTracking();
#endif
}
}
}
#endif
-69
View File
@@ -1,69 +0,0 @@
#pragma once
#include "configuration.h"
#ifdef USE_EINK_PARALLELDISPLAY
#include <OLEDDisplay.h>
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class FASTEPD;
/**
* Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library
*/
class EInkParallelDisplay : public OLEDDisplay
{
public:
enum EpdRotation {
EPD_ROT_LANDSCAPE = 0,
EPD_ROT_PORTRAIT = 90,
EPD_ROT_INVERTED_LANDSCAPE = 180,
EPD_ROT_INVERTED_PORTRAIT = 270,
};
EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation);
virtual ~EInkParallelDisplay();
// OLEDDisplay virtuals
bool connect() override;
void sendCommand(uint8_t com) override;
int getBufferOffset(void) override { return 0; }
void display(void) override;
bool forceDisplay(uint32_t msecLimit = 1000);
void endUpdate();
protected:
uint32_t lastDrawMsec = 0;
FASTEPD *epaper;
private:
// Async full-refresh support
std::atomic<bool> asyncFullRunning{false};
TaskHandle_t asyncTaskHandle = nullptr;
void startAsyncFullUpdate(int clearMode);
static void asyncFullUpdateTask(void *pvParameters);
#ifdef EINK_LIMIT_GHOSTING_PX
// helpers
void resetGhostPixelTracking();
void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out);
void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull);
// per-bit dirty buffer (same format as epaper buffers): one bit == one pixel
uint8_t *dirtyPixels = nullptr;
size_t dirtyPixelsSize = 0;
uint32_t ghostPixelCount = 0;
uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX;
#endif
EpdRotation rotation;
uint32_t previousImageHash = 0;
uint32_t lastUpdateMs = 0;
int fastRefreshCount = 0;
};
#endif
-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
+3 -12
View File
@@ -27,7 +27,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "EInkParallelDisplay.h"
#include <OLEDDisplay.h>
#include "DisplayFormatters.h"
@@ -365,14 +364,12 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK_PARALLELDISPLAY)
dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT);
#elif defined(USE_ST7567)
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -762,11 +759,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
}
// Tell EInk class to update the display
#if defined(USE_EINK_PARALLELDISPLAY)
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay();
#elif defined(USE_EINK)
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
#endif
#else
// No delay between UI frame rendering
if (forceUiUpdate) {
@@ -1005,10 +998,8 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
ui->update();
} while (ui->getUiState()->lastUpdate < startUpdate);
#if defined(USE_EINK_PARALLELDISPLAY)
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay(0);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
// Old EInkDisplay class
#if !defined(USE_EINK_DYNAMICDISPLAY)
static_cast<EInkDisplay *>(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit
#endif
@@ -1020,7 +1011,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
#ifdef EINK_HASQUIRK_GHOSTING
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
#else
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
#endif
}
#endif
+3 -3
View File
@@ -20,7 +20,7 @@
#include "graphics/fonts/OLEDDisplayFontsGR.h"
#endif
#if (defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)) && defined(USE_EINK)
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
#include "graphics/fonts/EinkDisplayFonts.h"
#endif
@@ -90,7 +90,7 @@
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)) && \
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
@@ -106,7 +106,7 @@
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#endif
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(T5_S3_EPAPER_PRO)
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK)
#undef FONT_SMALL
#undef FONT_MEDIUM
#undef FONT_LARGE
+6 -5
View File
@@ -121,10 +121,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
}
// === Screen Title ===
const char *headerTitle = titleStr ? titleStr : "";
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
if (config.display.heading_bold) {
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
}
}
display->setTextAlignment(TEXT_ALIGN_LEFT);
@@ -514,4 +515,4 @@ std::string sanitizeString(const std::string &input)
}
} // namespace graphics
#endif
#endif
+9 -9
View File
@@ -1348,7 +1348,7 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup();
tft->powerSaveOff();
#endif
@@ -1359,7 +1359,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#ifdef RAK14014
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172);
@@ -1375,7 +1375,7 @@ void TFTDisplay::sendCommand(uint8_t com)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep();
tft->powerSaveOn();
#endif
@@ -1386,7 +1386,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#ifdef UNPHONE
unphone.backlight(false); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#ifdef RAK14014
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(0);
#endif
@@ -1401,7 +1401,7 @@ void TFTDisplay::sendCommand(uint8_t com)
void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#ifdef RAK14014
// todo
#elif !defined(HACKADAY_COMMUNICATOR)
tft->setBrightness(_brightness);
@@ -1421,7 +1421,7 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
return tft->touch() != nullptr;
#else
return false;
@@ -1440,7 +1440,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
return tft->getTouch(x, y);
#else
return false;
@@ -1457,7 +1457,7 @@ bool TFTDisplay::connect()
{
concurrency::LockGuard g(spiLock);
LOG_INFO("Do TFT init");
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096)
#ifdef RAK14014
tft = new TFT_eSPI;
#elif defined(HACKADAY_COMMUNICATOR)
bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, 38 /* SCK */, 21 /* MOSI */, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
@@ -1494,7 +1494,7 @@ bool TFTDisplay::connect()
ft6336u.begin();
pinMode(SCREEN_TOUCH_INT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) || defined(HELTEC_MESH_NODE_T096)
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
tft->setRotation(1); // T-Deck has the TFT in landscape
#elif defined(T_WATCH_S3)
tft->setRotation(2); // T-Watch S3 left-handed orientation
+3 -9
View File
@@ -535,9 +535,6 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#ifndef T_DECK_PRO
barsOffset -= 12;
#endif
#if defined(T5_S3_EPAPER_PRO)
barsOffset += 60;
#endif
#endif
int barX = x + barsOffset;
if (currentResolution == ScreenResolution::UltraLow) {
@@ -587,12 +584,11 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap();
uint32_t heapTotal = memGet.getHeapSize();
uint32_t flashUsed = 0, flashTotal = 0;
#ifdef ESP32
#ifndef T5_S3_EPAPER_PRO
uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram();
uint32_t psramTotal = memGet.getPsramSize();
#endif
uint32_t flashUsed = 0, flashTotal = 0;
#ifdef ESP32
flashUsed = FSCom.usedBytes();
flashTotal = FSCom.totalBytes();
#endif
@@ -611,12 +607,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
// === Draw memory rows
drawUsageRow("Heap:", heapUsed, heapTotal, true);
#ifdef ESP32
#ifndef T5_S3_EPAPER_PRO
if (psramUsed > 0) {
line += 1;
drawUsageRow("PSRAM:", psramUsed, psramTotal);
}
#endif
if (flashTotal > 0) {
line += 1;
drawUsageRow("Flash:", flashUsed, flashTotal);
+4 -20
View File
@@ -1222,11 +1222,9 @@ void menuHandler::positionBaseMenu()
};
constexpr size_t baseCount = sizeof(baseOptions) / sizeof(baseOptions[0]);
static std::array<const char *, baseCount> baseLabels{};
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
constexpr size_t calibrateCount = sizeof(calibrateOptions) / sizeof(calibrateOptions[0]);
static std::array<const char *, baseCount> baseLabels{};
static std::array<const char *, calibrateCount> calibrateLabels{};
#endif
auto onSelection = [](const PositionMenuOption &option, int) -> void {
if (option.action == OptionsAction::Back) {
@@ -1252,11 +1250,9 @@ void menuHandler::positionBaseMenu()
screen->runNow();
break;
case PositionAction::CompassCalibrate:
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
if (accelerometerThread) {
accelerometerThread->calibrate(30);
}
#endif
break;
case PositionAction::GPSSmartPosition:
menuQueue = GpsSmartPositionMenu;
@@ -1274,15 +1270,11 @@ void menuHandler::positionBaseMenu()
};
BannerOverlayOptions bannerOptions;
#if !MESHTASTIC_EXCLUDE_ACCELEROMETER
if (accelerometerThread) {
bannerOptions = createStaticBannerOptions("GPS Action", calibrateOptions, calibrateLabels, onSelection);
} else {
bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection);
}
#else
bannerOptions = createStaticBannerOptions("GPS Action", baseOptions, baseLabels, onSelection);
#endif
screen->showOverlayBanner(bannerOptions);
}
@@ -2212,9 +2204,9 @@ void menuHandler::traceRouteMenu()
void menuHandler::testMenu()
{
enum optionsNumbers { Back, NumberPicker, ShowChirpy, TestAnnounce };
static const char *optionsArray[5] = {"Back"};
static int optionsEnumArray[5] = {Back};
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
@@ -2222,10 +2214,6 @@ void menuHandler::testMenu()
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
#ifdef HAS_I2S
optionsArray[options] = "Test Announce";
optionsEnumArray[options++] = TestAnnounce;
#endif
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Hidden Test Menu";
@@ -2240,10 +2228,6 @@ void menuHandler::testMenu()
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else if (selected == TestAnnounce) {
#ifdef HAS_I2S
audioThread->readAloud("This is a test of the emergency broadcast system. This is only a test.");
#endif
} else {
menuQueue = SystemBaseMenu;
screen->runNow();
+253 -75
View File
@@ -7,7 +7,6 @@
#include "NodeDB.h"
#include "UIRenderer.h"
#include "gps/RTC.h"
#include "graphics/EmoteRenderer.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
@@ -35,6 +34,44 @@ static std::vector<std::string> cachedLines;
static std::vector<int> cachedHeights;
static bool manualScrolling = false;
// UTF-8 skip helper
static inline size_t utf8CharLen(uint8_t c)
{
if ((c & 0xE0) == 0xC0)
return 2;
if ((c & 0xF0) == 0xE0)
return 3;
if ((c & 0xF8) == 0xF0)
return 4;
return 1;
}
// Remove variation selectors (FE0F) and skin tone modifiers from emoji so they match your labels
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)
float scrollY = 0.0f;
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)
{
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
@@ -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)
{
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 {
@@ -260,7 +417,13 @@ static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool i
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 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;
// Title string depending on mode
char titleStr[48];
snprintf(titleStr, sizeof(titleStr), "Messages");
static char titleBuf[32];
const char *titleStr = "Messages";
switch (currentMode) {
case ThreadMode::ALL:
snprintf(titleStr, sizeof(titleStr), "Messages");
titleStr = "Messages";
break;
case ThreadMode::CHANNEL: {
const char *cname = channels.getName(currentChannel);
if (cname && cname[0]) {
snprintf(titleStr, sizeof(titleStr), "#%s", cname);
snprintf(titleBuf, sizeof(titleBuf), "#%s", cname);
} else {
snprintf(titleStr, sizeof(titleStr), "Ch%d", currentChannel);
snprintf(titleBuf, sizeof(titleBuf), "Ch%d", currentChannel);
}
titleStr = titleBuf;
break;
}
case ThreadMode::DIRECT: {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentPeer);
if (node && node->has_user && node->user.short_name[0]) {
snprintf(titleStr, sizeof(titleStr), "@%s", node->user.short_name);
if (node && node->has_user) {
snprintf(titleBuf, sizeof(titleBuf), "@%s", node->user.short_name);
} else {
snprintf(titleStr, sizeof(titleStr), "@%08x", currentPeer);
snprintf(titleBuf, sizeof(titleBuf), "@%08x", currentPeer);
}
titleStr = titleBuf;
break;
}
}
@@ -501,50 +666,44 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender);
meshtastic_NodeInfoLite *node_recipient = nodeDB->getMeshNode(m.dest);
char senderName[64] = "";
char senderBuf[48] = "";
if (node && node->has_user) {
if (node->user.long_name[0]) {
strncpy(senderName, node->user.long_name, sizeof(senderName) - 1);
} else if (node->user.short_name[0]) {
strncpy(senderName, node->user.short_name, sizeof(senderName) - 1);
}
senderName[sizeof(senderName) - 1] = '\0';
}
if (!senderName[0]) {
snprintf(senderName, sizeof(senderName), "(%08x)", m.sender);
// Use long name if present
strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1);
senderBuf[sizeof(senderBuf) - 1] = '\0';
} else {
// No long/short name → show NodeID in parentheses
snprintf(senderBuf, sizeof(senderBuf), "(%08x)", m.sender);
}
// If this is *our own* message, override 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());
if (mine && node_recipient && node_recipient->has_user) {
if (node_recipient->user.long_name[0]) {
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);
strcpy(senderBuf, node_recipient->user.long_name);
}
// Shrink Sender name if needed
int availWidth = (mine ? rightTextWidth : leftTextWidth) - display->getStringWidth(timeBuf) -
display->getStringWidth(chanType) - graphics::UIRenderer::measureStringWithEmotes(display, " @...");
display->getStringWidth(chanType) - display->getStringWidth(" @...");
if (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
char headerStr[128];
char headerStr[96];
if (mine) {
if (currentMode == ThreadMode::ALL) {
if (strcmp(chanType, "(DM)") == 0) {
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, truncatedSender);
snprintf(headerStr, sizeof(headerStr), "%s to %s", timeBuf, senderBuf);
} else {
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);
}
} 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
allLines.push_back(headerStr);
allLines.push_back(std::string(headerStr));
isMine.push_back(mine);
isHeader.push_back(true);
ackForLine.push_back(m.ackStatus);
@@ -657,8 +816,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
topY = visualTop - BUBBLE_PAD_TOP_HEADER;
} else {
// Body start
const bool thisLineHasEmote =
graphics::EmoteRenderer::analyzeLine(nullptr, cachedLines[b.start].c_str(), 0, emotes, numEmotes).hasEmote;
bool thisLineHasEmote = false;
for (int e = 0; e < numEmotes; ++e) {
if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) {
thisLineHasEmote = true;
break;
}
}
if (thisLineHasEmote) {
constexpr int EMOTE_PADDING_ABOVE = 4;
visualTop -= EMOTE_PADDING_ABOVE;
@@ -687,7 +851,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
for (size_t i = b.start; i <= b.end; ++i) {
int w = 0;
if (isHeader[i]) {
w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str());
w = display->getStringWidth(cachedLines[i].c_str());
if (b.mine)
w += 12; // room for ACK/NACK/relay mark
} else {
@@ -743,7 +907,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
if (lineY > -cachedHeights[i] && lineY < scrollBottom) {
if (isHeader[i]) {
int w = graphics::UIRenderer::measureStringWithEmotes(display, cachedLines[i].c_str());
int w = display->getStringWidth(cachedLines[i].c_str());
int headerX;
if (isMine[i]) {
// push header left to avoid overlap with scrollbar
@@ -753,8 +917,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
} else {
headerX = x + textIndent;
}
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
false);
display->drawString(headerX, lineY, cachedLines[i].c_str());
// Draw underline just under header text
int underlineY = lineY + FONT_HEIGHT_SMALL;
@@ -842,7 +1005,11 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
} else {
word += ch;
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 (!line.empty())
lines.push_back(line);
@@ -871,20 +1038,31 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
std::vector<int> rowHeights;
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) {
const auto &line = lines[idx];
const int baseHeight = FONT_HEIGHT_SMALL;
int lineHeight = baseHeight;
const int tallestEmote = lineMetrics[idx].tallestHeight;
const bool hasEmote = lineMetrics[idx].hasEmote;
const bool nextHasEmote = (idx + 1 < lines.size()) && lineMetrics[idx + 1].hasEmote;
// Detect if THIS line or NEXT line contains an emote
bool hasEmote = false;
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]) {
// Header line spacing
@@ -934,22 +1112,22 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
// Banner logic
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet.from);
char longName[64] = "?";
if (node && node->has_user) {
if (node->user.long_name[0]) {
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
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';
}
char longName[48] = "?";
if (node && node->user.long_name) {
strncpy(longName, node->user.long_name, sizeof(longName) - 1);
longName[sizeof(longName) - 1] = '\0';
}
int availWidth = display->getWidth() - ((currentResolution == ScreenResolution::High) ? 40 : 20);
if (availWidth < 0)
availWidth = 0;
char truncatedLongName[64];
graphics::UIRenderer::truncateStringWithEmotes(display, longName, truncatedLongName, sizeof(truncatedLongName),
availWidth);
size_t origLen = strlen(longName);
while (longName[0] && display->getStringWidth(longName) > availWidth) {
longName[strlen(longName) - 1] = '\0';
}
if (strlen(longName) < origLen) {
strcat(longName, "...");
}
const char *msgRaw = reinterpret_cast<const char *>(packet.decoded.payload.bytes);
char banner[256];
@@ -967,8 +1145,8 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
}
if (isAlert) {
if (truncatedLongName[0])
snprintf(banner, sizeof(banner), "Alert Received from\n%s", truncatedLongName);
if (longName && longName[0])
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
else
strcpy(banner, "Alert Received");
} else {
@@ -976,11 +1154,11 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
if (isChannelMuted)
return;
if (truncatedLongName[0]) {
if (longName && longName[0]) {
if (currentResolution == ScreenResolution::UltraLow) {
strcpy(banner, "New Message");
} else {
snprintf(banner, sizeof(banner), "New Message from\n%s", truncatedLongName);
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
}
} else
strcpy(banner, "New Message");
@@ -1043,4 +1221,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet)
} // namespace MessageRenderer
} // namespace graphics
#endif
#endif
+54 -47
View File
@@ -79,15 +79,13 @@ void scrollDown()
// Utility Functions
// =============================
std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
{
(void)display;
(void)columnWidth;
static char nodeName[25]; // single static buffer we return
nodeName[0] = '\0';
auto fallbackId = [&] {
char id[12];
std::snprintf(id, sizeof(id), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
return std::string(id);
auto writeFallbackId = [&] {
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
};
// 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;
}
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
std::string nodeName = (raw && *raw) ? std::string(raw) : std::string{};
if (nodeName.empty()) {
nodeName = fallbackId();
// 2) Sanitize (empty if raw is null/empty)
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
writeFallbackId();
} else {
// %.*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;
@@ -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
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)
{
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);
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char timeStr[10];
@@ -210,7 +228,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->setTextAlignment(TEXT_ALIGN_LEFT);
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 (currentResolution == ScreenResolution::High) {
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);
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
int nameMaxWidth = columnWidth - 25;
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
int barsXOffset = columnWidth - barsOffset;
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
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 (currentResolution == ScreenResolution::High) {
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);
int nameMaxWidth =
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28)
: (isLeftCol ? 20 : 22)));
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
char distStr[10] = "";
@@ -357,7 +368,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
display->setTextAlignment(TEXT_ALIGN_LEFT);
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 (currentResolution == ScreenResolution::High) {
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
int nameMaxWidth =
getNodeNameMaxWidth(columnWidth, columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28)
: (isLeftCol ? 20 : 22)));
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
char nodeName[96];
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
nameMaxWidth);
const char *nodeName = getSafeNodeName(display, node, columnWidth);
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
display->setTextAlignment(TEXT_ALIGN_LEFT);
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 (currentResolution == ScreenResolution::High) {
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 graphics
#endif
#endif
+1 -2
View File
@@ -4,7 +4,6 @@
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#include <string>
namespace graphics
{
@@ -57,7 +56,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
// Utility functions
const char *getCurrentModeTitle_Nodes(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);
// Scrolling controls
+18 -56
View File
@@ -4,7 +4,6 @@
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "NotificationRenderer.h"
#include "UIRenderer.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
@@ -300,7 +299,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
for (int i = 0; i < lineCount; i++) {
linePointers[i] = lineStarts[i];
}
char scratchLineBuffer[visibleTotalLines - lineCount][64];
char scratchLineBuffer[visibleTotalLines - lineCount][40];
uint8_t firstOptionToShow = 0;
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
@@ -313,47 +312,28 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
int scratchLineNum = 0;
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
char tempName[48] = {0};
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i + 1);
if (node && node->has_user) {
const char *rawName = nullptr;
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);
}
char temp_name[16] = {0};
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else {
snprintf(tempName, sizeof(tempName), "(%04X)", (uint16_t)(node ? (node->num & 0xFFFF) : 0));
}
if (!tempName[0]) {
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 (i == curSelected) {
selectedNodenum = node ? node->num : 0;
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (currentResolution == ScreenResolution::High) {
strncpy(scratchLineBuffer[scratchLineNum], "> ", 3);
strncpy(scratchLineBuffer[scratchLineNum] + 2, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 3);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1);
strncpy(scratchLineBuffer[scratchLineNum] + used, " <", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1);
strncpy(scratchLineBuffer[scratchLineNum] + 2, temp_name, 36);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 2, " <", 3);
} else {
strncpy(scratchLineBuffer[scratchLineNum], ">", 2);
strncpy(scratchLineBuffer[scratchLineNum] + 1, tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 2);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
const size_t used = strnlen(scratchLineBuffer[scratchLineNum], sizeof(scratchLineBuffer[scratchLineNum]) - 1);
strncpy(scratchLineBuffer[scratchLineNum] + used, "<", sizeof(scratchLineBuffer[scratchLineNum]) - used - 1);
strncpy(scratchLineBuffer[scratchLineNum] + 1, temp_name, 37);
strncpy(scratchLineBuffer[scratchLineNum] + strlen(temp_name) + 1, "<", 2);
}
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
scratchLineBuffer[scratchLineNum][39] = '\0';
} else {
strncpy(scratchLineBuffer[scratchLineNum], tempName, sizeof(scratchLineBuffer[scratchLineNum]) - 1);
scratchLineBuffer[scratchLineNum][sizeof(scratchLineBuffer[scratchLineNum]) - 1] = '\0';
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
scratchLineBuffer[scratchLineNum][39] = '\0';
}
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
lineLengths[lineCount] = strlen(lines[lineCount]);
if (current_notification_type == notificationTypeEnum::node_picker) {
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);
}
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
// Consider extra width for signal bars on lines that contain "Signal:"
uint16_t potentialWidth = lineWidths[lineCount];
@@ -633,11 +607,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
display->setColor(BLACK);
int yOffset = 3;
if (current_notification_type == notificationTypeEnum::node_picker) {
UIRenderer::drawStringWithEmotes(display, textX, lineY - yOffset, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(textX, lineY - yOffset, lineBuffer);
}
display->drawString(textX, lineY - yOffset, lineBuffer);
display->setColor(WHITE);
lineY += (effectiveLineHeight - 2 - background_yOffset);
} else {
@@ -656,11 +626,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
int totalWidth = textWidth + barsWidth;
int groupStartX = boxLeft + (boxWidth - totalWidth) / 2;
if (current_notification_type == notificationTypeEnum::node_picker) {
UIRenderer::drawStringWithEmotes(display, groupStartX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(groupStartX, lineY, lineBuffer);
}
display->drawString(groupStartX, lineY, lineBuffer);
int baseX = groupStartX + textWidth + gap;
int baseY = lineY + effectiveLineHeight - 1;
@@ -676,11 +642,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
}
}
} else {
if (current_notification_type == notificationTypeEnum::node_picker) {
UIRenderer::drawStringWithEmotes(display, textX, lineY, lineBuffer, FONT_HEIGHT_SMALL, 1, false);
} else {
display->drawString(textX, lineY, lineBuffer);
}
display->drawString(textX, lineY, lineBuffer);
}
lineY += (effectiveLineHeight);
}
+51 -79
View File
@@ -8,7 +8,6 @@
#include "UIRenderer.h"
#include "airtime.h"
#include "gps/GeoCoord.h"
#include "graphics/EmoteRenderer.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TimeFormatters.h"
#include "graphics/images.h"
@@ -314,8 +313,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
#endif
currentFavoriteNodeNum = node->num;
// === Create the shortName and title string ===
const char *shortName = (node->has_user && node->user.short_name[0]) ? node->user.short_name : "Node";
char titlestr[40];
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
char titlestr[32] = {0};
snprintf(titlestr, sizeof(titlestr), "*%s*", shortName);
// === 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.
int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) ===
const char *username;
if (currentResolution == ScreenResolution::UltraLow) {
@@ -338,8 +338,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
}
if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// 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) ===
@@ -801,12 +802,14 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Node Identity ===
int textWidth = 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 ===
textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
textWidth = display->getStringWidth(shortnameble);
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
if (powerStatus->getHasBattery()) {
char batStr[20];
@@ -901,36 +904,36 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0;
int nameX = 0;
int yOffset = (currentResolution == ScreenResolution::High) ? 0 : 5;
const char *longName = (ourNode && ourNode->has_user && ourNode->user.long_name[0]) ? ourNode->user.long_name : "";
const char *shortName = owner.short_name ? owner.short_name : "";
char combinedName[96];
if (longName[0] && shortName[0]) {
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';
std::string longNameStr;
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
longNameStr = sanitizeString(ourNode->user.long_name);
}
if (SCREEN_WIDTH - UIRenderer::measureStringWithEmotes(display, combinedName) > 10) {
textWidth = UIRenderer::measureStringWithEmotes(display, combinedName);
char shortnameble[35];
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;
UIRenderer::drawStringWithEmotes(
display, nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset,
combinedName, FONT_HEIGHT_SMALL, 1, false);
display->drawString(
nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName);
} else {
// === LongName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, longName);
textWidth = display->getStringWidth(longNameStr.c_str());
nameX = (SCREEN_WIDTH - textWidth) / 2;
UIRenderer::drawStringWithEmotes(display, nameX, getTextPositions(display)[line++], longName, FONT_HEIGHT_SMALL, 1,
false);
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
// === ShortName Centered ===
textWidth = UIRenderer::measureStringWithEmotes(display, shortName);
textWidth = display->getStringWidth(shortnameble);
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);
}
#endif
graphics::drawCommonFooter(display, x, y);
@@ -1042,12 +1045,12 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused";
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 dividerGap = 1;
// 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 boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
@@ -1072,7 +1075,7 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
// Draw: text
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 + 1, pauseTextTop, pauseText); // Faux bold
@@ -1105,16 +1108,11 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
display->drawString(msgX, msgY, upperMsg);
}
// Draw version and short name in bottom middle
char footer[64];
if (owner.short_name && owner.short_name[0]) {
snprintf(footer, sizeof(footer), "%s %s", xstr(APP_VERSION_SHORT), owner.short_name);
} else {
snprintf(footer, sizeof(footer), "%s", xstr(APP_VERSION_SHORT));
}
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);
char buf[25];
snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT),
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf);
screen->forceDisplay();
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);
// Draw version and short name in upper right
const char *version = xstr(APP_VERSION_SHORT);
int versionX = x + SCREEN_WIDTH - display->getStringWidth(version);
display->drawString(versionX, y + 0, version);
if (owner.short_name && owner.short_name[0]) {
const char *shortName = owner.short_name;
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);
}
char buf[25];
snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT),
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(x + SCREEN_WIDTH, y + 0, buf);
screen->forceDisplay();
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);
// Draw version and shortname in upper right
const char *version = xstr(APP_VERSION_SHORT);
int versionX = x + SCREEN_WIDTH - display->getStringWidth(version);
display->drawString(versionX, y + 0, version);
if (owner.short_name && owner.short_name[0]) {
const char *shortName = owner.short_name;
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);
}
char buf[25];
snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : "");
display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(x + SCREEN_WIDTH, y + 0, buf);
screen->forceDisplay();
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;
}
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
#endif // HAS_SCREEN
-23
View File
@@ -1,7 +1,6 @@
#pragma once
#include "NodeDB.h"
#include "graphics/EmoteRenderer.h"
#include "graphics/Screen.h"
#include "graphics/emotes.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 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)
static bool haveGlyphs(const char *str);
}; // namespace UIRenderer
+2 -2
View File
@@ -10,7 +10,7 @@ const uint8_t ArialMT_Plain_10_RU[] PROGMEM = {
0xE0, // Number of chars: 224
// Jump Table:
0xFF, 0xFF, 0x00, 0x03, // 32
0xFF, 0xFF, 0x00, 0x0A, // 32
0x00, 0x00, 0x04, 0x03, // 33
0x00, 0x04, 0x05, 0x04, // 34
0x00, 0x09, 0x09, 0x06, // 35
@@ -1766,4 +1766,4 @@ const uint8_t ArialMT_Plain_24_RU[] PROGMEM = {
0x3F, // 255
};
#endif // OLED_RU
#endif // OLED_RU
+2 -2
View File
@@ -9,7 +9,7 @@ const uint8_t ArialMT_Plain_10_UA[] PROGMEM = {
0x20, // First char: 32
0xE0, // Number of chars: 224
// Jump Table:
0xFF, 0xFF, 0x00, 0x03, // 32
0xFF, 0xFF, 0x00, 0x0A, // 32
0x00, 0x00, 0x04, 0x03, // 33
0x00, 0x04, 0x05, 0x04, // 34
0x00, 0x09, 0x09, 0x06, // 35
@@ -1924,4 +1924,4 @@ const uint8_t ArialMT_Plain_24_UA[] PROGMEM = {
0xFF, // 1103
};
#endif // OLED_UA
#endif // OLED_UA
-1
View File
@@ -15,7 +15,6 @@
#include <GFX.h> // GFXRoot drawing lib
#include "mesh/MeshModule.h"
#include "mesh/MeshTypes.h"
#include "./AppletFont.h"
+1 -5
View File
@@ -16,10 +16,6 @@
DeviceScreen *deviceScreen = nullptr;
#ifndef TFT_TASK_STACK_SIZE
#define TFT_TASK_STACK_SIZE 16384
#endif
#ifdef ARCH_ESP32
// Get notified when the system is entering light sleep
CallbackObserver<DeviceScreen, void *> tftSleepObserver =
@@ -131,7 +127,7 @@ void tftSetup(void)
#ifdef ARCH_ESP32
tftSleepObserver.observe(&notifyLightSleep);
endSleepObserver.observe(&notifyLightSleepEnd);
xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(tft_task_handler, "tft", 10240, NULL, 1, NULL, 0);
#elif defined(ARCH_PORTDUINO)
std::thread *tft_task = new std::thread([] { tft_task_handler(); });
#endif
+4 -7
View File
@@ -34,7 +34,7 @@
#if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr;
#if defined(PIN_EINK_EN)
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
static bool touchBacklightWasOn = false;
static bool touchBacklightActive = false;
#endif
@@ -220,8 +220,8 @@ void InputBroker::Init()
};
touchConfig.singlePress = INPUT_BROKER_NONE;
touchConfig.longPress = INPUT_BROKER_BACK;
#if defined(PIN_EINK_EN)
// Touch pad drives the backlight on devices with e-ink backlight pin
#if defined(TTGO_T_ECHO_PLUS) && defined(PIN_EINK_EN)
// On T-Echo Plus the touch pad should only drive the backlight, not UI navigation/sounds
touchConfig.longPress = INPUT_BROKER_NONE;
touchConfig.suppressLeadUpSound = true;
touchConfig.onPress = []() {
@@ -299,7 +299,6 @@ void InputBroker::Init()
// Buttons. Moved here cause we need NodeDB to be initialized
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
UserButtonThread = new ButtonThread("UserButton");
#if !MESHTASTIC_EXCLUDE_SCREEN
if (screen) {
ButtonConfig userConfig;
userConfig.pinNumber = (uint8_t)_pinNum;
@@ -318,9 +317,7 @@ void InputBroker::Init()
userConfig.longPressTime = 500;
userConfig.longLongPress = INPUT_BROKER_SHUTDOWN;
UserButtonThread->initButton(userConfig);
} else
#endif
{
} else {
ButtonConfig userConfigNoScreen;
userConfigNoScreen.pinNumber = (uint8_t)_pinNum;
userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW;
+3 -3
View File
@@ -123,7 +123,7 @@ void printPartitionTable()
#include "AmbientLightingThread.h"
#include "PowerFSMThread.h"
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && !MESHTASTIC_EXCLUDE_ACCELEROMETER
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
#include "motion/AccelerometerThread.h"
AccelerometerThread *accelerometerThread = nullptr;
#endif
@@ -657,7 +657,7 @@ void setup()
}
#endif
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER
#if !defined(ARCH_STM32WL)
auto acc_info = i2cScanner->firstAccelerometer();
accelerometer_found = acc_info.type != ScanI2C::DeviceType::NONE ? acc_info.address : accelerometer_found;
LOG_DEBUG("acc_info = %i", acc_info.type);
@@ -737,7 +737,7 @@ void setup()
#endif
#if !MESHTASTIC_EXCLUDE_I2C
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ACCELEROMETER
#if !defined(ARCH_STM32WL)
if (acc_info.type != ScanI2C::DeviceType::NONE) {
accelerometerThread = new AccelerometerThread(acc_info.type);
}

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