Compare commits

..

1 Commits

Author SHA1 Message Date
mverch67
2f379b8f6d apply 180 degree hw roration Indicator BaseUI 2025-08-17 19:07:41 +02:00
407 changed files with 3416 additions and 13852 deletions

View File

@@ -76,7 +76,7 @@ bool loopCanSleep()
// Called just prior to starting Meshtastic. Allows for setting config values before startup. // Called just prior to starting Meshtastic. Allows for setting config values before startup.
void lateInitVariant() void lateInitVariant()
{ {
portduino_config.logoutputlevel = level_error; settingsMap[logoutputlevel] = level_error;
channelFile.channels[0] = meshtastic_Channel{ channelFile.channels[0] = meshtastic_Channel{
.has_settings = true, .has_settings = true,
.settings = .settings =
@@ -132,7 +132,7 @@ int portduino_main(int argc, char **argv); // Renamed "main" function from Mesht
// Start Meshtastic in a thread and wait till it has reached the ON state. // Start Meshtastic in a thread and wait till it has reached the ON state.
int LLVMFuzzerInitialize(int *argc, char ***argv) int LLVMFuzzerInitialize(int *argc, char ***argv)
{ {
portduino_config.maxtophone = 5; settingsMap[maxtophone] = 5;
meshtasticThread = std::thread([program = *argv[0]]() { meshtasticThread = std::thread([program = *argv[0]]() {
char nodeIdStr[12]; char nodeIdStr[12];

View File

@@ -11,6 +11,11 @@ runs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Uncomment build epoch
shell: bash
run: |
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: | run: |
@@ -18,7 +23,7 @@ runs:
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
cache: pip cache: pip

View File

@@ -19,8 +19,6 @@ jobs:
pio-build: pio-build:
name: build-${{ inputs.platform }} name: build-${{ inputs.platform }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
@@ -57,7 +55,6 @@ jobs:
- name: Store binaries as an artifact - name: Store binaries as an artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
id: upload
with: with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
overwrite: true overwrite: true

View File

@@ -1,176 +0,0 @@
name: Build One Arch
on:
workflow_dispatch:
inputs:
# trunk-ignore(checkov/CKV_GHA_7)
arch:
type: choice
options:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- native
permissions: read-all
env:
INPUT_ARCH: ${{ github.event.inputs.arch }}
jobs:
setup:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
TARGETS=$(./bin/generate_ci_matrix.py $INPUT_ARCH --level extra)
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "selected_arch=$TARGETS" >> $GITHUB_OUTPUT
outputs:
selected_arch: ${{ steps.jsonStep.outputs.selected_arch }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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 }}
build:
if: ${{ github.event_name != 'workflow_dispatch' }}
needs: [setup, version]
strategy:
fail-fast: false
matrix:
build: ${{ fromJson(needs.setup.outputs.selected_arch) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.arch }}
build-debian-src:
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ inputs.arch == 'native' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }}
uses: ./.github/workflows/test_native.yml
gather-artifacts:
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@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-${{inputs.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@v4
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
with:
name: firmware-${{inputs.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
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v4
with:
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,182 +0,0 @@
name: Build One Target
on:
workflow_dispatch:
inputs:
# trunk-ignore(checkov/CKV_GHA_7)
arch:
type: choice
options:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- native
target:
type: string
required: false
description: Choose the target board, e.g. nrf52_promicro_diy_tcxo. If blank, will find available targets.
# find-target:
# type: boolean
# default: true
# description: 'Find the available targets'
permissions: read-all
jobs:
find-targets:
if: ${{ inputs.target == '' }}
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level extra)
echo "Name: $GITHUB_REF_NAME" >> $GITHUB_STEP_SUMMARY
echo "Base: $GITHUB_BASE_REF" >> $GITHUB_STEP_SUMMARY
echo "Arch: ${{matrix.arch}}" >> $GITHUB_STEP_SUMMARY
echo "Ref: $GITHUB_REF" >> $GITHUB_STEP_SUMMARY
echo "Targets:" >> $GITHUB_STEP_SUMMARY
echo $TARGETS >> $GITHUB_STEP_SUMMARY
version:
if: ${{ inputs.target != '' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- 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 }}
build:
if: ${{ inputs.target != '' && inputs.arch != 'native' }}
needs: [version]
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ inputs.target }}
platform: ${{ inputs.arch }}
build-debian-src:
if: ${{ github.repository == 'meshtastic/firmware' && inputs.arch == 'native' }}
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ inputs.arch == 'native' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' && inputs.target != '' }}
uses: ./.github/workflows/test_native.yml
gather-artifacts:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-*-*
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@v4
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
with:
pattern: firmware-*-${{ 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
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v4
with:
name: debug-elfs-${{inputs.target}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{inputs.target}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{inputs.target}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,20 +21,18 @@ permissions:
jobs: jobs:
docker-multiarch: docker-multiarch:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_manifest.yml uses: ./.github/workflows/docker_manifest.yml
with: with:
release_channel: daily release_channel: daily
secrets: inherit secrets: inherit
package-ppa: package-ppa:
if: github.repository == 'meshtastic/firmware'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
series: series:
- jammy # 22.04 LTS - jammy # 22.04
- noble # 24.04 LTS - noble # 24.04
- plucky # 25.04 - plucky # 25.04
- questing # 25.10 - questing # 25.10
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
@@ -44,7 +42,6 @@ jobs:
secrets: inherit secrets: inherit
package-obs: package-obs:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/package_obs.yml uses: ./.github/workflows/package_obs.yml
with: with:
obs_project: network:Meshtastic:daily obs_project: network:Meshtastic:daily
@@ -52,7 +49,6 @@ jobs:
secrets: inherit secrets: inherit
hook-copr: hook-copr:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/hook_copr.yml uses: ./.github/workflows/hook_copr.yml
with: with:
copr_project: daily copr_project: daily

View File

@@ -3,7 +3,7 @@ concurrency:
group: ci-${{ github.head_ref || github.run_id }} group: ci-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
on: on:
# # Triggers the workflow on push but only for the main branches # # Triggers the workflow on push but only for the master branch
push: push:
branches: branches:
- master - master
@@ -28,15 +28,22 @@ on:
jobs: jobs:
setup: setup:
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
arch: arch:
- all - esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- check - check
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
cache: pip cache: pip
@@ -47,13 +54,19 @@ jobs:
if [[ "$GITHUB_HEAD_REF" == "" ]]; then if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}}) TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
else else
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr) TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
fi fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF" echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
echo "$TARGETS" >> $GITHUB_STEP_SUMMARY
outputs: outputs:
all: ${{ steps.jsonStep.outputs.all }} esp32: ${{ steps.jsonStep.outputs.esp32 }}
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }} check: ${{ steps.jsonStep.outputs.check }}
version: version:
@@ -75,30 +88,105 @@ jobs:
needs: setup needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix: ${{ fromJson(needs.setup.outputs.check) }}
check: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} if: ${{ github.event_name != 'workflow_dispatch' }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Build base - name: Build base
id: base id: base
uses: ./.github/actions/setup-base uses: ./.github/actions/setup-base
- name: Check ${{ matrix.check.board }} - name: Check ${{ matrix.board }}
run: bin/check-all.sh ${{ matrix.check.board }} run: bin/check-all.sh ${{ matrix.board }}
build: build-esp32:
needs: [setup, version] needs: [setup, version]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
build: ${{ fromJson(needs.setup.outputs.all) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_firmware.yml
with: with:
version: ${{ needs.version.outputs.long }} version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }} pio_env: ${{ matrix.board }}
platform: ${{ matrix.build.platform }} platform: esp32
build-esp32s3:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32s3
build-esp32c3:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c3
build-esp32c6:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c6
build-nrf52840:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: nrf52840
build-rp2040:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2040
build-rp2350:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2350
build-stm32:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: stm32
build-debian-src: build-debian-src:
if: github.repository == 'meshtastic/firmware' if: github.repository == 'meshtastic/firmware'
@@ -109,41 +197,67 @@ jobs:
secrets: inherit secrets: inherit
package-pio-deps-native-tft: package-pio-deps-native-tft:
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/package_pio_deps.yml uses: ./.github/workflows/package_pio_deps.yml
with: with:
pio_env: native-tft pio_env: native-tft
secrets: inherit secrets: inherit
test-native: test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }} if: ${{ !contains(github.ref_name, 'event/') }}
uses: ./.github/workflows/test_native.yml uses: ./.github/workflows/test_native.yml
docker: docker-deb-amd64:
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 uses: ./.github/workflows/docker_build.yml
with: with:
distro: ${{ matrix.distro }} distro: debian
platform: ${{ matrix.platform }} platform: linux/amd64
runs-on: ${{ contains(matrix.platform, 'arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} runs-on: ubuntu-24.04
pio_env: ${{ matrix.pio_env }} push: false
docker-deb-amd64-tft:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-alp-amd64:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-alp-amd64-tft:
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-deb-arm64:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: false
docker-deb-armv7:
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: false push: false
gather-artifacts: gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
if: github.repository == 'meshtastic/firmware'
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@@ -160,7 +274,18 @@ jobs:
- rp2350 - rp2350
- stm32 - stm32
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [version, build] needs:
[
version,
build-esp32,
build-esp32s3,
build-esp32c3,
build-esp32c6,
build-nrf52840,
build-rp2040,
build-rp2350,
build-stm32,
]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
@@ -232,7 +357,7 @@ jobs:
release-artifacts: release-artifacts:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }} if: ${{ github.event_name == 'workflow_dispatch' }}
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
needs: needs:
@@ -245,7 +370,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@@ -307,14 +432,14 @@ jobs:
- rp2350 - rp2350
- stm32 - stm32
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}} if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version] needs: [release-artifacts, version]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@@ -369,7 +494,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -1,376 +0,0 @@
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@v5
- 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@v5
- 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@v5
- 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@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
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@v4
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
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
chmod +x ./output/device-update.sh
- 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@v4
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
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@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- 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@v5
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@v5
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@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
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
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v5
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
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@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- uses: actions/download-artifact@v5
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

View File

@@ -31,7 +31,7 @@ jobs:
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -10,10 +10,10 @@ permissions:
jobs: jobs:
check-label: check-label:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
steps: steps:
- name: Check for PR labels - name: Check for PR labels
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const labels = context.payload.pull_request.labels.map(label => label.name); const labels = context.payload.pull_request.labels.map(label => label.name);

View File

@@ -1,238 +0,0 @@
name: Tests
# DISABLED: Changed from automatic PR triggers to manual only
on:
workflow_dispatch:
inputs:
reason:
description: "Reason for manual test run"
required: false
default: "Manual test execution"
concurrency:
group: tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
actions: read
checks: write
pull-requests: write
jobs:
native-tests:
name: "🧪 Native Tests"
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/test_native.yml
permissions:
contents: read
actions: read
checks: write
test-summary:
name: "📊 Test Results"
runs-on: ubuntu-latest
needs: [native-tests]
if: always()
permissions:
contents: read
actions: read
checks: write
pull-requests: write
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Download test artifacts
if: needs.native-tests.result != 'skipped'
uses: actions/download-artifact@v5
with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip
merge-multiple: true
- name: Parse test results and create detailed summary
id: test-results
run: |
echo "## 🧪 Test Results Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check overall job status first
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
echo "✅ **Overall Status**: PASSED" >> $GITHUB_STEP_SUMMARY
elif [[ "${{ needs.native-tests.result }}" == "failure" ]]; then
echo "❌ **Overall Status**: FAILED" >> $GITHUB_STEP_SUMMARY
elif [[ "${{ needs.native-tests.result }}" == "cancelled" ]]; then
echo "⏸️ **Overall Status**: CANCELLED" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Tests were cancelled before completion." >> $GITHUB_STEP_SUMMARY
exit 0
else
echo "⚠️ **Overall Status**: SKIPPED" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Tests were skipped." >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "" >> $GITHUB_STEP_SUMMARY
# Parse detailed test results if available
if [ -f "testreport.xml" ]; then
echo "### 🔍 Individual Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
python3 << 'EOF'
import xml.etree.ElementTree as ET
import os
try:
tree = ET.parse('testreport.xml')
root = tree.getroot()
total_tests = 0
passed_tests = 0
failed_tests = 0
skipped_tests = 0
# Parse testsuite elements
for testsuite in root.findall('.//testsuite'):
suite_name = testsuite.get('name', 'Unknown')
suite_tests = int(testsuite.get('tests', '0'))
suite_failures = int(testsuite.get('failures', '0'))
suite_errors = int(testsuite.get('errors', '0'))
suite_skipped = int(testsuite.get('skipped', '0'))
total_tests += suite_tests
failed_tests += suite_failures + suite_errors
skipped_tests += suite_skipped
passed_tests += suite_tests - suite_failures - suite_errors - suite_skipped
if suite_tests > 0:
status = "✅" if (suite_failures + suite_errors) == 0 else "❌"
print(f"**{status} Test Suite: {suite_name}**")
print(f"- Total: {suite_tests}")
print(f"- Passed: ✅ {suite_tests - suite_failures - suite_errors - suite_skipped}")
print(f"- Failed: ❌ {suite_failures + suite_errors}")
if suite_skipped > 0:
print(f"- Skipped: ⏭️ {suite_skipped}")
print("")
# Show individual test results for failed suites
if suite_failures + suite_errors > 0:
print("**Failed Tests:**")
for testcase in testsuite.findall('testcase'):
test_name = testcase.get('name', 'Unknown')
failure = testcase.find('failure')
error = testcase.find('error')
if failure is not None:
msg = failure.get('message', 'Unknown error')[:100]
print(f"- ❌ `{test_name}`: {msg}")
elif error is not None:
msg = error.get('message', 'Unknown error')[:100]
print(f"- ❌ `{test_name}`: ERROR - {msg}")
print("")
else:
# Show passed tests for successful suites
passed_count = 0
for testcase in testsuite.findall('testcase'):
if testcase.find('failure') is None and testcase.find('error') is None:
if passed_count < 5: # Limit to first 5 to avoid spam
test_name = testcase.get('name', 'Unknown')
print(f"- ✅ `{test_name}`: PASSED")
passed_count += 1
if passed_count > 5:
print(f"- ... and {passed_count - 5} more tests passed")
print("")
# Summary statistics
print("### 📊 Test Statistics")
print(f"- **Total Tests**: {total_tests}")
print(f"- **Passed**: ✅ {passed_tests}")
print(f"- **Failed**: ❌ {failed_tests}")
if skipped_tests > 0:
print(f"- **Skipped**: ⏭️ {skipped_tests}")
if failed_tests > 0:
print(f"\n❌ **{failed_tests} tests failed out of {total_tests} total**")
else:
print(f"\n✅ **All {total_tests} tests passed!**")
except Exception as e:
print(f"❌ Error parsing test results: {e}")
EOF
else
echo "⚠️ **No detailed test report available**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Test artifacts may not have been generated properly." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "View detailed logs in the [Actions tab](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
- name: Comment test results on PR
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
// Read the step summary to use as PR comment
let testSummary = "## 🧪 Test Results Summary\n\n";
if ("${{ needs.native-tests.result }}" === "success") {
testSummary += "✅ **All tests passed!**\n\n";
} else if ("${{ needs.native-tests.result }}" === "failure") {
testSummary += "❌ **Some tests failed.**\n\n";
} else {
testSummary += "⚠️ **Tests did not complete normally.**\n\n";
}
testSummary += `View detailed results: [Actions Run](${context.payload.repository.html_url}/actions/runs/${context.runId})\n\n`;
testSummary += "---\n";
testSummary += "*This comment will be automatically updated when new commits are pushed.*";
// Find existing comment
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.data.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('🧪 Test Results Summary')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: testSummary
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: testSummary
});
}
- name: Set overall status
run: |
if [[ "${{ needs.native-tests.result }}" == "success" ]]; then
echo "All tests passed! ✅"
exit 0
else
echo "Some tests failed! ❌"
exit 1
fi

View File

@@ -21,10 +21,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
series: series:
- jammy # 22.04 LTS - jammy # 22.04
- noble # 24.04 LTS - noble # 24.04
- plucky # 25.04 - plucky # 25.04
- questing # 25.10 # - questing # 25.10
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
with: with:
ppa_repo: |- ppa_repo: |-
@@ -63,7 +63,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -41,7 +41,7 @@ jobs:
# step 4 # step 4
- name: publish code scanning alerts - name: publish code scanning alerts
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@v3
with: with:
sarif_file: report.sarif sarif_file: report.sarif
category: semgrep category: semgrep

View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Stale PR+Issues - name: Stale PR+Issues
uses: actions/stale@v10.1.0 uses: actions/stale@v9.1.0
with: with:
days-before-stale: 45 days-before-stale: 45
exempt-issue-labels: pinned,3.0 exempt-issue-labels: pinned,3.0

View File

@@ -40,7 +40,7 @@ jobs:
- name: Integration test - name: Integration test
run: | run: |
.pio/build/coverage/program -s & .pio/build/coverage/program &
PID=$! PID=$!
timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done" timeout 20 bash -c "until ls -al /proc/$PID/fd | grep socket; do sleep 1; done"
echo "Simulator started, launching python test..." echo "Simulator started, launching python test..."

View File

@@ -47,7 +47,7 @@ jobs:
pio upgrade pio upgrade
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v5 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22

View File

@@ -39,7 +39,7 @@ jobs:
git push git push
- name: Comment on PR - name: Comment on PR
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View File

@@ -4,29 +4,29 @@ cli:
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.3 ref: v1.7.1
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.477 - checkov@3.2.461
- renovate@41.144.1 - renovate@41.71.1
- prettier@3.6.2 - prettier@3.6.2
- trufflehog@3.90.8 - trufflehog@3.90.4
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.8.6 - bandit@1.8.6
- trivy@0.67.1 - trivy@0.64.1
- taplo@0.10.0 - taplo@0.9.3
- ruff@0.14.0 - ruff@0.12.7
- isort@6.1.0 - isort@6.0.1
- markdownlint@0.45.0 - markdownlint@0.45.0
- oxipng@9.1.5 - oxipng@9.1.5
- svgo@4.0.0 - svgo@4.0.0
- actionlint@1.7.7 - actionlint@1.7.7
- flake8@7.3.0 - flake8@7.3.0
- hadolint@2.14.0 - hadolint@2.12.1-beta
- shfmt@3.6.0 - shfmt@3.6.0
- shellcheck@0.11.0 - shellcheck@0.10.0
- black@25.9.0 - black@25.1.0
- git-diff-check - git-diff-check
- gitleaks@8.28.0 - gitleaks@8.28.0
- clang-format@16.0.3 - clang-format@16.0.3

View File

@@ -3,7 +3,7 @@
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM python:3.14-slim-trixie AS builder FROM python:3.13-slim-trixie AS builder
ARG PIO_ENV=native ARG PIO_ENV=native
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC ENV TZ=Etc/UTC
@@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \
# Fetch compiled binary from the builder # Fetch compiled binary from the builder
COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/
COPY --from=builder /tmp/web /usr/share/meshtasticd/web/ COPY --from=builder /tmp/web /usr/share/meshtasticd/
# Copy config templates # Copy config templates
COPY ./bin/config.d /etc/meshtasticd/available.d COPY ./bin/config.d /etc/meshtasticd/available.d

View File

@@ -3,7 +3,7 @@
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions # trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM python:3.14-alpine3.22 AS builder FROM python:3.13-alpine3.22 AS builder
ARG PIO_ENV=native ARG PIO_ENV=native
ENV PIP_ROOT_USER_ACTION=ignore ENV PIP_ROOT_USER_ACTION=ignore

View File

@@ -31,13 +31,11 @@ build_flags =
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL -DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
-DAXP_DEBUG_PORT=Serial -DAXP_DEBUG_PORT=Serial
-DCONFIG_BT_NIMBLE_ENABLED -DCONFIG_BT_NIMBLE_ENABLED
-DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2
-DCONFIG_BT_NIMBLE_MAX_CCCDS=20 -DCONFIG_BT_NIMBLE_MAX_CCCDS=20
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192 -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
-DSERIAL_BUFFER_SIZE=4096 -DSERIAL_BUFFER_SIZE=4096
-DSERIAL_HAS_ON_RECEIVE
-DLIBPAX_ARDUINO -DLIBPAX_ARDUINO
-DLIBPAX_WIFI -DLIBPAX_WIFI
-DLIBPAX_BLE -DLIBPAX_BLE
@@ -57,7 +55,7 @@ lib_deps =
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master # renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib # renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
https://github.com/lewisxhe/XPowersLib/archive/v0.3.1.zip https://github.com/lewisxhe/XPowersLib/archive/v0.3.0.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master
https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip https://github.com/meshtastic/ESP32_Codec2/archive/633326c78ac251c059ab3a8c430fcdf25b41672f.zip
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto

View File

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

View File

@@ -2,7 +2,7 @@
[portduino_base] [portduino_base]
platform = platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip https://github.com/meshtastic/platform-native/archive/6cb7a455b440dd0738e8ed74a18136ed5cf7ea63.zip
framework = arduino framework = arduino
build_src_filter = build_src_filter =
@@ -31,8 +31,6 @@ lib_deps =
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
adafruit/Adafruit seesaw Library@1.7.9 adafruit/Adafruit seesaw Library@1.7.9
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
build_flags = build_flags =
${arduino_base.build_flags} ${arduino_base.build_flags}

View File

@@ -50,7 +50,7 @@ lib_deps =
${radiolib_base.lib_deps} ${radiolib_base.lib_deps}
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip
lib_ignore = lib_ignore =
OneButton OneButton

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
export PIP_BREAK_SYSTEM_PACKAGES=1 export PIP_BREAK_SYSTEM_PACKAGES=1
if (echo $2 | grep -q "esp32"); then if (echo $2 | grep -q "esp32"); then

View File

@@ -1,8 +0,0 @@
Lora:
### RAK13300in Slot 2 pins
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
spidev: spidev0.1
# CS: 7

View File

@@ -9,4 +9,13 @@ Lora:
DIO3_TCXO_VOLTAGE: true DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true DIO2_AS_RF_SWITCH: true
spidev: spidev0.0 spidev: spidev0.0
# CS: 8 # CS: 8
### RAK13300in Slot 2 pins
# IRQ: 18 #IO6
# Reset: 24 # IO4
# Busy: 19 # IO5
# # Ant_sw: 23 # IO3
# spidev: spidev0.1
# # CS: 7

View File

@@ -7,7 +7,6 @@ SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "TFT_BUILD=0" SET "TFT_BUILD=0"
SET "BIGDB8=0" SET "BIGDB8=0"
SET "MUIDB8=0"
SET "BIGDB16=0" SET "BIGDB16=0"
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
@@ -15,12 +14,11 @@ SET "LOGCOUNTER=0"
SET "BPS_RESET=0" SET "BPS_RESET=0"
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv" SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
SET "C3=esp32c3" SET "C3=esp32c3"
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator" SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3"
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv"
GOTO getopts GOTO getopts
:help :help
@@ -102,6 +100,7 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" (
) )
:skip-filename :skip-filename
SET "ESPTOOL_BAUD=1200"
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" ( IF NOT "__%PYTHON%__"=="____" (
@@ -121,10 +120,11 @@ IF NOT "__%PYTHON%__"=="____" (
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1 !ESPTOOL_CMD! >nul 2>&1
IF %ERRORLEVEL% EQU 9009 ( IF %ERRORLEVEL% GEQ 2 (
@REM 9009 = command not found on Windows @REM esptool exits with code 1 if help is displayed.
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1 EXIT /B 1
GOTO eof
) )
IF %DEBUG% EQU 1 ( IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
@@ -142,7 +142,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
IF %BPS_RESET% EQU 1 ( IF %BPS_RESET% EQU 1 (
@REM Attempt to change mode via 1200bps Reset. @REM Attempt to change mode via 1200bps Reset.
CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
GOTO eof GOTO eof
) )
@@ -164,15 +164,6 @@ FOR %%a IN (%BIGDB_8MB%) DO (
) )
:end_loop_bigdb_8mb :end_loop_bigdb_8mb
FOR %%a IN (%MUIDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %MUIDB_8MB%.
SET "MUIDB8=1"
GOTO end_loop_muidb_8mb
)
)
:end_loop_muidb_8mb
FOR %%a IN (%BIGDB_16MB%) DO ( FOR %%a IN (%BIGDB_16MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_16MB%. @REM We are working with any of %BIGDB_16MB%.
@@ -183,7 +174,6 @@ FOR %%a IN (%BIGDB_16MB%) DO (
:end_loop_bigdb_16mb :end_loop_bigdb_16mb
IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected." IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected." IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
@REM Extract BASENAME from %FILENAME% for later use. @REM Extract BASENAME from %FILENAME% for later use.
@@ -228,12 +218,6 @@ IF %BIGDB8% EQU 1 (
SET "SPIFFS_OFFSET=0x670000" SET "SPIFFS_OFFSET=0x670000"
) )
@REM Offsets for MUIDB 8mb.
IF %MUIDB8% EQU 1 (
SET "OTA_OFFSET=0x5D0000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for BigDB 16mb. @REM Offsets for BigDB 16mb.
IF %BIGDB16% EQU 1 ( IF %BIGDB16% EQU 1 (
SET "OTA_OFFSET=0x650000" SET "OTA_OFFSET=0x650000"

View File

@@ -1,47 +1,42 @@
#!/usr/bin/env bash #!/bin/bash
PYTHON=${PYTHON:-$(which python3 python | head -n 1)} PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false BPS_RESET=false
TFT_BUILD=false TFT_BUILD=false
MCU="" MCU=""
# Constants
RESET_BAUD=1200
FIRMWARE_OFFSET=0x00
# Variant groups # Variant groups
BIGDB_8MB=( BIGDB_8MB=(
"crowpanel-esp32s3" "picomputer-s3"
"heltec_capsule_sensor_v3" "unphone"
"heltec-v3" "seeed-sensecap-indicator"
"heltec-vision-master-e213" "crowpanel-esp32s3"
"heltec-vision-master-e290" "heltec_capsule_sensor_v3"
"heltec-vision-master-t190" "heltec-v3"
"heltec-wireless-paper" "heltec-vision-master-e213"
"heltec-wireless-tracker" "heltec-vision-master-e290"
"heltec-wsl-v3" "heltec-vision-master-t190"
"icarus" "heltec-wireless-paper"
"seeed-xiao-s3" "heltec-wireless-tracker"
"tbeam-s3-core" "heltec-wsl-v3"
"tracksenger" "icarus"
) "seeed-xiao-s3"
MUIDB_8MB=( "tbeam-s3-core"
"picomputer-s3" "tracksenger"
"unphone"
"seeed-sensecap-indicator"
) )
BIGDB_16MB=( BIGDB_16MB=(
"t-deck" "t-deck"
"mesh-tab" "mesh-tab"
"t-energy-s3" "t-energy-s3"
"dreamcatcher" "dreamcatcher"
"ESP32-S3-Pico" "ESP32-S3-Pico"
"m5stack-cores3" "m5stack-cores3"
"station-g2" "station-g2"
"t-eth-elite" "t-eth-elite"
"tlora-pager"
"t-watch-s3" "t-watch-s3"
"elecrow-adv" "elecrow-adv-35-tft"
"elecrow-adv-24-28-tft"
"elecrow-adv1-43-50-70-tft"
) )
S3_VARIANTS=( S3_VARIANTS=(
"s3" "s3"
@@ -52,7 +47,6 @@ S3_VARIANTS=(
"station-g2" "station-g2"
"unphone" "unphone"
"t-eth-elite" "t-eth-elite"
"tlora-pager"
"mesh-tab" "mesh-tab"
"dreamcatcher" "dreamcatcher"
"ESP32-S3-Pico" "ESP32-S3-Pico"
@@ -112,8 +106,8 @@ while [ $# -gt 0 ]; do
shift shift
;; ;;
--1200bps-reset) --1200bps-reset)
BPS_RESET=true BPS_RESET=true
;; ;;
--) # Stop parsing options --) # Stop parsing options
shift shift
break break
@@ -127,7 +121,7 @@ while [ $# -gt 0 ]; do
done done
if [[ $BPS_RESET == true ]]; then if [[ $BPS_RESET == true ]]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
exit 0 exit 0
fi fi
@@ -164,13 +158,6 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
fi fi
done done
for variant in "${MUIDB_8MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0x670000
OTA_OFFSET=0x5D0000
fi
done
# littlefs* offset for BigDB 16mb and OTA OFFSET. # littlefs* offset for BigDB 16mb and OTA OFFSET.
for variant in "${BIGDB_16MB[@]}"; do for variant in "${BIGDB_16MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then if [ -z "${FILENAME##*"$variant"*}" ]; then
@@ -214,8 +201,8 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
fi fi
echo "Trying to flash ${FILENAME}, but first erasing and writing system information" echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
$ESPTOOL_CMD erase-flash $ESPTOOL_CMD erase_flash
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" $ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"

View File

@@ -6,8 +6,6 @@ SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0" SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "RESET_BAUD=1200"
SET "UPDATE_OFFSET=0x10000"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0" SET "LOGCOUNTER=0"
SET "CHANGE_MODE=0" SET "CHANGE_MODE=0"
@@ -87,13 +85,14 @@ IF "!FILENAME:update=!"=="!FILENAME!" (
) )
:skip-filename :skip-filename
SET "ESPTOOL_BAUD=1200"
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" ( IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
WHERE esptool >nul 2>&1 WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 ( IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found. @REM WHERE exits with code 0 if esptool is found.
@@ -106,11 +105,11 @@ IF NOT "__%PYTHON%__"=="____" (
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1 !ESPTOOL_CMD! >nul 2>&1
CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" IF %ERRORLEVEL% GEQ 2 (
IF %ERRORLEVEL% EQU 9009 ( @REM esptool exits with code 1 if help is displayed.
@REM 9009 = command not found on Windows
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1 EXIT /B 1
GOTO eof
) )
IF %DEBUG% EQU 1 ( IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
@@ -128,13 +127,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
IF %CHANGE_MODE% EQU 1 ( IF %CHANGE_MODE% EQU 1 (
@REM Attempt to change mode via 1200bps Reset. @REM Attempt to change mode via 1200bps Reset.
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
GOTO eof GOTO eof
) )
@REM Flashing operations. @REM Flashing operations.
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Script complete!." CALL :LOG_MESSAGE INFO "Script complete!."
@@ -146,9 +145,9 @@ EXIT /B %ERRORLEVEL%
:RUN_ESPTOOL :RUN_ESPTOOL
@REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Subroutine used to run ESPTOOL_CMD with arguments.
@REM Also handles %ERRORLEVEL%. @REM Also handles %ERRORLEVEL%.
@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
@REM. @REM.
@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" @REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
CALL :RESET_ERROR CALL :RESET_ERROR
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4

View File

@@ -1,13 +1,8 @@
#!/usr/bin/env bash #!/bin/bash
PYTHON=${PYTHON:-$(which python3 python|head -n 1)} PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
CHANGE_MODE=false CHANGE_MODE=false
# Constants
FLASH_BAUD=115200
RESET_BAUD=1200
UPDATE_OFFSET=0x10000
# Determine the correct esptool command to use # Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then if "$PYTHON" -m esptool version >/dev/null 2>&1; then
ESPTOOL_CMD="$PYTHON -m esptool" ESPTOOL_CMD="$PYTHON -m esptool"
@@ -69,7 +64,7 @@ done
shift "$((OPTIND-1))" shift "$((OPTIND-1))"
if [ "$CHANGE_MODE" = true ]; then if [ "$CHANGE_MODE" = true ]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
exit 0 exit 0
fi fi
@@ -80,7 +75,7 @@ fi
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
echo "Trying to flash update ${FILENAME}" echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}"
else else
show_help show_help
echo "Invalid file: ${FILENAME}" echo "Invalid file: ${FILENAME}"

View File

@@ -1,32 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python
"""Generate the CI matrix.""" """Generate the CI matrix."""
import argparse
import json import json
import sys
import random
import re import re
from platformio.project.config import ProjectConfig from platformio.project.config import ProjectConfig
parser = argparse.ArgumentParser(description="Generate the CI matrix") options = sys.argv[1:]
parser.add_argument("platform", help="Platform to build for")
parser.add_argument(
"--level",
choices=["extra", "pr"],
nargs="*",
default=[],
help="Board level to build for (omit for full release boards)",
)
args = parser.parse_args()
outlist = [] outlist = []
if len(options) < 1:
print(json.dumps(outlist))
exit(1)
cfg = ProjectConfig.get_instance() cfg = ProjectConfig.get_instance()
pio_envs = cfg.envs() pio_envs = cfg.envs()
# Gather all PlatformIO environments for filtering later # Gather all PlatformIO environments for filtering later
all_envs = [] all_envs = []
for pio_env in pio_envs: for pio_env in pio_envs:
env_build_flags = cfg.get(f"env:{pio_env}", "build_flags") env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags')
env_platform = None env_platform = None
for flag in env_build_flags: for flag in env_build_flags:
# Extract the platform from the build flags # Extract the platform from the build flags
@@ -41,35 +37,36 @@ for pio_env in pio_envs:
exit(1) exit(1)
# Store env details as a dictionary, and add to 'all_envs' list # Store env details as a dictionary, and add to 'all_envs' list
env = { env = {
"ci": {"board": pio_env, "platform": env_platform}, 'name': pio_env,
"board_level": cfg.get(f"env:{pio_env}", "board_level", default=None), 'platform': env_platform,
"board_check": bool(cfg.get(f"env:{pio_env}", "board_check", default=False)), 'board_level': cfg.get(f"env:{pio_env}", 'board_level', default=None),
'board_check': bool(cfg.get(f"env:{pio_env}", 'board_check', default=False))
} }
all_envs.append(env) all_envs.append(env)
# Filter outputs based on options # Filter outputs based on options
# Check is mutually exclusive with other options (except 'pr') # Check is mutually exclusive with other options (except 'pr')
if "check" in args.platform: if "check" in options:
for env in all_envs: for env in all_envs:
if env["board_check"]: if env['board_check']:
if "pr" in args.level: if "pr" in options:
if env["board_level"] == "pr": if env['board_level'] == 'pr':
outlist.append(env["ci"]) outlist.append(env['name'])
else: else:
outlist.append(env["ci"]) outlist.append(env['name'])
# Filter (non-check) builds by platform # Filter (non-check) builds by platform
else: else:
for env in all_envs: for env in all_envs:
if args.platform == env["ci"]["platform"] or args.platform == "all": if options[0] == env['platform']:
# Always include board_level = 'pr' # Always include board_level = 'pr'
if env["board_level"] == "pr": if env['board_level'] == 'pr':
outlist.append(env["ci"]) outlist.append(env['name'])
# Include board_level = 'extra' when requested # Include board_level = 'extra' when requested
elif "extra" in args.level and env["board_level"] == "extra": elif "extra" in options and env['board_level'] == "extra":
outlist.append(env["ci"]) outlist.append(env['name'])
# If no board level is specified, include in release builds (not PR) # If no board level is specified, include in release builds (not PR)
elif "pr" not in args.level and not env["board_level"]: elif "pr" not in options and not env['board_level']:
outlist.append(env["ci"]) outlist.append(env['name'])
# Return as a JSON list # Return as a JSON list
print(json.dumps(outlist)) print(json.dumps(outlist))

View File

@@ -1,116 +0,0 @@
#!/bin/bash
# Script to cancel all running GitHub Actions workflows
# Requires GitHub CLI (gh) to be installed and authenticated
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if gh CLI is installed
if ! command -v gh &> /dev/null; then
print_error "GitHub CLI (gh) is not installed. Please install it first:"
echo " brew install gh"
echo " Or visit: https://cli.github.com/"
exit 1
fi
# Check if authenticated
if ! gh auth status &> /dev/null; then
print_error "GitHub CLI is not authenticated. Please run:"
echo " gh auth login"
exit 1
fi
# Get repository info
REPO=$(gh repo view --json owner,name -q '.owner.login + "/" + .name')
if [[ -z "$REPO" ]]; then
print_error "Could not determine repository. Make sure you're in a GitHub repository."
exit 1
fi
print_status "Working with repository: $REPO"
# Get all active workflows (both queued and in-progress)
print_status "Fetching active workflows (queued and in-progress)..."
QUEUED_WORKFLOWS=$(gh run list --status queued --json databaseId,displayTitle,headBranch,status --limit 100)
IN_PROGRESS_WORKFLOWS=$(gh run list --status in_progress --json databaseId,displayTitle,headBranch,status --limit 100)
# Combine both lists
ALL_WORKFLOWS=$(echo "$QUEUED_WORKFLOWS $IN_PROGRESS_WORKFLOWS" | jq -s 'add | unique_by(.databaseId)')
if [[ "$ALL_WORKFLOWS" == "[]" ]]; then
print_status "No active workflows found."
exit 0
fi
# Parse and display active workflows
echo
print_warning "Found active workflows:"
echo "$ALL_WORKFLOWS" | jq -r '.[] | " - \(.displayTitle) (Branch: \(.headBranch), Status: \(.status), ID: \(.databaseId))"'
echo
read -p "Do you want to cancel ALL these workflows? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_status "Cancelled by user."
exit 0
fi
# Cancel each workflow
print_status "Cancelling workflows..."
CANCELLED_COUNT=0
FAILED_COUNT=0
while IFS= read -r WORKFLOW_ID; do
if [[ -n "$WORKFLOW_ID" ]]; then
print_status "Cancelling workflow ID: $WORKFLOW_ID"
if gh run cancel "$WORKFLOW_ID" 2>/dev/null; then
((CANCELLED_COUNT++))
else
print_error "Failed to cancel workflow ID: $WORKFLOW_ID"
((FAILED_COUNT++))
fi
fi
done < <(echo "$ALL_WORKFLOWS" | jq -r '.[].databaseId')
echo
print_status "Summary:"
echo " - Cancelled: $CANCELLED_COUNT workflows"
if [[ $FAILED_COUNT -gt 0 ]]; then
echo " - Failed: $FAILED_COUNT workflows"
fi
print_status "Done!"
# Optional: Show remaining active workflows
echo
print_status "Checking for any remaining active workflows..."
REMAINING_QUEUED=$(gh run list --status queued --json databaseId --limit 10)
REMAINING_IN_PROGRESS=$(gh run list --status in_progress --json databaseId --limit 10)
REMAINING_ALL=$(echo "$REMAINING_QUEUED $REMAINING_IN_PROGRESS" | jq -s 'add | unique_by(.databaseId)')
if [[ "$REMAINING_ALL" == "[]" ]]; then
print_status "All workflows successfully cancelled."
else
REMAINING_COUNT=$(echo "$REMAINING_ALL" | jq '. | length')
print_warning "Still $REMAINING_COUNT workflows active (may take a moment to update status)"
fi

View File

@@ -87,27 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.13" date="2025-10-11">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.13</url>
</release>
<release version="2.7.12" date="2025-10-01">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12</url>
</release>
<release version="2.7.11" date="2025-09-24">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11</url>
</release>
<release version="2.7.10" date="2025-09-18">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10</url>
</release>
<release version="2.7.9" date="2025-09-03">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
</release>
<release version="2.7.8" date="2025-08-30">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8</url>
</release>
<release version="2.7.7" date="2025-08-28">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7</url>
</release>
<release version="2.7.6" date="2025-08-12"> <release version="2.7.6" date="2025-08-12">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
</release> </release>

View File

@@ -6,8 +6,6 @@ from os.path import join
import subprocess import subprocess
import json import json
import re import re
import time
from datetime import datetime
from readprops import readProps from readprops import readProps
@@ -86,7 +84,7 @@ if platform.name == "espressif32":
if platform.name == "nordicnrf52": if platform.name == "nordicnrf52":
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"", env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py $BUILD_DIR/firmware.hex -c -f 0xADA52840 -o $BUILD_DIR/firmware.uf2",
"Generating UF2 file")) "Generating UF2 file"))
Import("projenv") Import("projenv")
@@ -127,16 +125,11 @@ for pref in userPrefs:
pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
# General options that are passed to the C and C++ compilers # General options that are passed to the C and C++ compilers
# Calculate unix epoch for current day (midnight)
current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
build_epoch = int(current_date.timestamp())
flags = [ flags = [
"-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION=" + verObj["long"],
"-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_VERSION_SHORT=" + verObj["short"],
"-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_ENV=" + env.get("PIOENV"),
"-DAPP_REPO=" + repo_owner, "-DAPP_REPO=" + repo_owner,
"-DBUILD_EPOCH=" + str(build_epoch),
] + pref_flags ] + pref_flags
print ("Using flags:") print ("Using flags:")

View File

@@ -1 +1 @@
2.6.6 2.6.4

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"],
["0x239A", "0x0071"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_solar",
"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/project/meshsolar/",
"vendor": "Heltec"
}

View File

@@ -1,43 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_qspi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "qspi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 2097152,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}

View File

@@ -1,37 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=0",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_wireless_tracker_v2"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Heltec Wireless Tracker V2",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org",
"vendor": "Heltec"
}

View File

@@ -1,52 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "MeshTiny",
"mcu": "nrf52840",
"variant": "meshtiny",
"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",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "freertos"],
"name": "MeshTiny",
"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://github.com/meshtastic/firmware",
"vendor": "MTools Tec"
}

View File

@@ -1,52 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "Muzi R1 Neo",
"mcu": "nrf52840",
"variant": "r1-neo",
"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",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "freertos"],
"name": "WisCore RAK4631 Board",
"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://muzi.works/",
"vendor": "Muzi Works"
}

View File

@@ -2,7 +2,7 @@
"build": { "build": {
"arduino": { "arduino": {
"ldscript": "esp32s3_out.ld", "ldscript": "esp32s3_out.ld",
"partitions": "partition-table-8MB.csv", "partitions": "default_8MB.csv",
"memory_type": "qio_opi" "memory_type": "qio_opi"
}, },
"core": "esp32", "core": "esp32",

View File

@@ -3,7 +3,7 @@
"arduino": { "arduino": {
"ldscript": "esp32s3_out.ld", "ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi", "memory_type": "qio_opi",
"partitions": "partition-table-8MB.csv" "partitions": "default_8MB.csv"
}, },
"core": "esp32", "core": "esp32",
"extra_flags": [ "extra_flags": [

View File

@@ -5,7 +5,7 @@
}, },
"core": "stm32", "core": "stm32",
"cpu": "cortex-m4", "cpu": "cortex-m4",
"extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX",
"f_cpu": "48000000L", "f_cpu": "48000000L",
"mcu": "stm32wle5ccu", "mcu": "stm32wle5ccu",
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U",

46
debian/changelog vendored
View File

@@ -1,19 +1,43 @@
meshtasticd (2.7.13.0) unstable; urgency=medium meshtasticd (2.7.6.0) UNRELEASED; urgency=medium
* Version 2.7.13
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 11 Oct 2025 15:27:28 +0000
meshtasticd (2.7.12.0) unstable; urgency=medium
[ Austin Lane ] [ Austin Lane ]
* Initial packaging * Initial packaging
* Version 2.5.19 * GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
[ ] [ ]
* GitHub Actions Automatic version bump * GitHub Actions Automatic version bump
[ GitHub Actions ] [ ]
* Version 2.7.12 * GitHub Actions Automatic version bump
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 01 Oct 2025 19:51:41 +0000 [ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ Ubuntu ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
-- <github-actions[bot]@users.noreply.github.com> Tue, 12 Aug 2025 23:48:48 +0000

View File

@@ -1,8 +1,7 @@
#!/usr/bin/bash #!/usr/bin/bash
export DEBFULLNAME="GitHub Actions"
export DEBEMAIL="github-actions[bot]@users.noreply.github.com" export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
PKG_VERSION=$(python3 bin/buildinfo.py short) PKG_VERSION=$(python3 bin/buildinfo.py short)
dch --newversion "$PKG_VERSION.0" \ dch --newversion "$PKG_VERSION.0" \
--distribution unstable \ --distribution UNRELEASED \
"Version $PKG_VERSION" "GitHub Actions Automatic version bump"

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
# postinst script for meshtasticd # postinst script for meshtasticd
# #
# see: dh_installdeb(1) # see: dh_installdeb(1)

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/bash
# postrm script for meshtasticd # postrm script for meshtasticd
# #
# see: dh_installdeb(1) # see: dh_installdeb(1)

View File

@@ -1,7 +0,0 @@
# This is a layout for 8MB of flash for MUI devices
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x5C0000,
flashApp, app, ota_1, 0x5D0000,0x0A0000,
spiffs, data, spiffs, 0x670000,0x180000
1 # This is a layout for 8MB of flash for MUI devices
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x5000,
4 otadata, data, ota, 0xe000, 0x2000,
5 app0, app, ota_0, 0x10000, 0x5C0000,
6 flashApp, app, ota_1, 0x5D0000,0x0A0000,
7 spiffs, data, spiffs, 0x670000,0x180000

View File

@@ -53,16 +53,14 @@ build_flags = -Wno-missing-field-initializers
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-DBUILD_EPOCH=$UNIX_TIME
#-D OLED_PL=1 #-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
#-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = direct monitor_filters = direct
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -70,7 +68,7 @@ lib_deps =
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master # renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master # renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip https://github.com/meshtastic/ArduinoThread/archive/7c3ee9e1951551b949763b1f5280f8db1fa4068d.zip
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb # renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
nanopb/Nanopb@0.4.91 nanopb/Nanopb@0.4.91
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32 # renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
@@ -115,18 +113,18 @@ lib_deps =
[radiolib_base] [radiolib_base]
lib_deps = lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.3.0 jgromes/RadioLib@7.2.1
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/19b7855e9a1d9deff37391659ca7194e4ef57c43.zip https://github.com/meshtastic/device-ui/archive/0cd108ff783539e41ef38258ba2784ab3b1bdc97.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
lib_deps = lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO # renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
adafruit/Adafruit BusIO@1.17.4 adafruit/Adafruit BusIO@1.17.2
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor # renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
adafruit/Adafruit Unified Sensor@1.1.15 adafruit/Adafruit Unified Sensor@1.1.15
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library # renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library
@@ -159,8 +157,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8 emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5 adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 # renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3 mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
@@ -179,8 +177,6 @@ lib_deps =
adafruit/Adafruit PCT2075@1.0.5 adafruit/Adafruit PCT2075@1.0.5
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
dfrobot/DFRobot_BMM150@1.0.0 dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2
; (not included in native / portduino) ; (not included in native / portduino)
[environmental_extra] [environmental_extra]

View File

@@ -8,7 +8,6 @@
"replacements:all", "replacements:all",
"workarounds:all" "workarounds:all"
], ],
"baseBranchPatterns": ["master"],
"forkProcessing": "enabled", "forkProcessing": "enabled",
"ignoreDeps": [ "ignoreDeps": [
"protobufs" "protobufs"

View File

@@ -183,9 +183,9 @@ class AmbientLightingThread : public concurrency::OSThread
#endif #endif
#endif #endif
pixels.show(); pixels.show();
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
#endif #endif
#ifdef RGBLED_CA #ifdef RGBLED_CA
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);

View File

@@ -11,11 +11,6 @@
#include <AudioOutputI2S.h> #include <AudioOutputI2S.h>
#include <ESP8266SAM.h> #include <ESP8266SAM.h>
#ifdef USE_XL9555
#include "ExtensionIOXL9555.hpp"
extern ExtensionIOXL9555 io;
#endif
#define AUDIO_THREAD_INTERVAL_MS 100 #define AUDIO_THREAD_INTERVAL_MS 100
class AudioThread : public concurrency::OSThread class AudioThread : public concurrency::OSThread
@@ -25,16 +20,12 @@ class AudioThread : public concurrency::OSThread
void beginRttl(const void *data, uint32_t len) void beginRttl(const void *data, uint32_t len)
{ {
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
setCPUFast(true); setCPUFast(true);
rtttlFile = new AudioFileSourcePROGMEM(data, len); rtttlFile = new AudioFileSourcePROGMEM(data, len);
i2sRtttl = new AudioGeneratorRTTTL(); i2sRtttl = new AudioGeneratorRTTTL();
i2sRtttl->begin(rtttlFile, audioOut); i2sRtttl->begin(rtttlFile, audioOut);
} }
// Also handles actually playing the RTTTL, needs to be called in loop
bool isPlaying() bool isPlaying()
{ {
if (i2sRtttl != nullptr) { if (i2sRtttl != nullptr) {
@@ -54,9 +45,6 @@ class AudioThread : public concurrency::OSThread
rtttlFile = nullptr; rtttlFile = nullptr;
setCPUFast(false); setCPUFast(false);
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, LOW);
#endif
} }
void readAloud(const char *text) void readAloud(const char *text)
@@ -67,16 +55,10 @@ class AudioThread : public concurrency::OSThread
i2sRtttl = nullptr; i2sRtttl = nullptr;
} }
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
#endif
ESP8266SAM *sam = new ESP8266SAM; ESP8266SAM *sam = new ESP8266SAM;
sam->Say(audioOut, text); sam->Say(audioOut, text);
delete sam; delete sam;
setCPUFast(false); setCPUFast(false);
#ifdef T_LORA_PAGER
io.digitalWrite(EXPANDS_AMP_EN, LOW);
#endif
} }
protected: protected:

View File

@@ -146,7 +146,7 @@ inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *mess
{ {
int result; int result;
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
bool utf = !portduino_config.ascii_logs; bool utf = !settingsMap[ascii_logs];
#else #else
bool utf = true; bool utf = true;
#endif #endif

View File

@@ -2,12 +2,6 @@
#include "configuration.h" #include "configuration.h"
// Forward declarations
#if defined(DEBUG_HEAP)
class MemGet;
extern MemGet memGet;
#endif
// DEBUG LED // DEBUG LED
#ifndef LED_STATE_ON #ifndef LED_STATE_ON
#define LED_STATE_ON 1 #define LED_STATE_ON 1
@@ -29,7 +23,6 @@ extern MemGet memGet;
#define MESHTASTIC_LOG_LEVEL_ERROR "ERROR" #define MESHTASTIC_LOG_LEVEL_ERROR "ERROR"
#define MESHTASTIC_LOG_LEVEL_CRIT "CRIT " #define MESHTASTIC_LOG_LEVEL_CRIT "CRIT "
#define MESHTASTIC_LOG_LEVEL_TRACE "TRACE" #define MESHTASTIC_LOG_LEVEL_TRACE "TRACE"
#define MESHTASTIC_LOG_LEVEL_HEAP "HEAP"
#include "SerialConsole.h" #include "SerialConsole.h"
@@ -69,25 +62,6 @@ extern MemGet memGet;
#endif #endif
#endif #endif
#if defined(DEBUG_HEAP)
#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__)
// Macro-based heap debugging
#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap();
#define DEBUG_HEAP_AFTER(context, ptr) \
do { \
auto heapAfter = memGet.getFreeHeap(); \
if (heapBefore != heapAfter) { \
LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \
} \
} while (0)
#else
#define LOG_HEAP(...)
#define DEBUG_HEAP_BEFORE
#define DEBUG_HEAP_AFTER(context, ptr)
#endif
/// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic
extern "C" void logLegacy(const char *level, const char *fmt, ...); extern "C" void logLegacy(const char *level, const char *fmt, ...);

View File

@@ -1,14 +1,7 @@
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName)
bool usePreset)
{ {
// If use_preset is false, always return "Custom"
if (!usePreset) {
return "Custom";
}
switch (preset) { switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
return useShortName ? "ShortT" : "ShortTurbo"; return useShortName ? "ShortT" : "ShortTurbo";
@@ -38,46 +31,4 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
return useShortName ? "Custom" : "Invalid"; return useShortName ? "Custom" : "Invalid";
break; break;
} }
}
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
{
switch (role) {
case meshtastic_Config_DeviceConfig_Role_CLIENT:
return "Client";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
return "Client Mute";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
return "Client Hidden";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE:
return "Client Base";
break;
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
return "Lost and Found";
break;
case meshtastic_Config_DeviceConfig_Role_TRACKER:
return "Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_SENSOR:
return "Sensor";
break;
case meshtastic_Config_DeviceConfig_Role_TAK:
return "TAK";
break;
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
return "TAK Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER:
return "Router";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
return "Router Late";
break;
default:
return "Unknown";
break;
}
} }

View File

@@ -4,7 +4,5 @@
class DisplayFormatters class DisplayFormatters
{ {
public: public:
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName);
bool usePreset);
static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role);
}; };

View File

@@ -22,9 +22,6 @@ class GPSStatus : public Status
meshtastic_Position p = meshtastic_Position_init_default; meshtastic_Position p = meshtastic_Position_init_default;
/// Time of last valid GPS fix (millis since boot)
uint32_t lastFixMillis = 0;
public: public:
GPSStatus() { statusType = STATUS_TYPE_GPS; } GPSStatus() { statusType = STATUS_TYPE_GPS; }
@@ -86,9 +83,6 @@ class GPSStatus : public Status
uint32_t getNumSatellites() const { return p.sats_in_view; } uint32_t getNumSatellites() const { return p.sats_in_view; }
/// Return millis() when the last GPS fix occurred (0 = never)
uint32_t getLastFixMillis() const { return lastFixMillis; }
bool matches(const GPSStatus *newStatus) const bool matches(const GPSStatus *newStatus) const
{ {
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
@@ -120,9 +114,6 @@ class GPSStatus : public Status
if (isDirty) { if (isDirty) {
if (hasLock) { if (hasLock) {
// Record time of last valid GPS fix
lastFixMillis = millis();
// In debug logs, identify position by @timestamp:stage (stage 3 = notify) // In debug logs, identify position by @timestamp:stage (stage 3 = notify)
LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp,
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,

View File

@@ -128,7 +128,6 @@ RAK9154Sensor rak9154Sensor;
#ifdef HAS_PPM #ifdef HAS_PPM
// note: XPOWERS_CHIP_XXX must be defined in variant.h // note: XPOWERS_CHIP_XXX must be defined in variant.h
#include <XPowersLib.h> #include <XPowersLib.h>
XPowersPPM *PPM = NULL;
#endif #endif
#ifdef HAS_BQ27220 #ifdef HAS_BQ27220
@@ -562,7 +561,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
config.power.device_battery_ina_address) { config.power.device_battery_ina_address) {
if (!ina226Sensor.isInitialized()) if (!ina226Sensor.isInitialized())
return ina226Sensor.runOnce() > 0; return ina226Sensor.runOnce() > 0;
return ina226Sensor.isRunning();
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first == } else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
config.power.device_battery_ina_address) { config.power.device_battery_ina_address) {
if (!ina260Sensor.isInitialized()) if (!ina260Sensor.isInitialized())
@@ -683,8 +681,6 @@ bool Power::setup()
found = true; found = true;
} else if (lipoChargerInit()) { } else if (lipoChargerInit()) {
found = true; found = true;
} else if (meshSolarInit()) {
found = true;
} else if (analogInit()) { } else if (analogInit()) {
found = true; found = true;
} }
@@ -692,16 +688,6 @@ bool Power::setup()
#ifdef NRF_APM #ifdef NRF_APM
found = true; found = true;
#endif #endif
#ifdef EXT_PWR_DETECT
attachInterrupt(
EXT_PWR_DETECT,
[]() {
power->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
},
CHANGE);
#endif
enabled = found; enabled = found;
low_voltage_counter = 0; low_voltage_counter = 0;
@@ -757,11 +743,7 @@ void Power::shutdown()
#if HAS_SCREEN #if HAS_SCREEN
if (screen) { if (screen) {
#ifdef T_DECK_PRO
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
#else
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
#endif
} }
#endif #endif
#if !defined(ARCH_STM32WL) #if !defined(ARCH_STM32WL)
@@ -779,7 +761,7 @@ void Power::shutdown()
#ifdef PIN_LED3 #ifdef PIN_LED3
ledOff(PIN_LED3); ledOff(PIN_LED3);
#endif #endif
doDeepSleep(DELAY_FOREVER, true, true); doDeepSleep(DELAY_FOREVER, false, true);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
#else #else
@@ -844,27 +826,18 @@ void Power::readPowerStatus()
newStatus.notifyObservers(&powerStatus2); newStatus.notifyObservers(&powerStatus2);
#ifdef DEBUG_HEAP #ifdef DEBUG_HEAP
if (lastheap != memGet.getFreeHeap()) { if (lastheap != memGet.getFreeHeap()) {
// Use stack-allocated buffer to avoid heap allocations in monitoring code std::string threadlist = "Threads running:";
char threadlist[256] = "Threads running:";
int threadlistLen = strlen(threadlist);
int running = 0; int running = 0;
for (int i = 0; i < MAX_THREADS; i++) { for (int i = 0; i < MAX_THREADS; i++) {
auto thread = concurrency::mainController.get(i); auto thread = concurrency::mainController.get(i);
if ((thread != nullptr) && (thread->enabled)) { if ((thread != nullptr) && (thread->enabled)) {
// Use snprintf to safely append to stack buffer without heap allocation threadlist += vformat(" %s", thread->ThreadName.c_str());
int remaining = sizeof(threadlist) - threadlistLen - 1;
if (remaining > 0) {
int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str());
if (written > 0 && written < remaining) {
threadlistLen += written;
}
}
running++; running++;
} }
} }
LOG_HEAP(threadlist); LOG_DEBUG(threadlist.c_str());
LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
lastheap = memGet.getFreeHeap(); lastheap = memGet.getFreeHeap();
} }
#ifdef DEBUG_HEAP_MQTT #ifdef DEBUG_HEAP_MQTT
@@ -876,19 +849,15 @@ void Power::readPowerStatus()
sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]);
auto newHeap = memGet.getFreeHeap(); auto newHeap = memGet.getFreeHeap();
// Use stack-allocated buffers to avoid heap allocations in monitoring code std::string heapTopic =
char heapTopic[128]; (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac);
snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); std::string heapString = std::to_string(newHeap);
char heapString[16]; mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false);
snprintf(heapString, sizeof(heapString), "%u", newHeap);
mqtt->pubSub.publish(heapTopic, heapString, false);
auto wifiRSSI = WiFi.RSSI(); auto wifiRSSI = WiFi.RSSI();
char wifiTopic[128]; std::string wifiTopic =
snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac);
char wifiString[16]; std::string wifiString = std::to_string(wifiRSSI);
snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false);
mqtt->pubSub.publish(wifiTopic, wifiString, false);
} }
#endif #endif
@@ -1349,6 +1318,7 @@ bool Power::lipoInit()
class LipoCharger : public HasBatteryLevel class LipoCharger : public HasBatteryLevel
{ {
private: private:
XPowersPPM *ppm = nullptr;
BQ27220 *bq = nullptr; BQ27220 *bq = nullptr;
public: public:
@@ -1357,41 +1327,41 @@ class LipoCharger : public HasBatteryLevel
*/ */
bool runOnce() bool runOnce()
{ {
if (PPM == nullptr) { if (ppm == nullptr) {
PPM = new XPowersPPM; ppm = new XPowersPPM;
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
if (result) { if (result) {
LOG_INFO("PPM BQ25896 init succeeded"); LOG_INFO("PPM BQ25896 init succeeded");
// Set the minimum operating voltage. Below this voltage, the PPM will protect // Set the minimum operating voltage. Below this voltage, the PPM will protect
// PPM->setSysPowerDownVoltage(3100); // ppm->setSysPowerDownVoltage(3100);
// Set input current limit, default is 500mA // Set input current limit, default is 500mA
// PPM->setInputCurrentLimit(800); // ppm->setInputCurrentLimit(800);
// Disable current limit pin // Disable current limit pin
// PPM->disableCurrentLimitPin(); // ppm->disableCurrentLimitPin();
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
PPM->setChargeTargetVoltage(4288); ppm->setChargeTargetVoltage(4288);
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
// PPM->setPrechargeCurr(64); // ppm->setPrechargeCurr(64);
// The premise is that limit pin is disabled, or it will // The premise is that limit pin is disabled, or it will
// only follow the maximum charging current set by limit pin. // only follow the maximum charging current set by limit pin.
// Set the charging current , Range:0~5056mA ,step:64mA // Set the charging current , Range:0~5056mA ,step:64mA
PPM->setChargerConstantCurr(1024); ppm->setChargerConstantCurr(1024);
// To obtain voltage data, the ADC must be enabled first // To obtain voltage data, the ADC must be enabled first
PPM->enableMeasure(); ppm->enableMeasure();
// Turn on charging function // Turn on charging function
// If there is no battery connected, do not turn on the charging function // If there is no battery connected, do not turn on the charging function
PPM->enableCharge(); ppm->enableCharge();
} else { } else {
LOG_WARN("PPM BQ25896 init failed"); LOG_WARN("PPM BQ25896 init failed");
delete PPM; delete ppm;
PPM = nullptr; ppm = nullptr;
return false; return false;
} }
} }
@@ -1432,23 +1402,23 @@ class LipoCharger : public HasBatteryLevel
/** /**
* return true if there is a battery installed in this unit * return true if there is a battery installed in this unit
*/ */
virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; }
/** /**
* return true if there is an external power source detected * return true if there is an external power source detected
*/ */
virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; }
/** /**
* return true if the battery is currently charging * return true if the battery is currently charging
*/ */
virtual bool isCharging() override virtual bool isCharging() override
{ {
bool isCharging = PPM->isCharging(); bool isCharging = ppm->isCharging();
if (isCharging) { if (isCharging) {
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
} else { } else {
if (!PPM->isVbusIn()) { if (!ppm->isVbusIn()) {
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
} }
} }
@@ -1480,73 +1450,3 @@ bool Power::lipoChargerInit()
return false; return false;
} }
#endif #endif
#ifdef HELTEC_MESH_SOLAR
#include "meshSolarApp.h"
/**
* meshSolar class for an SMBUS battery sensor.
*/
class meshSolarBatteryLevel : public HasBatteryLevel
{
public:
/**
* Init the I2C meshSolar battery level sensor
*/
bool runOnce()
{
meshSolarStart();
return true;
}
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }
/**
* return true if there is an external power source detected
*/
virtual bool isVbusIn() override { return meshSolarIsVbusIn(); }
/**
* return true if the battery is currently charging
*/
virtual bool isCharging() override { return meshSolarIsCharging(); }
};
meshSolarBatteryLevel meshSolarLevel;
/**
* Init the meshSolar battery level sensor
*/
bool Power::meshSolarInit()
{
bool result = meshSolarLevel.runOnce();
LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &meshSolarLevel;
return true;
}
#else
/**
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::meshSolarInit()
{
return false;
}
#endif

View File

@@ -4,7 +4,6 @@
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "configuration.h" #include "configuration.h"
#include "main.h" #include "main.h"
#include "memGet.h"
#include "mesh/generated/meshtastic/mesh.pb.h" #include "mesh/generated/meshtastic/mesh.pb.h"
#include <assert.h> #include <assert.h>
#include <cstring> #include <cstring>
@@ -58,7 +57,7 @@ size_t RedirectablePrint::vprintf(const char *logLevel, const char *format, va_l
#endif #endif
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
bool color = !portduino_config.ascii_logs; bool color = !settingsMap[ascii_logs];
#else #else
bool color = true; bool color = true;
#endif #endif
@@ -100,7 +99,7 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
size_t r = 0; size_t r = 0;
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
bool color = !portduino_config.ascii_logs; bool color = !settingsMap[ascii_logs];
#else #else
bool color = true; bool color = true;
#endif #endif
@@ -167,16 +166,6 @@ void RedirectablePrint::log_to_serial(const char *logLevel, const char *format,
print(thread->ThreadName); print(thread->ThreadName);
print("] "); print("] ");
} }
#ifdef DEBUG_HEAP
// Add heap free space bytes prefix before every log message
#ifdef ARCH_PORTDUINO
::printf("[heap %u] ", memGet.getFreeHeap());
#else
printf("[heap %u] ", memGet.getFreeHeap());
#endif
#endif // DEBUG_HEAP
r += vprintf(logLevel, format, arg); r += vprintf(logLevel, format, arg);
} }
@@ -299,7 +288,7 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
// level trace is special, two possible ways to handle it. // level trace is special, two possible ways to handle it.
if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { if (strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
if (portduino_config.traceFilename != "") { if (settingsStrings[traceFilename] != "") {
va_list arg; va_list arg;
va_start(arg, format); va_start(arg, format);
try { try {
@@ -308,18 +297,18 @@ void RedirectablePrint::log(const char *logLevel, const char *format, ...)
} }
va_end(arg); va_end(arg);
} }
if (portduino_config.logoutputlevel < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) { if (settingsMap[logoutputlevel] < level_trace && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_TRACE) == 0) {
delete[] newFormat; delete[] newFormat;
return; return;
} }
} }
if (portduino_config.logoutputlevel < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) { if (settingsMap[logoutputlevel] < level_debug && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_DEBUG) == 0) {
delete[] newFormat; delete[] newFormat;
return; return;
} else if (portduino_config.logoutputlevel < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) { } else if (settingsMap[logoutputlevel] < level_info && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_INFO) == 0) {
delete[] newFormat; delete[] newFormat;
return; return;
} else if (portduino_config.logoutputlevel < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) { } else if (settingsMap[logoutputlevel] < level_warn && strcmp(logLevel, MESHTASTIC_LOG_LEVEL_WARN) == 0) {
delete[] newFormat; delete[] newFormat;
return; return;
} }

View File

@@ -6,14 +6,6 @@
#include "configuration.h" #include "configuration.h"
#include "time.h" #include "time.h"
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#define IS_USB_SERIAL
#ifdef SERIAL_HAS_ON_RECEIVE
#undef SERIAL_HAS_ON_RECEIVE
#endif
#include "HWCDC.h"
#endif
#ifdef RP2040_SLOW_CLOCK #ifdef RP2040_SLOW_CLOCK
#define Port Serial2 #define Port Serial2
#else #else
@@ -30,12 +22,7 @@ SerialConsole *console;
void consoleInit() void consoleInit()
{ {
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
#if defined(SERIAL_HAS_ON_RECEIVE)
// onReceive does only exist for HardwareSerial not for USB CDC serial
Port.onReceive([sc]() { sc->rxInt(); });
#endif
DEBUG_PORT.rpInit(); // Simply sets up semaphore DEBUG_PORT.rpInit(); // Simply sets up semaphore
} }
@@ -77,22 +64,7 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
int32_t SerialConsole::runOnce() int32_t SerialConsole::runOnce()
{ {
#ifdef HELTEC_MESH_SOLAR return runOncePart();
// After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
if (moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port &&
moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) {
return 250;
}
#endif
int32_t delay = runOncePart();
#if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2)
return Port.available() ? delay : INT32_MAX;
#elif defined(IS_USB_SERIAL)
return HWCDC::isPlugged() ? delay : (1000 * 20);
#else
return delay;
#endif
} }
void SerialConsole::flush() void SerialConsole::flush()
@@ -100,18 +72,6 @@ void SerialConsole::flush()
Port.flush(); Port.flush();
} }
// trigger tx of serial data
void SerialConsole::onNowHasData(uint32_t fromRadioNum)
{
setIntervalFromNow(0);
}
// trigger rx of serial data
void SerialConsole::rxInt()
{
setIntervalFromNow(0);
}
// For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages // For the serial port we can't really detect if any client is on the other side, so instead just look for recent messages
bool SerialConsole::checkIsConnected() bool SerialConsole::checkIsConnected()
{ {

View File

@@ -32,14 +32,11 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
void flush(); void flush();
void rxInt();
protected: protected:
/// Check the current underlying physical link to see if the client is currently connected /// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override; virtual bool checkIsConnected() override;
virtual void onNowHasData(uint32_t fromRadioNum) override;
/// Possibly switch to protobufs if we see a valid protobuf message /// Possibly switch to protobufs if we see a valid protobuf message
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg); virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
}; };

View File

@@ -5,7 +5,7 @@
BuzzerFeedbackThread *buzzerFeedbackThread; BuzzerFeedbackThread *buzzerFeedbackThread;
BuzzerFeedbackThread::BuzzerFeedbackThread() BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback")
{ {
if (inputBroker) if (inputBroker)
inputObserver.observe(inputBroker); inputObserver.observe(inputBroker);
@@ -15,24 +15,24 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
{ {
// Only provide feedback if buzzer is enabled for notifications // Only provide feedback if buzzer is enabled for notifications
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED ||
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY || config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) {
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY) {
return 0; // Let other handlers process the event return 0; // Let other handlers process the event
} }
// Track last event time for potential future use
lastEventTime = millis();
needsUpdate = true;
// Handle different input events with appropriate buzzer feedback // Handle different input events with appropriate buzzer feedback
switch (event->inputEvent) { switch (event->inputEvent) {
case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_USER_PRESS:
case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_ALT_PRESS:
case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT:
case INPUT_BROKER_SELECT_LONG:
playBeep(); // Confirmation feedback playBeep(); // Confirmation feedback
break; break;
case INPUT_BROKER_UP: case INPUT_BROKER_UP:
case INPUT_BROKER_UP_LONG:
case INPUT_BROKER_DOWN: case INPUT_BROKER_DOWN:
case INPUT_BROKER_DOWN_LONG:
case INPUT_BROKER_LEFT: case INPUT_BROKER_LEFT:
case INPUT_BROKER_RIGHT: case INPUT_BROKER_RIGHT:
playChirp(); // Navigation feedback playChirp(); // Navigation feedback
@@ -58,4 +58,15 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
} }
return 0; // Allow other handlers to process the event return 0; // Allow other handlers to process the event
} }
int32_t BuzzerFeedbackThread::runOnce()
{
// This thread is primarily event-driven, but we can use runOnce
// for any periodic tasks if needed in the future
needsUpdate = false;
// Run every 100ms when active, less frequently when idle
return needsUpdate ? 100 : 1000;
}

View File

@@ -4,7 +4,7 @@
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "input/InputBroker.h" #include "input/InputBroker.h"
class BuzzerFeedbackThread class BuzzerFeedbackThread : public concurrency::OSThread
{ {
CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver = CallbackObserver<BuzzerFeedbackThread, const InputEvent *> inputObserver =
CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent); CallbackObserver<BuzzerFeedbackThread, const InputEvent *>(this, &BuzzerFeedbackThread::handleInputEvent);
@@ -12,6 +12,13 @@ class BuzzerFeedbackThread
public: public:
BuzzerFeedbackThread(); BuzzerFeedbackThread();
int handleInputEvent(const InputEvent *event); int handleInputEvent(const InputEvent *event);
protected:
virtual int32_t runOnce() override;
private:
uint32_t lastEventTime = 0;
bool needsUpdate = false;
}; };
extern BuzzerFeedbackThread *buzzerFeedbackThread; extern BuzzerFeedbackThread *buzzerFeedbackThread;

View File

@@ -86,13 +86,11 @@ void OSThread::run()
#ifdef DEBUG_HEAP #ifdef DEBUG_HEAP
auto newHeap = memGet.getFreeHeap(); auto newHeap = memGet.getFreeHeap();
if (newHeap < heap) if (newHeap < heap)
LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
if (heap < newHeap) if (heap < newHeap)
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
#endif
#ifdef DEBUG_LOOP_TIMING
LOG_DEBUG("====== Thread next run in: %d", newDelay);
#endif #endif
runned(); runned();
if (newDelay >= 0) if (newDelay >= 0)

View File

@@ -26,16 +26,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <Arduino.h> #include <Arduino.h>
#if __has_include("Melopero_RV3028.h") #ifdef RV3028_RTC
#include "Melopero_RV3028.h" #include "Melopero_RV3028.h"
#endif #endif
#if __has_include("pcf8563.h") #ifdef PCF8563_RTC
#include "pcf8563.h" #include "pcf8563.h"
#endif #endif
/* Offer chance for variant-specific defines */
#include "variant.h"
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Version // Version
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -120,12 +117,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define SX126X_MAX_POWER 22 #define SX126X_MAX_POWER 22
#endif #endif
#ifdef USE_GC1109_PA
// Power Amps are often non-linear, so we can use an array of values for the power curve
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
#endif
// Default system gain to 0 if not defined // Default system gain to 0 if not defined
#ifndef TX_GAIN_LORA #ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0 #define TX_GAIN_LORA 0
@@ -144,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// OLED & Input // OLED & Input
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #if defined(SEEED_WIO_TRACKER_L1)
#define SSD1306_ADDRESS 0x3D #define SSD1306_ADDRESS 0x3D
#define USE_SH1106 #define USE_SH1106
#else #else
@@ -263,18 +254,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// convert 24-bit color to 16-bit (56K) // convert 24-bit color to 16-bit (56K)
#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) #define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
/* Step #1: offer chance for variant-specific defines */
#include "variant.h"
#if defined(VEXT_ENABLE) && !defined(VEXT_ON_VALUE) #if defined(VEXT_ENABLE) && !defined(VEXT_ON_VALUE)
// Older variant.h files might not be defining this value, so stay with the old default // Older variant.h files might not be defining this value, so stay with the old default
#define VEXT_ON_VALUE LOW #define VEXT_ON_VALUE LOW
#endif #endif
// -----------------------------------------------------------------------------
// Rotary encoder
// -----------------------------------------------------------------------------
#ifndef ROTARY_DELAY
#define ROTARY_DELAY 5
#endif
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// GPS // GPS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@@ -25,8 +25,8 @@ ScanI2C::FoundDevice ScanI2C::firstScreen() const
ScanI2C::FoundDevice ScanI2C::firstRTC() const ScanI2C::FoundDevice ScanI2C::firstRTC() const
{ {
ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563, RTC_RX8130CE}; ScanI2C::DeviceType types[] = {RTC_RV3028, RTC_PCF8563};
return firstOfOrNONE(3, types); return firstOfOrNONE(2, types);
} }
ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstKeyboard() const

View File

@@ -14,7 +14,6 @@ class ScanI2C
SCREEN_ST7567, SCREEN_ST7567,
RTC_RV3028, RTC_RV3028,
RTC_PCF8563, RTC_PCF8563,
RTC_RX8130CE,
CARDKB, CARDKB,
TDECKKB, TDECKKB,
BBQ10KB, BBQ10KB,
@@ -80,9 +79,7 @@ class ScanI2C
BQ27220, BQ27220,
LTR553ALS, LTR553ALS,
BHI260AP, BHI260AP,
BMM150, BMM150
TSL2561,
DRV2605
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;

View File

@@ -1,16 +0,0 @@
#include "ScanI2CConsumer.h"
#include <forward_list>
static std::forward_list<ScanI2CConsumer *> ScanI2CConsumers;
ScanI2CConsumer::ScanI2CConsumer()
{
ScanI2CConsumers.push_front(this);
}
void ScanI2CCompleted(ScanI2C *i2cScanner)
{
for (ScanI2CConsumer *consumer : ScanI2CConsumers) {
consumer->i2cScanFinished(i2cScanner);
}
}

View File

@@ -1,13 +0,0 @@
#pragma once
#include "ScanI2C.h"
#include <stddef.h>
class ScanI2CConsumer
{
public:
ScanI2CConsumer();
virtual void i2cScanFinished(ScanI2C *i2cScanner) = 0;
};
void ScanI2CCompleted(ScanI2C *i2cScanner);

View File

@@ -197,9 +197,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#ifdef PCF8563_RTC #ifdef PCF8563_RTC
SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address) SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
#endif #endif
#ifdef RX8130CE_RTC
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif
case CARDKB_ADDR: case CARDKB_ADDR:
// Do we have the RAK14006 instead? // Do we have the RAK14006 instead?
@@ -297,7 +294,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = AHT10; type = AHT10;
break; break;
#endif #endif
#if !defined(M5STACK_UNITC6L)
case INA_ADDR: case INA_ADDR:
case INA_ADDR_ALTERNATE: case INA_ADDR_ALTERNATE:
case INA_ADDR_WAVESHARE_UPS: case INA_ADDR_WAVESHARE_UPS:
@@ -344,7 +340,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
// else: probably a RAK12500/UBLOX GPS on I2C // else: probably a RAK12500/UBLOX GPS on I2C
} }
break; break;
#endif
case MCP9808_ADDR: case MCP9808_ADDR:
// We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some // We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some
// weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips. // weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips.
@@ -378,8 +373,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2); registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0x11f3 || registerValue == 0xe9c || if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) {
registerValue == 0xc8d) {
type = SHT4X; type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address); logFoundDevice("SHT4X", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {
@@ -467,17 +461,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
case TSL25911_ADDR: SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1);
if (registerValue == 0x50) {
type = TSL2591;
logFoundDevice("TSL25911", (uint8_t)addr.address);
} else {
type = TSL2561;
logFoundDevice("TSL2561", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
@@ -499,14 +483,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = MLX90614; type = MLX90614;
logFoundDevice("MLX90614", (uint8_t)addr.address); logFoundDevice("MLX90614", (uint8_t)addr.address);
} else { } else {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS type = MPR121KB;
if (registerValue == 0xe0) { logFoundDevice("MPR121KB", (uint8_t)addr.address);
type = DRV2605;
logFoundDevice("DRV2605", (uint8_t)addr.address);
} else {
type = MPR121KB;
logFoundDevice("MPR121KB", (uint8_t)addr.address);
}
} }
break; break;
@@ -581,7 +559,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port)
scanPort(port, nullptr, 0); scanPort(port, nullptr, 0);
} }
TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) const
{ {
if (address.port == ScanI2C::I2CPort::WIRE) { if (address.port == ScanI2C::I2CPort::WIRE) {
return &Wire; return &Wire;

View File

@@ -23,12 +23,12 @@ class ScanI2CTwoWire : public ScanI2C
ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override; ScanI2C::FoundDevice find(ScanI2C::DeviceType) const override;
TwoWire *fetchI2CBus(ScanI2C::DeviceAddress) const;
bool exists(ScanI2C::DeviceType) const override; bool exists(ScanI2C::DeviceType) const override;
size_t countDevices() const override; size_t countDevices() const override;
static TwoWire *fetchI2CBus(ScanI2C::DeviceAddress);
protected: protected:
FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override; FoundDevice firstOfOrNONE(size_t, DeviceType[]) const override;

View File

@@ -1,4 +1,5 @@
#include <cstring> // Include for strstr #include <cstring> // Include for strstr
#include <string>
#include <vector> #include <vector>
#include "configuration.h" #include "configuration.h"
@@ -494,27 +495,38 @@ bool GPS::setup()
if (!didSerialInit) { if (!didSerialInit) {
int msglen = 0; int msglen = 0;
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) { if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
#ifdef TRACKER_T1000_E
// add power up/down strategy, improve ag3335 detection success
digitalWrite(PIN_GPS_EN, LOW);
delay(500);
digitalWrite(GPS_VRTC_EN, LOW);
delay(1000);
digitalWrite(GPS_VRTC_EN, HIGH);
delay(500);
digitalWrite(PIN_GPS_EN, HIGH);
delay(1000);
#endif
if (probeTries < GPS_PROBETRIES) { if (probeTries < GPS_PROBETRIES) {
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
gnssModel = probe(serialSpeeds[speedSelect]); gnssModel = probe(serialSpeeds[speedSelect]);
if (gnssModel == GNSS_MODEL_UNKNOWN) { if (gnssModel == GNSS_MODEL_UNKNOWN) {
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) { if (++speedSelect == array_count(serialSpeeds)) {
speedSelect = 0; speedSelect = 0;
++probeTries; ++probeTries;
} }
} }
} }
// Rare Serial Speeds // Rare Serial Speeds
#ifndef CONFIG_IDF_TARGET_ESP32C6
if (probeTries == GPS_PROBETRIES) { if (probeTries == GPS_PROBETRIES) {
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
gnssModel = probe(rareSerialSpeeds[speedSelect]); gnssModel = probe(rareSerialSpeeds[speedSelect]);
if (gnssModel == GNSS_MODEL_UNKNOWN) { if (gnssModel == GNSS_MODEL_UNKNOWN) {
if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) { if (++speedSelect == array_count(rareSerialSpeeds)) {
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE); LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
return true; return true;
} }
} }
} }
#endif
} }
if (gnssModel != GNSS_MODEL_UNKNOWN) { if (gnssModel != GNSS_MODEL_UNKNOWN) {
@@ -796,14 +808,6 @@ bool GPS::setup()
} else { } else {
LOG_INFO("GNSS module configuration saved!"); LOG_INFO("GNSS module configuration saved!");
} }
} else if (gnssModel == GNSS_MODEL_CM121) {
// only ask for RMC and GGA
// enable GGA
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
delay(250);
// enable RMC
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
delay(250);
} }
didSerialInit = true; didSerialInit = true;
} }
@@ -839,6 +843,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
setPowerPMU(true); // Power (PMU): on setPowerPMU(true); // Power (PMU): on
writePinStandby(false); // Standby (pin): awake (not standby) writePinStandby(false); // Standby (pin): awake (not standby)
setPowerUBLOX(true); // Standby (UBLOX): awake setPowerUBLOX(true); // Standby (UBLOX): awake
#ifdef GNSS_AIROHA
lastFixStartMsec = 0;
#endif
break; break;
case GPS_SOFTSLEEP: case GPS_SOFTSLEEP:
@@ -856,7 +863,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
writePinStandby(true); // Standby (pin): asleep (not awake) writePinStandby(true); // Standby (pin): asleep (not awake)
setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed
#ifdef GNSS_AIROHA #ifdef GNSS_AIROHA
digitalWrite(PIN_GPS_EN, LOW); if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
@@ -868,7 +877,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
writePinStandby(true); // Standby (pin): asleep writePinStandby(true); // Standby (pin): asleep
setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely
#ifdef GNSS_AIROHA #ifdef GNSS_AIROHA
digitalWrite(PIN_GPS_EN, LOW); if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
} }
@@ -1020,7 +1031,7 @@ void GPS::down()
LOG_DEBUG("%us until next search", sleepTime / 1000); LOG_DEBUG("%us until next search", sleepTime / 1000);
// If update interval less than 10 seconds, no attempt to sleep // If update interval less than 10 seconds, no attempt to sleep
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0) if (updateInterval <= 10 * 1000UL || sleepTime == 0)
setPowerState(GPS_IDLE); setPowerState(GPS_IDLE);
else { else {
@@ -1051,8 +1062,6 @@ void GPS::down()
} }
// If update interval long enough (or softsleep unsupported): hardsleep instead // If update interval long enough (or softsleep unsupported): hardsleep instead
setPowerState(GPS_HARDSLEEP, sleepTime); setPowerState(GPS_HARDSLEEP, sleepTime);
// Reset the fix quality to 0, since we're off.
fixQual = 0;
} }
} }
@@ -1081,7 +1090,7 @@ int32_t GPS::runOnce()
return disable(); return disable();
} }
if (!setup()) if (!setup())
return currentDelay; // Setup failed, re-run in two seconds return 2000; // Setup failed, re-run in two seconds
// We have now loaded our saved preferences from flash // We have now loaded our saved preferences from flash
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
@@ -1091,29 +1100,11 @@ int32_t GPS::runOnce()
publishUpdate(); publishUpdate();
} }
// ======================== GPS_ACTIVE state ======================== // Repeaters have no need for GPS
// In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages. if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
// We use the following logic to determine when to update the local position return disable();
// or time by running GPS::publishUpdate. }
// Note: Local position update is asynchronous to position broadcast. We
// generally run this state every gps_update_interval seconds, and in most cases
// gps_update_interval is faster than the position broadcast interval so there's a
// fresh position ready when the device wants to broadcast one on the mesh.
//
// 1. Got a time for the first time --> set the time, don't publish.
// 2. Got a lock for the first time
// --> If gps_update_interval is <= 10s --> publishUpdate
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
// 3. Got a lock after turning back on
// --> If gps_update_interval is <= 10s --> publishUpdate
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
// 4. Hold has expired
// --> If we have a time and a location --> publishUpdate
// --> down()
// 5. Search time has expired
// --> If we have a time and a location --> publishUpdate
// --> If we had a location before but don't now --> publishUpdate
// --> down()
if (whileActive()) { if (whileActive()) {
// if we have received valid NMEA claim we are connected // if we have received valid NMEA claim we are connected
setConnected(); setConnected();
@@ -1123,81 +1114,42 @@ int32_t GPS::runOnce()
if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue()) if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue())
up(); up();
// quality of the previous fix. We set it to 0 when we go down, so it's a way // If we've already set time from the GPS, no need to ask the GPS
// to check if we're getting a lock after being GPS_OFF. bool gotTime = (getRTCQuality() >= RTCQualityGPS);
uint8_t prev_fixQual = fixQual; if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
gotTime = true;
shouldPublish = true;
}
if (powerState == GPS_ACTIVE) { bool gotLoc = lookForLocation();
// if gps_update_interval is <=10s, GPS never goes off, so we treat that differently if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval); LOG_DEBUG("hasValidLocation RISING EDGE");
hasValidLocation = true;
shouldPublish = true;
}
// 1. Got a time for the first time bool tooLong = scheduling.searchedTooLong();
bool gotTime = (getRTCQuality() >= RTCQualityGPS); if (tooLong)
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
gotTime = true;
}
// 2. Got a lock for the first time, or 3. Got a lock after turning back on // Once we get a location we no longer desperately want an update
bool gotLoc = lookForLocation(); if ((gotLoc && gotTime) || tooLong) {
if (gotLoc) {
#ifdef GPS_DEBUG
if (!hasValidLocation) { // declare that we have location ASAP
LOG_DEBUG("hasValidLocation RISING EDGE");
}
#endif
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) {
hasValidLocation = true;
shouldPublish = true;
} else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) {
hasValidLocation = true;
// Hold for up to 20secs after getting a lock to download ephemeris etc
uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS;
if (holdTime > GPS_FIX_HOLD_MAX_MS)
holdTime = GPS_FIX_HOLD_MAX_MS;
fixHoldEnds = millis() + holdTime;
#ifdef GPS_DEBUG
LOG_DEBUG("Holding for %ums after lock", holdTime);
#endif
}
}
bool tooLong = scheduling.searchedTooLong(); if (tooLong) {
if (tooLong && !gotLoc) {
LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
// we didn't get a location during this ack window, therefore declare loss of lock // we didn't get a location during this ack window, therefore declare loss of lock
if (hasValidLocation) { if (hasValidLocation) {
p = meshtastic_Position_init_default;
hasValidLocation = false;
shouldPublish = true;
#ifdef GPS_DEBUG
LOG_DEBUG("hasValidLocation FALLING EDGE"); LOG_DEBUG("hasValidLocation FALLING EDGE");
#endif
} }
p = meshtastic_Position_init_default;
hasValidLocation = false;
} }
// Hold has expired , Search time has expired, we got a time only, or we never needed to hold. down();
bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds); shouldPublish = true; // publish our update for this just finished acquisition window
if (shouldPublish || tooLong || holdExpired) {
if (gotTime && hasValidLocation) {
shouldPublish = true;
}
if (shouldPublish) {
fixHoldEnds = 0;
publishUpdate();
}
// There's a chance we just got a time, so keep going to see if we can get a location too
if (tooLong || holdExpired) {
down();
}
#ifdef GPS_DEBUG
} else if (fixHoldEnds != 0) {
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
#endif
}
} }
// ===================== end GPS_ACTIVE state ========================
// If state has changed do a publish
publishUpdate();
if (config.position.fixed_position == true && hasValidLocation) if (config.position.fixed_position == true && hasValidLocation)
return disable(); // This should trigger when we have a fixed position, and get that first position return disable(); // This should trigger when we have a fixed position, and get that first position
@@ -1246,7 +1198,7 @@ static const char *DETECTED_MESSAGE = "%s detected";
LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \
clearBuffer(); \ clearBuffer(); \
_serial_gps->write(COMMAND "\r\n"); \ _serial_gps->write(COMMAND "\r\n"); \
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \
if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \
return detectedDriver; \ return detectedDriver; \
} \ } \
@@ -1254,249 +1206,190 @@ static const char *DETECTED_MESSAGE = "%s detected";
GnssModel_t GPS::probe(int serialSpeed) GnssModel_t GPS::probe(int serialSpeed)
{ {
uint8_t buffer[768] = {0};
switch (currentStep) {
case 0: {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) #if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
_serial_gps->end(); _serial_gps->end();
_serial_gps->begin(serialSpeed); _serial_gps->begin(serialSpeed);
#elif defined(ARCH_RP2040) #elif defined(ARCH_RP2040)
_serial_gps->end(); _serial_gps->end();
_serial_gps->setFIFOSize(256); _serial_gps->setFIFOSize(256);
_serial_gps->begin(serialSpeed); _serial_gps->begin(serialSpeed);
#else #else
if (_serial_gps->baudRate() != serialSpeed) { if (_serial_gps->baudRate() != serialSpeed) {
LOG_DEBUG("Set GPS Baud to %i", serialSpeed); LOG_DEBUG("Set Baud to %i", serialSpeed);
_serial_gps->updateBaudRate(serialSpeed); _serial_gps->updateBaudRate(serialSpeed);
} }
#endif #endif
memset(&ublox_info, 0, sizeof(ublox_info)); memset(&ublox_info, 0, sizeof(ublox_info));
delay(100); uint8_t buffer[768] = {0};
delay(100);
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices) // Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
_serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n");
delay(20); delay(20);
// Close NMEA sequences on Ublox // Close NMEA sequences on Ublox
_serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n"); _serial_gps->write("$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n");
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n"); _serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
_serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n"); _serial_gps->write("$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n");
delay(20); delay(20);
// Close NMEA sequences on CM121
_serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n");
_serial_gps->write("$CFGMSG,0,2,0,1*18\r\n");
_serial_gps->write("$CFGMSG,0,3,0,1*19\r\n");
currentDelay = 20;
currentStep = 1;
return GNSS_MODEL_UNKNOWN;
}
case 1: {
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
std::vector<ChipInfo> unicore = { std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
currentDelay = 20;
currentStep = 2;
return GNSS_MODEL_UNKNOWN;
}
case 2: {
std::vector<ChipInfo> atgm = {
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
currentDelay = 20;
currentStep = 3;
return GNSS_MODEL_UNKNOWN;
}
case 3: {
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
std::vector<ChipInfo> airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
currentDelay = 20;
currentStep = 4;
return GNSS_MODEL_UNKNOWN;
}
case 4: {
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
currentDelay = 20;
currentStep = 5;
return GNSS_MODEL_UNKNOWN;
}
case 5: {
// Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms std::vector<ChipInfo> atgm = {
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n"); {"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
delay(20); /* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
std::vector<ChipInfo> mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D}, {"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
{"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B}, PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
{"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B},
{"L80", "_3339_", GNSS_MODEL_MTK_L76B}};
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500); /* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
currentDelay = 20; _serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
currentStep = 6; _serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
std::vector<ChipInfo> airoha = {{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
// Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
delay(20);
std::vector<ChipInfo> mtk = {{"L76B", "Quectel-L76B", GNSS_MODEL_MTK_L76B}, {"PA1010D", "1010D", GNSS_MODEL_MTK_PA1010D},
{"PA1616S", "1616S", GNSS_MODEL_MTK_PA1616S}, {"LS20031", "MC-1513", GNSS_MODEL_MTK_L76B},
{"L96", "Quectel-L96", GNSS_MODEL_MTK_L76B}, {"L80-R", "_3337_", GNSS_MODEL_MTK_L76B},
{"L80", "_3339_", GNSS_MODEL_MTK_L76B}};
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
UBXChecksum(cfg_rate, sizeof(cfg_rate));
clearBuffer();
_serial_gps->write(cfg_rate, sizeof(cfg_rate));
// Check that the returned response class and message ID are correct
GPS_RESPONSE response = getACK(0x06, 0x08, 750);
if (response == GNSS_RESPONSE_NONE) {
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
return GNSS_MODEL_UNKNOWN; return GNSS_MODEL_UNKNOWN;
} else if (response == GNSS_RESPONSE_FRAME_ERRORS) {
LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed);
} }
case 6: {
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00}; memset(buffer, 0, sizeof(buffer));
UBXChecksum(cfg_rate, sizeof(cfg_rate)); uint8_t _message_MONVER[8] = {
clearBuffer(); 0xB5, 0x62, // Sync message for UBX protocol
_serial_gps->write(cfg_rate, sizeof(cfg_rate)); 0x0A, 0x04, // Message class and ID (UBX-MON-VER)
// Check that the returned response class and message ID are correct 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload)
GPS_RESPONSE response = getACK(0x06, 0x08, 750); 0x00, 0x00 // Checksum
if (response == GNSS_RESPONSE_NONE) { };
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); // Get Ublox gnss module hardware and software info
currentDelay = 2000; UBXChecksum(_message_MONVER, sizeof(_message_MONVER));
currentStep = 0; clearBuffer();
return GNSS_MODEL_UNKNOWN; _serial_gps->write(_message_MONVER, sizeof(_message_MONVER));
} else if (response == GNSS_RESPONSE_FRAME_ERRORS) {
LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed); uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200);
if (len) {
uint16_t position = 0;
for (int i = 0; i < 30; i++) {
ublox_info.swVersion[i] = buffer[position];
position++;
}
for (int i = 0; i < 10; i++) {
ublox_info.hwVersion[i] = buffer[position];
position++;
}
while (len >= position + 30) {
for (int i = 0; i < 30; i++) {
ublox_info.extension[ublox_info.extensionNo][i] = buffer[position];
position++;
}
ublox_info.extensionNo++;
if (ublox_info.extensionNo > 9)
break;
}
LOG_DEBUG("Module Info : ");
LOG_DEBUG("Soft version: %s", ublox_info.swVersion);
LOG_DEBUG("Hard version: %s", ublox_info.hwVersion);
LOG_DEBUG("Extensions:%d", ublox_info.extensionNo);
for (int i = 0; i < ublox_info.extensionNo; i++) {
LOG_DEBUG(" %s", ublox_info.extension[i]);
} }
memset(buffer, 0, sizeof(buffer)); memset(buffer, 0, sizeof(buffer));
uint8_t _message_MONVER[8] = {
0xB5, 0x62, // Sync message for UBX protocol
0x0A, 0x04, // Message class and ID (UBX-MON-VER)
0x00, 0x00, // Length of payload (we're asking for an answer, so no payload)
0x00, 0x00 // Checksum
};
// Get Ublox gnss module hardware and software info
UBXChecksum(_message_MONVER, sizeof(_message_MONVER));
clearBuffer();
_serial_gps->write(_message_MONVER, sizeof(_message_MONVER));
uint16_t len = getACK(buffer, sizeof(buffer), 0x0A, 0x04, 1200); // tips: extensionNo field is 0 on some 6M GNSS modules
if (len) { for (int i = 0; i < ublox_info.extensionNo; ++i) {
uint16_t position = 0; if (!strncmp(ublox_info.extension[i], "MOD=", 4)) {
for (int i = 0; i < 30; i++) { strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer));
ublox_info.swVersion[i] = buffer[position]; } else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) {
position++; char *ptr = nullptr;
} memset(buffer, 0, sizeof(buffer));
for (int i = 0; i < 10; i++) { strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer));
ublox_info.hwVersion[i] = buffer[position]; LOG_DEBUG("Protocol Version:%s", (char *)buffer);
position++; if (strlen((char *)buffer)) {
} ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10);
LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version);
while (len >= position + 30) { } else {
for (int i = 0; i < 30; i++) { ublox_info.protocol_version = 0;
ublox_info.extension[ublox_info.extensionNo][i] = buffer[position];
position++;
} }
ublox_info.extensionNo++;
if (ublox_info.extensionNo > 9)
break;
}
LOG_DEBUG("Module Info : ");
LOG_DEBUG("Soft version: %s", ublox_info.swVersion);
LOG_DEBUG("Hard version: %s", ublox_info.hwVersion);
LOG_DEBUG("Extensions:%d", ublox_info.extensionNo);
for (int i = 0; i < ublox_info.extensionNo; i++) {
LOG_DEBUG(" %s", ublox_info.extension[i]);
}
memset(buffer, 0, sizeof(buffer));
// tips: extensionNo field is 0 on some 6M GNSS modules
for (int i = 0; i < ublox_info.extensionNo; ++i) {
if (!strncmp(ublox_info.extension[i], "MOD=", 4)) {
strncpy((char *)buffer, &(ublox_info.extension[i][4]), sizeof(buffer));
} else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) {
char *ptr = nullptr;
memset(buffer, 0, sizeof(buffer));
strncpy((char *)buffer, &(ublox_info.extension[i][8]), sizeof(buffer));
LOG_DEBUG("Protocol Version:%s", (char *)buffer);
if (strlen((char *)buffer)) {
ublox_info.protocol_version = strtoul((char *)buffer, &ptr, 10);
LOG_DEBUG("ProtVer=%d", ublox_info.protocol_version);
} else {
ublox_info.protocol_version = 0;
}
}
}
if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6");
return GNSS_MODEL_UBLOX6;
} else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7");
return GNSS_MODEL_UBLOX7;
} else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8");
return GNSS_MODEL_UBLOX8;
} else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9");
return GNSS_MODEL_UBLOX9;
} else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10");
return GNSS_MODEL_UBLOX10;
} }
} }
} if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 6", "6");
return GNSS_MODEL_UBLOX6;
} else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 7", "7");
return GNSS_MODEL_UBLOX7;
} else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 8", "8");
return GNSS_MODEL_UBLOX8;
} else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 9", "9");
return GNSS_MODEL_UBLOX9;
} else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) {
LOG_INFO(DETECTED_MESSAGE, "U-blox 10", "10");
return GNSS_MODEL_UBLOX10;
}
} }
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed); LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
currentDelay = 2000;
currentStep = 0;
return GNSS_MODEL_UNKNOWN; return GNSS_MODEL_UNKNOWN;
} }
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed) GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap)
{ {
// Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline String response = "";
// Higher baud rates get proportionally larger buffers to handle more data
int bufferSize = (serialSpeed * 256) / 9600;
// Clamp buffer size between reasonable limits
if (bufferSize < 128)
bufferSize = 128;
if (bufferSize > 2048)
bufferSize = 2048;
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
uint16_t responseLen = 0;
unsigned long start = millis(); unsigned long start = millis();
while (millis() - start < timeout) { while (millis() - start < timeout) {
if (_serial_gps->available()) { if (_serial_gps->available()) {
char c = _serial_gps->read(); response += (char)_serial_gps->read();
// Add char to buffer if there's space if (response.endsWith(",") || response.endsWith("\r\n")) {
if (responseLen < bufferSize - 1) {
response[responseLen++] = c;
response[responseLen] = '\0';
}
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
LOG_DEBUG(response); LOG_DEBUG(response.c_str());
#endif #endif
// check if we can see our chips // check if we can see our chips
for (const auto &chipInfo : responseMap) { for (const auto &chipInfo : responseMap) {
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
LOG_INFO("%s detected", chipInfo.chipName.c_str()); LOG_INFO("%s detected", chipInfo.chipName.c_str());
delete[] response; // Cleanup before return
return chipInfo.driver; return chipInfo.driver;
} }
} }
} }
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { if (response.endsWith("\r\n")) {
// Reset the response buffer for the next potential message response.trim();
responseLen = 0; response = ""; // Reset the response string for the next potential message
response[0] = '\0';
} }
} }
} }
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
LOG_DEBUG(response); LOG_DEBUG(response.c_str());
#endif #endif
delete[] response; // Cleanup before return return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
} }
GPS *GPS::createGps() GPS *GPS::createGps()
@@ -1521,7 +1414,7 @@ GPS *GPS::createGps()
_en_gpio = PIN_GPS_EN; _en_gpio = PIN_GPS_EN;
#endif #endif
#ifdef ARCH_PORTDUINO #ifdef ARCH_PORTDUINO
if (!portduino_config.has_gps) if (!settingsMap[has_gps])
return nullptr; return nullptr;
#endif #endif
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
@@ -1611,10 +1504,28 @@ static int32_t toDegInt(RawDegrees d)
* Perform any processing that should be done only while the GPS is awake and looking for a fix. * Perform any processing that should be done only while the GPS is awake and looking for a fix.
* Override this method to check for new locations * Override this method to check for new locations
* *
* @return true if we've set a new time * @return true if we've acquired a new location
*/ */
bool GPS::lookForTime() bool GPS::lookForTime()
{ {
#ifdef GNSS_AIROHA
uint8_t fix = reader.fixQuality();
if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
} else {
clearBuffer();
}
} else {
lastFixStartMsec = millis();
return false;
}
} else {
return false;
}
#endif
auto ti = reader.time; auto ti = reader.time;
auto d = reader.date; auto d = reader.date;
if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed
@@ -1631,13 +1542,13 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
t.tm_year = d.year() - 1900; t.tm_year = d.year() - 1900;
t.tm_isdst = false; t.tm_isdst = false;
if (t.tm_mon > -1) { if (t.tm_mon > -1) {
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_sec, ti.age());
t.tm_min, t.tm_sec, ti.age()); if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) {
return true; // Clear the GPS buffer if we got an invalid time
} else { clearBuffer();
return false;
} }
return true;
} else } else
return false; return false;
} else } else
@@ -1652,6 +1563,25 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
*/ */
bool GPS::lookForLocation() bool GPS::lookForLocation()
{ {
#ifdef GNSS_AIROHA
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
uint8_t fix = reader.fixQuality();
if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
} else {
clearBuffer();
}
} else {
lastFixStartMsec = millis();
return false;
}
} else {
return false;
}
}
#endif
// By default, TinyGPS++ does not parse GPGSA lines, which give us // By default, TinyGPS++ does not parse GPGSA lines, which give us
// the 2D/3D fixType (see NMEAGPS.h) // the 2D/3D fixType (see NMEAGPS.h)
// At a minimum, use the fixQuality indicator in GPGGA (FIXME?) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?)

View File

@@ -16,9 +16,6 @@
#define GPS_EN_ACTIVE 1 #define GPS_EN_ACTIVE 1
#endif #endif
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
typedef enum { typedef enum {
GNSS_MODEL_ATGM336H, GNSS_MODEL_ATGM336H,
GNSS_MODEL_MTK, GNSS_MODEL_MTK,
@@ -34,8 +31,7 @@ typedef enum {
GNSS_MODEL_MTK_PA1616S, GNSS_MODEL_MTK_PA1616S,
GNSS_MODEL_AG3335, GNSS_MODEL_AG3335,
GNSS_MODEL_AG3352, GNSS_MODEL_AG3352,
GNSS_MODEL_LS20031, GNSS_MODEL_LS20031
GNSS_MODEL_CM121
} GnssModel_t; } GnssModel_t;
typedef enum { typedef enum {
@@ -154,8 +150,6 @@ class GPS : private concurrency::OSThread
TinyGPSPlus reader; TinyGPSPlus reader;
uint8_t fixQual = 0; // fix quality from GPGGA uint8_t fixQual = 0; // fix quality from GPGGA
uint32_t lastChecksumFailCount = 0; uint32_t lastChecksumFailCount = 0;
uint8_t currentStep = 0;
int32_t currentDelay = 2000;
#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS #ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS
// (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field // (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field
@@ -165,7 +159,7 @@ class GPS : private concurrency::OSThread
uint8_t fixType = 0; // fix type from GPGSA uint8_t fixType = 0; // fix type from GPGSA
#endif #endif
uint32_t fixHoldEnds = 0; uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0;
uint32_t rx_gpio = 0; uint32_t rx_gpio = 0;
uint32_t tx_gpio = 0; uint32_t tx_gpio = 0;
@@ -178,6 +172,8 @@ class GPS : private concurrency::OSThread
*/ */
bool hasValidLocation = false; // default to false, until we complete our first read bool hasValidLocation = false; // default to false, until we complete our first read
bool isInPowersave = false;
bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop() bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop()
bool hasGPS = false; // Do we have a GPS we are talking to bool hasGPS = false; // Do we have a GPS we are talking to
@@ -240,7 +236,7 @@ class GPS : private concurrency::OSThread
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed); GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap);
// Get GNSS model // Get GNSS model
GnssModel_t probe(int serialSpeed); GnssModel_t probe(int serialSpeed);

View File

@@ -9,9 +9,6 @@
static RTCQuality currentQuality = RTCQualityNone; static RTCQuality currentQuality = RTCQualityNone;
uint32_t lastSetFromPhoneNtpOrGps = 0; uint32_t lastSetFromPhoneNtpOrGps = 0;
static uint32_t lastTimeValidationWarning = 0;
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
RTCQuality getRTCQuality() RTCQuality getRTCQuality()
{ {
return currentQuality; return currentQuality;
@@ -26,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda
* Reads the current date and time from the RTC module and updates the system time. * Reads the current date and time from the RTC module and updates the system time.
* @return True if the RTC was successfully read and the system time was updated, false otherwise. * @return True if the RTC was successfully read and the system time was updated, false otherwise.
*/ */
RTCSetResult readFromRTC() void readFromRTC()
{ {
struct timeval tv; /* btw settimeofday() is helpful here too*/ struct timeval tv; /* btw settimeofday() is helpful here too*/
#ifdef RV3028_RTC #ifdef RV3028_RTC
@@ -47,25 +44,15 @@ RTCSetResult readFromRTC()
t.tm_sec = rtc.getSecond(); t.tm_sec = rtc.getSecond();
tv.tv_sec = gm_mktime(&t); tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0; tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
}
return RTCSetResultInvalidTime;
}
#endif
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, 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); t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) { if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice; currentQuality = RTCQualityDevice;
} }
return RTCSetResultSuccess;
} }
#elif defined(PCF8563_RTC) #elif defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) { if (rtc_found.address == PCF8563_RTC) {
@@ -88,55 +75,15 @@ RTCSetResult readFromRTC()
t.tm_sec = tc.second; t.tm_sec = tc.second;
tv.tv_sec = gm_mktime(&t); tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0; tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
}
#endif
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) { if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice; currentQuality = RTCQualityDevice;
} }
return RTCSetResultSuccess;
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
uint32_t now = millis();
ArtronShop_RX8130CE rtc(&Wire);
tm t;
if (rtc.getTime(&t)) {
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
}
#endif
if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
}
} }
#else #else
if (!gettimeofday(&tv, NULL)) { if (!gettimeofday(&tv, NULL)) {
@@ -145,10 +92,8 @@ RTCSetResult readFromRTC()
LOG_DEBUG("Read RTC time as %ld", printableEpoch); LOG_DEBUG("Read RTC time as %ld", printableEpoch);
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
return RTCSetResultSuccess;
} }
#endif #endif
return RTCSetResultNotSet;
} }
/** /**
@@ -156,7 +101,7 @@ RTCSetResult readFromRTC()
* *
* @param q The quality of the provided time. * @param q The quality of the provided time.
* @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to.
* @return RTCSetResult * @return True if the RTC was set, false otherwise.
* *
* If we haven't yet set our RTC this boot, set it from a GPS derived time * If we haven't yet set our RTC this boot, set it from a GPS derived time
*/ */
@@ -167,20 +112,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH #ifdef BUILD_EPOCH
if (tv->tv_sec < BUILD_EPOCH) { if (tv->tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
} else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
// Calculate max allowed time safely to avoid overflow in logging
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime; return RTCSetResultInvalidTime;
} }
#endif #endif
@@ -243,17 +175,6 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} }
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
ArtronShop_RX8130CE rtc(&Wire);
tm *t = gmtime(&tv->tv_sec);
if (rtc.setTime(*t)) {
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} else {
LOG_WARN("Failed to set time for RX8130CE");
}
}
#elif defined(ARCH_ESP32) #elif defined(ARCH_ESP32)
settimeofday(tv, NULL); settimeofday(tv, NULL);
#endif #endif
@@ -309,20 +230,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH #ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) { if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
} else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
// Calculate max allowed time safely to avoid overflow in logging
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime; return RTCSetResultInvalidTime;
} }
#endif #endif
@@ -382,40 +290,14 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
time_t gm_mktime(struct tm *tm) time_t gm_mktime(struct tm *tm)
{ {
#if !MESHTASTIC_EXCLUDE_TZ #if !MESHTASTIC_EXCLUDE_TZ
time_t result = 0; setenv("TZ", "GMT0", 1);
time_t res = mktime(tm);
// First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch. if (*config.device.tzdef) {
int year = 1900 + tm->tm_year; // tm_year is years since 1900 setenv("TZ", config.device.tzdef, 1);
int year_minus_one = year - 1; } else {
int days_before_this_year = 0; setenv("TZ", "UTC0", 1);
days_before_this_year += year_minus_one * 365;
// leap days: every 4 years, except 100s, but including 400s.
days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400;
// subtract from 1970-01-01 to get days since epoch
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
// Now, within this tm->year, compute the days *before* this tm->month starts.
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
// If this is a leap year, and we're past February, add a day:
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
days_this_year_before_this_month += 1;
} }
return res;
// And within this month:
int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31
// Now combine them all together, and convert days to seconds:
result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today);
result *= 86400L;
// Finally, add in the hours, minutes, and seconds of today:
result += tm->tm_hour * 3600;
result += tm->tm_min * 60;
result += tm->tm_sec;
return result;
#else #else
return mktime(tm); return mktime(tm);
#endif #endif

View File

@@ -4,10 +4,6 @@
#include "sys/time.h" #include "sys/time.h"
#include <Arduino.h> #include <Arduino.h>
#ifdef RX8130CE_RTC
#include <ArtronShop_RX8130CE.h>
#endif
enum RTCQuality { enum RTCQuality {
/// We haven't had our RTC set yet /// We haven't had our RTC set yet
@@ -52,13 +48,10 @@ uint32_t getTime(bool local = false);
/// Return time since 1970 in secs. If quality is RTCQualityNone return zero /// Return time since 1970 in secs. If quality is RTCQualityNone return zero
uint32_t getValidTime(RTCQuality minQuality, bool local = false); uint32_t getValidTime(RTCQuality minQuality, bool local = false);
RTCSetResult readFromRTC(); void readFromRTC();
time_t gm_mktime(struct tm *tm); time_t gm_mktime(struct tm *tm);
#define SEC_PER_DAY 86400 #define SEC_PER_DAY 86400
#define SEC_PER_HOUR 3600 #define SEC_PER_HOUR 3600
#define SEC_PER_MIN 60 #define SEC_PER_MIN 60
#ifdef BUILD_EPOCH
static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow
#endif

View File

@@ -67,28 +67,20 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
// FIXME - only draw bits have changed (use backbuf similar to the other displays) // FIXME - only draw bits have changed (use backbuf similar to the other displays)
const bool flipped = config.display.flip_screen; const bool flipped = config.display.flip_screen;
// HACK for L1 EInk
#if defined(SEEED_WIO_TRACKER_L1_EINK)
// For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes
for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) {
auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7));
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
}
}
#else
for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) { for (uint32_t x = 0; x < displayWidth; x++) {
// get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
auto b = buffer[x + (y / 8) * displayWidth]; auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7)); auto isset = b & (1 << (y & 7));
// Handle flip here, rather than with setRotation(),
// Avoids issues when display width is not a multiple of 8
if (flipped) if (flipped)
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
else else
adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE);
} }
} }
#endif
// Trigger the refresh in GxEPD2 // Trigger the refresh in GxEPD2
LOG_DEBUG("Update E-Paper"); LOG_DEBUG("Update E-Paper");
@@ -243,7 +235,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(1); adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) #elif defined(HELTEC_MESH_POCKET)
{ {
spi1 = &SPI1; spi1 = &SPI1;
spi1->begin(); spi1->begin();
@@ -257,7 +249,6 @@ bool EInkDisplay::connect()
// Init GxEPD2 // Init GxEPD2
adafruitDisplay->init(); adafruitDisplay->init();
adafruitDisplay->setRotation(3); adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)

View File

@@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay
SPIClass *hspi = NULL; SPIClass *hspi = NULL;
#endif #endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK) #if defined(HELTEC_MESH_POCKET)
SPIClass *spi1 = NULL; SPIClass *spi1 = NULL;
#endif #endif

View File

@@ -1,687 +0,0 @@
/*----------------------------------------------------------------------------/
Lovyan GFX - Graphics library for embedded devices.
Original Source:
https://github.com/lovyan03/LovyanGFX/
Licence:
[FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt)
Author:
[lovyan03](https://twitter.com/lovyan03)
Contributors:
[ciniml](https://github.com/ciniml)
[mongonta0716](https://github.com/mongonta0716)
[tobozo](https://github.com/tobozo)
Porting for SDL:
[imliubo](https://github.com/imliubo)
/----------------------------------------------------------------------------*/
#include "Panel_sdl.hpp"
#if defined(SDL_h_)
// #include "../common.hpp"
// #include "../../misc/common_function.hpp"
// #include "../../Bus.hpp"
#include <list>
#include <math.h>
#include <vector>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace lgfx
{
inline namespace v1
{
SDL_Keymod Panel_sdl::_keymod = KMOD_NONE;
static SDL_semaphore *_update_in_semaphore = nullptr;
static SDL_semaphore *_update_out_semaphore = nullptr;
volatile static uint32_t _in_step_exec = 0;
volatile static uint32_t _msec_step_exec = 512;
static bool _inited = false;
static bool _all_close = false;
volatile uint8_t Panel_sdl::_gpio_dummy_values[EMULATED_GPIO_MAX];
static inline void *heap_alloc_dma(size_t length)
{
return malloc(length);
} // aligned_alloc(16, length);
static inline void heap_free(void *buf)
{
free(buf);
}
static std::list<monitor_t *> _list_monitor;
static monitor_t *const getMonitorByWindowID(uint32_t windowID)
{
for (auto &m : _list_monitor) {
if (SDL_GetWindowID(m->window) == windowID) {
return m;
}
}
return nullptr;
}
//----------------------------------------------------------------------------
static std::vector<Panel_sdl::KeyCodeMapping_t> _key_code_map;
void Panel_sdl::addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio)
{
if (gpio > EMULATED_GPIO_MAX)
return;
KeyCodeMapping_t map;
map.keycode = keyCode;
map.gpio = gpio;
_key_code_map.push_back(map);
}
int Panel_sdl::getKeyCodeMapping(SDL_KeyCode keyCode)
{
for (const auto &i : _key_code_map) {
if (i.keycode == keyCode)
return i.gpio;
}
return -1;
}
void Panel_sdl::_event_proc(void)
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
if ((event.type == SDL_KEYDOWN) || (event.type == SDL_KEYUP)) {
auto mon = getMonitorByWindowID(event.button.windowID);
int gpio = -1;
/// Check key mapping
gpio = getKeyCodeMapping((SDL_KeyCode)event.key.keysym.sym);
if (gpio < 0) {
switch (event.key.keysym.sym) { /// M5StackのBtnABtnCのエミュレート;
// case SDLK_LEFT: gpio = 39; break;
// case SDLK_DOWN: gpio = 38; break;
// case SDLK_RIGHT: gpio = 37; break;
// case SDLK_UP: gpio = 36; break;
/// L/Rキーで画面回転
case SDLK_r:
case SDLK_l:
if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) {
if (mon != nullptr) {
mon->frame_rotation = (mon->frame_rotation += event.key.keysym.sym == SDLK_r ? 1 : -1);
int x, y, w, h;
SDL_GetWindowSize(mon->window, &w, &h);
SDL_GetWindowPosition(mon->window, &x, &y);
SDL_SetWindowSize(mon->window, h, w);
SDL_SetWindowPosition(mon->window, x + (w - h) / 2, y + (h - w) / 2);
mon->panel->sdl_invalidate();
}
}
break;
/// 16キーで画面拡大率変更
case SDLK_1:
case SDLK_2:
case SDLK_3:
case SDLK_4:
case SDLK_5:
case SDLK_6:
if (event.type == SDL_KEYDOWN && event.key.keysym.mod == _keymod) {
if (mon != nullptr) {
int size = 1 + (event.key.keysym.sym - SDLK_1);
_update_scaling(mon, size, size);
}
}
break;
default:
continue;
}
}
if (event.type == SDL_KEYDOWN) {
Panel_sdl::gpio_lo(gpio);
} else {
Panel_sdl::gpio_hi(gpio);
}
} else if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEMOTION) {
auto mon = getMonitorByWindowID(event.button.windowID);
if (mon != nullptr) {
{
int x, y, w, h;
SDL_GetWindowSize(mon->window, &w, &h);
SDL_GetMouseState(&x, &y);
float sf = sinf(mon->frame_angle * M_PI / 180);
float cf = cosf(mon->frame_angle * M_PI / 180);
x -= w / 2.0f;
y -= h / 2.0f;
float nx = y * sf + x * cf;
float ny = y * cf - x * sf;
if (mon->frame_rotation & 1) {
std::swap(w, h);
}
x = (nx * mon->frame_width / w) + (mon->frame_width >> 1);
y = (ny * mon->frame_height / h) + (mon->frame_height >> 1);
mon->touch_x = x - mon->frame_inner_x;
mon->touch_y = y - mon->frame_inner_y;
}
if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) {
mon->touched = true;
}
if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT) {
mon->touched = false;
}
}
} else if (event.type == SDL_WINDOWEVENT) {
auto monitor = getMonitorByWindowID(event.window.windowID);
if (monitor) {
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
int mw, mh;
SDL_GetRendererOutputSize(monitor->renderer, &mw, &mh);
if (monitor->frame_rotation & 1) {
std::swap(mw, mh);
}
monitor->scaling_x = (mw * 2 / monitor->frame_width) / 2.0f;
monitor->scaling_y = (mh * 2 / monitor->frame_height) / 2.0f;
monitor->panel->sdl_invalidate();
} else if (event.window.event == SDL_WINDOWEVENT_CLOSE) {
monitor->closing = true;
}
}
} else if (event.type == SDL_QUIT) {
for (auto &m : _list_monitor) {
m->closing = true;
}
}
}
}
/// デバッガでステップ実行されていることを検出するスレッド用関数。
static int detectDebugger(bool *running)
{
uint32_t prev_ms = SDL_GetTicks();
do {
SDL_Delay(1);
uint32_t ms = SDL_GetTicks();
/// 時間間隔が広すぎる場合はステップ実行中 (ブレークポイントで止まった)と判断する。
/// また、解除されたと判断した後も1023msecほど状態を維持する。
if (ms - prev_ms > 64) {
_in_step_exec = _msec_step_exec;
} else if (_in_step_exec) {
--_in_step_exec;
}
prev_ms = ms;
} while (*running);
return 0;
}
void Panel_sdl::_update_proc(void)
{
for (auto it = _list_monitor.begin(); it != _list_monitor.end();) {
if ((*it)->closing) {
if ((*it)->texture_frameimage) {
SDL_DestroyTexture((*it)->texture_frameimage);
}
SDL_DestroyTexture((*it)->texture);
SDL_DestroyRenderer((*it)->renderer);
SDL_DestroyWindow((*it)->window);
_list_monitor.erase(it++);
if (_list_monitor.empty()) {
_all_close = true;
return;
}
continue;
}
(*it)->panel->sdl_update();
++it;
}
}
int Panel_sdl::setup(void)
{
if (_inited)
return 1;
_inited = true;
/// Add default keycode mapping
/// M5StackのBtnABtnCのエミュレート;
addKeyCodeMapping(SDLK_LEFT, 39);
addKeyCodeMapping(SDLK_DOWN, 38);
addKeyCodeMapping(SDLK_RIGHT, 37);
addKeyCodeMapping(SDLK_UP, 36);
SDL_CreateThread((SDL_ThreadFunction)detectDebugger, "dbg", &_inited);
_update_in_semaphore = SDL_CreateSemaphore(0);
_update_out_semaphore = SDL_CreateSemaphore(0);
for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) {
gpio_hi(pin);
}
/*Initialize the SDL*/
SDL_Init(SDL_INIT_VIDEO);
SDL_StartTextInput();
// SDL_SetThreadPriority(SDL_ThreadPriority::SDL_THREAD_PRIORITY_HIGH);
return 0;
}
int Panel_sdl::loop(void)
{
if (!_inited)
return 1;
_event_proc();
SDL_SemWaitTimeout(_update_in_semaphore, 1);
_update_proc();
_event_proc();
if (SDL_SemValue(_update_out_semaphore) == 0) {
SDL_SemPost(_update_out_semaphore);
}
return _all_close;
}
int Panel_sdl::close(void)
{
if (!_inited)
return 1;
_inited = false;
SDL_StopTextInput();
SDL_DestroySemaphore(_update_in_semaphore);
SDL_DestroySemaphore(_update_out_semaphore);
SDL_Quit();
return 0;
}
int Panel_sdl::main(int (*fn)(bool *), uint32_t msec_step_exec)
{
_msec_step_exec = msec_step_exec;
/// SDLの準備
if (0 != Panel_sdl::setup()) {
return 1;
}
/// ユーザコード関数の動作・停止フラグ
bool running = true;
/// ユーザコード関数を起動する
auto thread = SDL_CreateThread((SDL_ThreadFunction)fn, "fn", &running);
/// 全部のウィンドウが閉じられるまでSDLのイベント・描画処理を継続
while (0 == Panel_sdl::loop()) {
};
/// ユーザコード関数を終了する
running = false;
SDL_WaitThread(thread, nullptr);
/// SDLを終了する
return Panel_sdl::close();
}
void Panel_sdl::setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y)
{
monitor.scaling_x = scaling_x;
monitor.scaling_y = scaling_y;
}
void Panel_sdl::setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y)
{
monitor.frame_image = frame_image;
monitor.frame_width = frame_width;
monitor.frame_height = frame_height;
monitor.frame_inner_x = inner_x;
monitor.frame_inner_y = inner_y;
}
void Panel_sdl::setFrameRotation(uint_fast16_t frame_rotation)
{
monitor.frame_rotation = frame_rotation;
monitor.frame_angle = (monitor.frame_rotation) * 90;
}
Panel_sdl::~Panel_sdl(void)
{
_list_monitor.remove(&monitor);
SDL_DestroyMutex(_sdl_mutex);
}
Panel_sdl::Panel_sdl(void) : Panel_FrameBufferBase()
{
_sdl_mutex = SDL_CreateMutex();
_auto_display = true;
monitor.panel = this;
}
bool Panel_sdl::init(bool use_reset)
{
initFrameBuffer(_cfg.panel_width * 4, _cfg.panel_height);
bool res = Panel_FrameBufferBase::init(use_reset);
_list_monitor.push_back(&monitor);
return res;
}
color_depth_t Panel_sdl::setColorDepth(color_depth_t depth)
{
auto bits = depth & color_depth_t::bit_mask;
if (bits >= 16) {
depth = (bits > 16) ? rgb888_3Byte : rgb565_2Byte;
} else {
depth = (depth == color_depth_t::grayscale_8bit) ? grayscale_8bit : rgb332_1Byte;
}
_write_depth = depth;
_read_depth = depth;
return depth;
}
Panel_sdl::lock_t::lock_t(Panel_sdl *parent) : _parent{parent}
{
SDL_LockMutex(parent->_sdl_mutex);
};
Panel_sdl::lock_t::~lock_t(void)
{
++_parent->_modified_counter;
SDL_UnlockMutex(_parent->_sdl_mutex);
if (SDL_SemValue(_update_in_semaphore) < 2) {
SDL_SemPost(_update_in_semaphore);
if (!_in_step_exec) {
SDL_SemWaitTimeout(_update_out_semaphore, 1);
}
}
};
void Panel_sdl::drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor)
{
lock_t lock(this);
Panel_FrameBufferBase::drawPixelPreclipped(x, y, rawcolor);
}
void Panel_sdl::writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor)
{
lock_t lock(this);
Panel_FrameBufferBase::writeFillRectPreclipped(x, y, w, h, rawcolor);
}
void Panel_sdl::writeBlock(uint32_t rawcolor, uint32_t length)
{
// lock_t lock(this);
Panel_FrameBufferBase::writeBlock(rawcolor, length);
}
void Panel_sdl::writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma)
{
lock_t lock(this);
Panel_FrameBufferBase::writeImage(x, y, w, h, param, use_dma);
}
void Panel_sdl::writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param)
{
lock_t lock(this);
Panel_FrameBufferBase::writeImageARGB(x, y, w, h, param);
}
void Panel_sdl::writePixels(pixelcopy_t *param, uint32_t len, bool use_dma)
{
lock_t lock(this);
Panel_FrameBufferBase::writePixels(param, len, use_dma);
}
void Panel_sdl::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h)
{
(void)x;
(void)y;
(void)w;
(void)h;
if (_in_step_exec) {
if (_display_counter != _modified_counter) {
do {
SDL_SemPost(_update_in_semaphore);
SDL_SemWaitTimeout(_update_out_semaphore, 1);
} while (_display_counter != _modified_counter);
SDL_Delay(1);
}
}
}
uint_fast8_t Panel_sdl::getTouchRaw(touch_point_t *tp, uint_fast8_t count)
{
(void)count;
tp->x = monitor.touch_x;
tp->y = monitor.touch_y;
tp->size = monitor.touched ? 1 : 0;
tp->id = 0;
return monitor.touched;
}
void Panel_sdl::setWindowTitle(const char *title)
{
_window_title = title;
if (monitor.window) {
SDL_SetWindowTitle(monitor.window, _window_title);
}
}
void Panel_sdl::_update_scaling(monitor_t *mon, float sx, float sy)
{
mon->scaling_x = sx;
mon->scaling_y = sy;
int nw = mon->frame_width;
int nh = mon->frame_height;
if (mon->frame_rotation & 1) {
std::swap(nw, nh);
}
int x, y, w, h;
int rw, rh;
SDL_GetRendererOutputSize(mon->renderer, &rw, &rh);
SDL_GetWindowSize(mon->window, &w, &h);
nw = nw * sx * w / rw;
nh = nh * sy * h / rh;
SDL_GetWindowPosition(mon->window, &x, &y);
SDL_SetWindowSize(mon->window, nw, nh);
SDL_SetWindowPosition(mon->window, x + (w - nw) / 2, y + (h - nh) / 2);
mon->panel->sdl_invalidate();
}
void Panel_sdl::sdl_create(monitor_t *m)
{
int flag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
#if SDL_FULLSCREEN
flag |= SDL_WINDOW_FULLSCREEN;
#endif
if (m->frame_width < _cfg.panel_width) {
m->frame_width = _cfg.panel_width;
}
if (m->frame_height < _cfg.panel_height) {
m->frame_height = _cfg.panel_height;
}
int window_width = m->frame_width * m->scaling_x;
int window_height = m->frame_height * m->scaling_y;
int scaling_x = m->scaling_x;
int scaling_y = m->scaling_y;
if (m->frame_rotation & 1) {
std::swap(window_width, window_height);
std::swap(scaling_x, scaling_y);
}
{
m->window = SDL_CreateWindow(_window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_width, window_height,
flag); /*last param. SDL_WINDOW_BORDERLESS to hide borders*/
}
m->renderer = SDL_CreateRenderer(m->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
m->texture =
SDL_CreateTexture(m->renderer, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, _cfg.panel_width, _cfg.panel_height);
SDL_SetTextureBlendMode(m->texture, SDL_BLENDMODE_NONE);
if (m->frame_image) {
// 枠画像用のサーフェイスを作成
auto sf = SDL_CreateRGBSurfaceFrom((void *)m->frame_image, m->frame_width, m->frame_height, 32, m->frame_width * 4,
0xFF000000, 0xFF0000, 0xFF00, 0xFF);
if (sf != nullptr) {
// 枠画像からテクスチャを作成
m->texture_frameimage = SDL_CreateTextureFromSurface(m->renderer, sf);
SDL_FreeSurface(sf);
}
}
SDL_SetTextureBlendMode(m->texture_frameimage, SDL_BLENDMODE_BLEND);
_update_scaling(m, scaling_x, scaling_y);
}
void Panel_sdl::sdl_update(void)
{
if (monitor.renderer == nullptr) {
sdl_create(&monitor);
}
bool step_exec = _in_step_exec;
if (_texupdate_counter != _modified_counter) {
pixelcopy_t pc(nullptr, color_depth_t::rgb888_3Byte, _write_depth, false);
if (_write_depth == rgb565_2Byte) {
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, swap565_t>;
} else if (_write_depth == rgb888_3Byte) {
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, bgr888_t>;
} else if (_write_depth == rgb332_1Byte) {
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, rgb332_t>;
} else if (_write_depth == grayscale_8bit) {
pc.fp_copy = pixelcopy_t::copy_rgb_fast<bgr888_t, grayscale_t>;
}
if (0 == SDL_LockMutex(_sdl_mutex)) {
_texupdate_counter = _modified_counter;
for (int y = 0; y < _cfg.panel_height; ++y) {
pc.src_x32 = 0;
pc.src_data = _lines_buffer[y];
pc.fp_copy(&_texturebuf[y * _cfg.panel_width], 0, _cfg.panel_width, &pc);
}
SDL_UnlockMutex(_sdl_mutex);
SDL_UpdateTexture(monitor.texture, nullptr, _texturebuf, _cfg.panel_width * sizeof(rgb888_t));
}
}
int angle = monitor.frame_angle;
int target = (monitor.frame_rotation) * 90;
angle = (((target * 4) + (angle * 4) + (angle < target ? 8 : 0)) >> 3);
if (monitor.frame_angle != angle) { // 表示する向きを変える
monitor.frame_angle = angle;
sdl_invalidate();
} else if (monitor.frame_rotation & ~3u) {
monitor.frame_rotation &= 3;
monitor.frame_angle = (monitor.frame_rotation) * 90;
sdl_invalidate();
}
if (_invalidated || (_display_counter != _texupdate_counter)) {
SDL_RendererInfo info;
if (0 == SDL_GetRendererInfo(monitor.renderer, &info)) {
// ステップ実行中はVSYNCを待機しない
if (((bool)(info.flags & SDL_RENDERER_PRESENTVSYNC)) == step_exec) {
SDL_RenderSetVSync(monitor.renderer, !step_exec);
}
}
{
int red = 0;
int green = 0;
int blue = 0;
#if defined(M5GFX_BACK_COLOR)
red = ((M5GFX_BACK_COLOR) >> 16) & 0xFF;
green = ((M5GFX_BACK_COLOR) >> 8) & 0xFF;
blue = ((M5GFX_BACK_COLOR)) & 0xFF;
#endif
SDL_SetRenderDrawColor(monitor.renderer, red, green, blue, 0xFF);
}
SDL_RenderClear(monitor.renderer);
if (_invalidated) {
_invalidated = false;
int mw, mh;
SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh);
}
render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height, angle);
render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle);
SDL_RenderPresent(monitor.renderer);
_display_counter = _texupdate_counter;
if (_invalidated) {
_invalidated = false;
SDL_SetRenderDrawColor(monitor.renderer, 0, 0, 0, 0xFF);
SDL_RenderClear(monitor.renderer);
render_texture(monitor.texture, monitor.frame_inner_x, monitor.frame_inner_y, _cfg.panel_width, _cfg.panel_height,
angle);
render_texture(monitor.texture_frameimage, 0, 0, monitor.frame_width, monitor.frame_height, angle);
SDL_RenderPresent(monitor.renderer);
}
}
}
void Panel_sdl::render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle)
{
SDL_Point pivot;
pivot.x = (monitor.frame_width / 2.0f - tx) * (float)monitor.scaling_x;
pivot.y = (monitor.frame_height / 2.0f - ty) * (float)monitor.scaling_y;
SDL_Rect dstrect;
dstrect.w = tw * monitor.scaling_x;
dstrect.h = th * monitor.scaling_y;
int mw, mh;
SDL_GetRendererOutputSize(monitor.renderer, &mw, &mh);
dstrect.x = mw / 2.0f - pivot.x;
dstrect.y = mh / 2.0f - pivot.y;
SDL_RenderCopyEx(monitor.renderer, texture, nullptr, &dstrect, angle, &pivot, SDL_RendererFlip::SDL_FLIP_NONE);
}
bool Panel_sdl::initFrameBuffer(size_t width, size_t height)
{
uint8_t **lineArray = (uint8_t **)heap_alloc_dma(height * sizeof(uint8_t *));
if (nullptr == lineArray) {
return false;
}
_texturebuf = (rgb888_t *)heap_alloc_dma(width * height * sizeof(rgb888_t));
/// 8byte alignment;
width = (width + 7) & ~7u;
_lines_buffer = lineArray;
memset(lineArray, 0, height * sizeof(uint8_t *));
uint8_t *framebuffer = (uint8_t *)heap_alloc_dma(width * height + 16);
auto fb = framebuffer;
{
for (size_t y = 0; y < height; ++y) {
lineArray[y] = fb;
fb += width;
}
}
return true;
}
void Panel_sdl::deinitFrameBuffer(void)
{
auto lines = _lines_buffer;
_lines_buffer = nullptr;
if (lines != nullptr) {
heap_free(lines[0]);
heap_free(lines);
}
if (_texturebuf) {
heap_free(_texturebuf);
_texturebuf = nullptr;
}
}
//----------------------------------------------------------------------------
} // namespace v1
} // namespace lgfx
#endif

View File

@@ -1,166 +0,0 @@
/*----------------------------------------------------------------------------/
Lovyan GFX - Graphics library for embedded devices.
Original Source:
https://github.com/lovyan03/LovyanGFX/
Licence:
[FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt)
Author:
[lovyan03](https://twitter.com/lovyan03)
Contributors:
[ciniml](https://github.com/ciniml)
[mongonta0716](https://github.com/mongonta0716)
[tobozo](https://github.com/tobozo)
Porting for SDL:
[imliubo](https://github.com/imliubo)
/----------------------------------------------------------------------------*/
#pragma once
#define SDL_MAIN_HANDLED
// cppcheck-suppress preprocessorErrorDirective
#if __has_include(<SDL2/SDL.h>)
#include <SDL2/SDL.h>
#include <SDL2/SDL_main.h>
#elif __has_include(<SDL.h>)
#include <SDL.h>
#include <SDL_main.h>
#endif
#if defined(SDL_h_)
#include "lgfx/v1/Touch.hpp"
#include "lgfx/v1/misc/range.hpp"
#include "lgfx/v1/panel/Panel_FrameBufferBase.hpp"
#include <cstdint>
namespace lgfx
{
inline namespace v1
{
struct Panel_sdl;
struct monitor_t {
SDL_Window *window = nullptr;
SDL_Renderer *renderer = nullptr;
SDL_Texture *texture = nullptr;
SDL_Texture *texture_frameimage = nullptr;
Panel_sdl *panel = nullptr;
// 外枠
const void *frame_image = 0;
uint_fast16_t frame_width = 0;
uint_fast16_t frame_height = 0;
uint_fast16_t frame_inner_x = 0;
uint_fast16_t frame_inner_y = 0;
int_fast16_t frame_rotation = 0;
int_fast16_t frame_angle = 0;
float scaling_x = 1;
float scaling_y = 1;
int_fast16_t touch_x, touch_y;
bool touched = false;
bool closing = false;
};
//----------------------------------------------------------------------------
struct Touch_sdl : public ITouch {
bool init(void) override { return true; }
void wakeup(void) override {}
void sleep(void) override {}
bool isEnable(void) override { return true; };
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; }
};
//----------------------------------------------------------------------------
struct Panel_sdl : public Panel_FrameBufferBase {
static constexpr size_t EMULATED_GPIO_MAX = 128;
static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX];
public:
Panel_sdl(void);
virtual ~Panel_sdl(void);
bool init(bool use_reset) override;
color_depth_t setColorDepth(color_depth_t depth) override;
void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override;
// void setInvert(bool invert) override {}
void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override;
void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override;
void writeBlock(uint32_t rawcolor, uint32_t length) override;
void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param,
bool use_dma) override;
void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override;
void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override;
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override;
void setWindowTitle(const char *title);
void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y);
void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y);
void setFrameRotation(uint_fast16_t frame_rotaion);
void setBrightness(uint8_t brightness) override{};
static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; }
static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; }
static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; }
static int setup(void);
static int loop(void);
static int close(void);
static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512);
static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; }
struct KeyCodeMapping_t {
SDL_KeyCode keycode = SDLK_UNKNOWN;
uint8_t gpio = 0;
};
static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio);
static int getKeyCodeMapping(SDL_KeyCode keyCode);
protected:
const char *_window_title = "LGFX Simulator";
SDL_mutex *_sdl_mutex = nullptr;
void sdl_create(monitor_t *m);
void sdl_update(void);
touch_point_t _touch_point;
monitor_t monitor;
rgb888_t *_texturebuf = nullptr;
uint_fast16_t _modified_counter;
uint_fast16_t _texupdate_counter;
uint_fast16_t _display_counter;
bool _invalidated;
static void _event_proc(void);
static void _update_proc(void);
static void _update_scaling(monitor_t *m, float sx, float sy);
void sdl_invalidate(void) { _invalidated = true; }
void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle);
bool initFrameBuffer(size_t width, size_t height);
void deinitFrameBuffer(void);
static SDL_Keymod _keymod;
struct lock_t {
lock_t(Panel_sdl *parent);
~lock_t();
protected:
Panel_sdl *_parent;
};
};
//----------------------------------------------------------------------------
} // namespace v1
} // namespace lgfx
#endif

View File

@@ -25,7 +25,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "PowerMon.h" #include "PowerMon.h"
#include "Throttle.h" #include "Throttle.h"
#include "configuration.h" #include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN #if HAS_SCREEN
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
@@ -59,6 +58,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/Channels.h" #include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "meshUtils.h"
#include "modules/ExternalNotificationModule.h" #include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h" #include "modules/TextMessageModule.h"
#include "modules/WaypointModule.h" #include "modules/WaypointModule.h"
@@ -83,11 +83,6 @@ extern uint16_t TFT_MESH;
#include "platform/portduino/PortduinoGlue.h" #include "platform/portduino/PortduinoGlue.h"
#endif #endif
#if defined(T_LORA_PAGER)
// KB backlight control
#include "input/cardKbI2cImpl.h"
#endif
using namespace meshtastic; /** @todo remove */ using namespace meshtastic; /** @todo remove */
namespace graphics namespace graphics
@@ -100,7 +95,7 @@ namespace graphics
#define NUM_EXTRA_FRAMES 3 // text message and debug frame #define NUM_EXTRA_FRAMES 3 // text message and debug frame
// if defined a pixel will blink to show redraws // if defined a pixel will blink to show redraws
// #define SHOW_REDRAWS // #define SHOW_REDRAWS
#define ASCII_BELL '\x07'
// A text message frame + debug frame + all the node infos // A text message frame + debug frame + all the node infos
FrameCallback *normalFrames; FrameCallback *normalFrames;
static uint32_t targetFramerate = IDLE_FRAMERATE; static uint32_t targetFramerate = IDLE_FRAMERATE;
@@ -221,44 +216,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
ui->update(); ui->update();
} }
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback)
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
if (NotificationRenderer::virtualKeyboard) {
delete NotificationRenderer::virtualKeyboard;
NotificationRenderer::virtualKeyboard = nullptr;
}
NotificationRenderer::textInputCallback = nullptr;
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
if (header) {
NotificationRenderer::virtualKeyboard->setHeader(header);
}
if (initialText) {
NotificationRenderer::virtualKeyboard->setInputText(initialText);
}
// Set up callback with safer cleanup mechanism
NotificationRenderer::textInputCallback = textCallback;
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
NotificationRenderer::alertBannerMessage[255] = '\0';
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
// Set the overlay using the same pattern as other notification types
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
uint8_t module_frame; uint8_t module_frame;
@@ -360,16 +317,8 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
#elif defined(USE_SSD1306) #elif defined(USE_SSD1306)
dispdev = new SSD1306Wire(address.address, -1, -1, geometry, dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_SPISSD1306)
dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
if (!dispdev->init()) {
LOG_DEBUG("Error: SSD1306 not detected!");
} else {
static_cast<SSD1306Spi *>(dispdev)->setHorizontalOffset(32);
LOG_INFO("SSD1306 init success");
}
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
dispdev = new TFTDisplay(address.address, -1, -1, geometry, dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
@@ -383,7 +332,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif ARCH_PORTDUINO #elif ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (portduino_config.displayPanel != no_screen) { if (settingsMap[displayPanel] != no_screen) {
LOG_DEBUG("Make TFTDisplay!"); LOG_DEBUG("Make TFTDisplay!");
dispdev = new TFTDisplay(address.address, -1, -1, geometry, dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -453,7 +402,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#endif #endif
dispdev->displayOn(); dispdev->displayOn();
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2) #ifdef HELTEC_TRACKER_V1_X
ui->init(); ui->init();
#endif #endif
#ifdef USE_ST7789 #ifdef USE_ST7789
@@ -558,7 +507,7 @@ void Screen::setup()
// === Apply loaded brightness === // === Apply loaded brightness ===
#if defined(ST7789_CS) #if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
dispdev->setBrightness(brightness); dispdev->setBrightness(brightness);
#endif #endif
LOG_INFO("Applied screen brightness: %d", brightness); LOG_INFO("Applied screen brightness: %d", brightness);
@@ -601,11 +550,11 @@ void Screen::setup()
#else #else
if (!config.display.flip_screen) { if (!config.display.flip_screen) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically(); static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789) #elif defined(USE_ST7789)
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically(); static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
#elif !defined(M5STACK_UNITC6L) #else
dispdev->flipScreenVertically(); dispdev->flipScreenVertically();
#endif #endif
} }
@@ -631,7 +580,7 @@ void Screen::setup()
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (portduino_config.touchscreenModule) { if (settingsMap[touchscreenModule]) {
touchScreenImpl1 = touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch); new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1->init(); touchScreenImpl1->init();
@@ -660,19 +609,6 @@ void Screen::setup()
MeshModule::observeUIEvents(&uiFrameEventObserver); MeshModule::observeUIEvents(&uiFrameEventObserver);
} }
void Screen::setOn(bool on, FrameCallback einkScreensaver)
{
#if defined(T_LORA_PAGER)
if (cardKbI2cImpl)
cardKbI2cImpl->toggleBacklight(on);
#endif
if (!on)
// We handle off commands immediately, because they might be called because the CPU is shutting down
handleSetOn(false, einkScreensaver);
else
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
}
void Screen::forceDisplay(bool forceUiUpdate) void Screen::forceDisplay(bool forceUiUpdate)
{ {
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup. // Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
@@ -756,11 +692,7 @@ int32_t Screen::runOnce()
#ifndef DISABLE_WELCOME_UNSET #ifndef DISABLE_WELCOME_UNSET
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
#if defined(M5STACK_UNITC6L)
menuHandler::LoraRegionPicker();
#else
menuHandler::OnboardMessage(); menuHandler::OnboardMessage();
#endif
} }
#endif #endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -781,19 +713,13 @@ int32_t Screen::runOnce()
handleSetOn(false); handleSetOn(false);
break; break;
case Cmd::ON_PRESS: case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { handleOnPress();
handleOnPress();
}
break; break;
case Cmd::SHOW_PREV_FRAME: case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { handleShowPrevFrame();
handleShowPrevFrame();
}
break; break;
case Cmd::SHOW_NEXT_FRAME: case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { handleShowNextFrame();
handleShowNextFrame();
}
break; break;
case Cmd::START_ALERT_FRAME: { case Cmd::START_ALERT_FRAME: {
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
@@ -815,9 +741,7 @@ int32_t Screen::runOnce()
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
case Cmd::STOP_BOOT_SCREEN: case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { setFrames();
setFrames();
}
break; break;
case Cmd::NOOP: case Cmd::NOOP:
break; break;
@@ -853,7 +777,6 @@ int32_t Screen::runOnce()
if (showingNormalScreen) { if (showingNormalScreen) {
// standard screen loop handling here // standard screen loop handling here
if (config.display.auto_screen_carousel_secs > 0 && if (config.display.auto_screen_carousel_secs > 0 &&
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
@@ -944,11 +867,6 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
// Called when a frame should be added / removed, or custom frames should be cleared // Called when a frame should be added / removed, or custom frames should be cleared
void Screen::setFrames(FrameFocus focus) void Screen::setFrames(FrameFocus focus)
{ {
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t originalPosition = ui->getUiState()->currentFrame;
uint8_t previousFrameCount = framesetInfo.frameCount; uint8_t previousFrameCount = framesetInfo.frameCount;
FramesetInfo fsi; // Location of specific frames, for applying focus parameter FramesetInfo fsi; // Location of specific frames, for applying focus parameter
@@ -971,95 +889,71 @@ void Screen::setFrames(FrameFocus focus)
} }
#if defined(DISPLAY_CLOCK_FRAME) #if defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) { fsi.positions.clock = numframes;
fsi.positions.clock = numframes; normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
#if defined(M5STACK_UNITC6L) : graphics::ClockRenderer::drawDigitalClockFrame;
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; indicatorIcons.push_back(digital_icon_clock);
#else
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
#endif
indicatorIcons.push_back(digital_icon_clock);
}
#endif #endif
// Declare this early so its available in FOCUS_PRESERVE block // Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) { fsi.positions.home = numframes;
fsi.positions.home = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; indicatorIcons.push_back(icon_home);
indicatorIcons.push_back(icon_home);
}
fsi.positions.textMessage = numframes; fsi.positions.textMessage = numframes;
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
indicatorIcons.push_back(icon_mail); indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK #ifndef USE_EINK
if (!hiddenFrames.nodelist) { fsi.positions.nodelist = numframes;
fsi.positions.nodelist = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; indicatorIcons.push_back(icon_nodes);
indicatorIcons.push_back(icon_nodes);
}
#endif #endif
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
#ifdef USE_EINK #ifdef USE_EINK
if (!hiddenFrames.nodelist_lastheard) { fsi.positions.nodelist_lastheard = numframes;
fsi.positions.nodelist_lastheard = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; indicatorIcons.push_back(icon_nodes);
indicatorIcons.push_back(icon_nodes);
} fsi.positions.nodelist_hopsignal = numframes;
if (!hiddenFrames.nodelist_hopsignal) { normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
fsi.positions.nodelist_hopsignal = numframes; indicatorIcons.push_back(icon_signal);
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal); fsi.positions.nodelist_distance = numframes;
} normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
if (!hiddenFrames.nodelist_distance) { indicatorIcons.push_back(icon_distance);
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
}
#endif #endif
#if HAS_GPS #if HAS_GPS
if (!hiddenFrames.nodelist_bearings) { fsi.positions.nodelist_bearings = numframes;
fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list);
indicatorIcons.push_back(icon_list);
} fsi.positions.gps = numframes;
if (!hiddenFrames.gps) { normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
fsi.positions.gps = numframes; indicatorIcons.push_back(icon_compass);
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
}
#endif #endif
if (RadioLibInterface::instance && !hiddenFrames.lora) { if (RadioLibInterface::instance) {
fsi.positions.lora = numframes; fsi.positions.lora = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
indicatorIcons.push_back(icon_radio); indicatorIcons.push_back(icon_radio);
} }
if (!hiddenFrames.system) { if (!dismissedFrames.memory) {
fsi.positions.system = numframes; fsi.positions.memory = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
indicatorIcons.push_back(icon_system); indicatorIcons.push_back(icon_memory);
} }
#if !defined(DISPLAY_CLOCK_FRAME) #if !defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) { fsi.positions.clock = numframes;
fsi.positions.clock = numframes; normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame : graphics::ClockRenderer::drawDigitalClockFrame;
: graphics::ClockRenderer::drawDigitalClockFrame; indicatorIcons.push_back(digital_icon_clock);
indicatorIcons.push_back(digital_icon_clock);
}
#endif #endif
if (!hiddenFrames.chirpy) {
fsi.positions.chirpy = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
indicatorIcons.push_back(chirpy_small);
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO) #if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!hiddenFrames.wifi && isWifiAvailable()) { if (!dismissedFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = numframes; fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi); indicatorIcons.push_back(icon_wifi);
@@ -1101,29 +995,27 @@ void Screen::setFrames(FrameFocus focus)
if (numMeshNodes > 0) if (numMeshNodes > 0)
numMeshNodes--; numMeshNodes--;
if (!hiddenFrames.show_favorites) { // Temporary array to hold favorite node frames
// Temporary array to hold favorite node frames std::vector<FrameCallback> favoriteFrames;
std::vector<FrameCallback> favoriteFrames;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
}
} }
}
// Insert favorite frames *after* collecting them all // Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) { if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes; fsi.positions.firstFavorite = numframes;
for (const auto &f : favoriteFrames) { for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f; normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node); indicatorIcons.push_back(icon_node);
}
fsi.positions.lastFavorite = numframes - 1;
} else {
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
} }
fsi.positions.lastFavorite = numframes - 1;
} else {
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
} }
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
@@ -1162,7 +1054,7 @@ void Screen::setFrames(FrameFocus focus)
ui->switchToFrame(fsi.positions.clock); ui->switchToFrame(fsi.positions.clock);
break; break;
case FOCUS_SYSTEM: case FOCUS_SYSTEM:
ui->switchToFrame(fsi.positions.system); ui->switchToFrame(fsi.positions.memory);
break; break;
case FOCUS_PRESERVE: case FOCUS_PRESERVE:
@@ -1190,101 +1082,30 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
setFastFramerate(); setFastFramerate();
} }
void Screen::toggleFrameVisibility(const std::string &frameName)
{
#ifndef USE_EINK
if (frameName == "nodelist") {
hiddenFrames.nodelist = !hiddenFrames.nodelist;
}
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard") {
hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard;
}
if (frameName == "nodelist_hopsignal") {
hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal;
}
if (frameName == "nodelist_distance") {
hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance;
}
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings") {
hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings;
}
if (frameName == "gps") {
hiddenFrames.gps = !hiddenFrames.gps;
}
#endif
if (frameName == "lora") {
hiddenFrames.lora = !hiddenFrames.lora;
}
if (frameName == "clock") {
hiddenFrames.clock = !hiddenFrames.clock;
}
if (frameName == "show_favorites") {
hiddenFrames.show_favorites = !hiddenFrames.show_favorites;
}
if (frameName == "chirpy") {
hiddenFrames.chirpy = !hiddenFrames.chirpy;
}
}
bool Screen::isFrameHidden(const std::string &frameName) const
{
#ifndef USE_EINK
if (frameName == "nodelist")
return hiddenFrames.nodelist;
#endif
#ifdef USE_EINK
if (frameName == "nodelist_lastheard")
return hiddenFrames.nodelist_lastheard;
if (frameName == "nodelist_hopsignal")
return hiddenFrames.nodelist_hopsignal;
if (frameName == "nodelist_distance")
return hiddenFrames.nodelist_distance;
#endif
#if HAS_GPS
if (frameName == "nodelist_bearings")
return hiddenFrames.nodelist_bearings;
if (frameName == "gps")
return hiddenFrames.gps;
#endif
if (frameName == "lora")
return hiddenFrames.lora;
if (frameName == "clock")
return hiddenFrames.clock;
if (frameName == "show_favorites")
return hiddenFrames.show_favorites;
if (frameName == "chirpy")
return hiddenFrames.chirpy;
return false;
}
// Dismisses the currently displayed screen frame, if possible // Dismisses the currently displayed screen frame, if possible
// Relevant for text message, waypoint, others in future? // Relevant for text message, waypoint, others in future?
// Triggered with a CardKB keycombo // Triggered with a CardKB keycombo
void Screen::hideCurrentFrame() void Screen::dismissCurrentFrame()
{ {
uint8_t currentFrame = ui->getUiState()->currentFrame; uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false; bool dismissed = false;
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
LOG_INFO("Hide Text Message"); LOG_INFO("Dismiss Text Message");
devicestate.has_rx_text_message = false; devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
LOG_DEBUG("Hide Waypoint"); LOG_DEBUG("Dismiss Waypoint");
devicestate.has_rx_waypoint = false; devicestate.has_rx_waypoint = false;
hiddenFrames.waypoint = true; dismissedFrames.waypoint = true;
dismissed = true; dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) { } else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Hide WiFi Screen"); LOG_DEBUG("Dismiss WiFi Screen");
hiddenFrames.wifi = true; dismissedFrames.wifi = true;
dismissed = true; dismissed = true;
} else if (currentFrame == framesetInfo.positions.lora) { } else if (currentFrame == framesetInfo.positions.memory) {
LOG_INFO("Hide LoRa"); LOG_INFO("Dismiss Memory");
hiddenFrames.lora = true; dismissedFrames.memory = true;
dismissed = true; dismissed = true;
} }
@@ -1317,8 +1138,7 @@ void Screen::blink()
delay(50); delay(50);
count = count - 1; count = count - 1;
} }
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay.
// OLEDDisplay.
dispdev->setBrightness(brightness); dispdev->setBrightness(brightness);
} }
@@ -1406,10 +1226,6 @@ void Screen::handleShowNextFrame()
void Screen::setFastFramerate() void Screen::setFastFramerate()
{ {
#if defined(M5STACK_UNITC6L)
dispdev->clear();
dispdev->display();
#endif
// We are about to start a transition so speed up fps // We are about to start a transition so speed up fps
targetFramerate = SCREEN_TRANSITION_FRAMERATE; targetFramerate = SCREEN_TRANSITION_FRAMERATE;
@@ -1428,9 +1244,6 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg)
} }
nodeDB->updateGUI = false; nodeDB->updateGUI = false;
break; break;
case STATUS_TYPE_POWER:
forceDisplay(true);
break;
} }
return 0; return 0;
@@ -1444,7 +1257,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Outgoing message (likely sent from phone) // Outgoing message (likely sent from phone)
devicestate.has_rx_text_message = false; devicestate.has_rx_text_message = false;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
hiddenFrames.textMessage = true; dismissedFrames.textMessage = true;
hasUnreadMessage = false; // Clear unread state when user replies hasUnreadMessage = false; // Clear unread state when user replies
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
@@ -1461,61 +1274,36 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
} }
// === Prepare banner content === // === Prepare banner content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes); const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
char banner[256]; char banner[256];
// Check for bell character in message to determine alert type
bool isAlert = false; bool isAlert = false;
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra || if (msgRaw[i] == '\x07') {
moduleConfig.external_notification.alert_bell_buzzer) isAlert = true;
// Check for bell character to determine if this message is an alert break;
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
if (msgRaw[i] == ASCII_BELL) {
isAlert = true;
break;
}
} }
}
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
// 'mute' preferences set to any specific node or channel.
if (isAlert) { if (isAlert) {
if (longName && longName[0]) { if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
} else { } else {
strcpy(banner, "Alert Received"); strcpy(banner, "Alert Received");
} }
screen->showSimpleBanner(banner, 3000); } else {
} else if (!channel.settings.has_module_settings || !channel.settings.module_settings.is_muted) {
if (longName && longName[0]) { if (longName && longName[0]) {
#if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message");
#else
snprintf(banner, sizeof(banner), "New Message from\n%s", longName); snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
#endif
} else { } else {
strcpy(banner, "New Message"); strcpy(banner, "New Message");
} }
#if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep();
}
#else
screen->showSimpleBanner(banner, 3000);
#endif
} }
screen->showSimpleBanner(banner, 3000);
} }
} }
@@ -1525,11 +1313,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Triggered by MeshModules // Triggered by MeshModules
int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleUIFrameEvent(const UIFrameEvent *event)
{ {
// Block UI frame events when virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return 0;
}
if (showingNormalScreen) { if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call // Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
@@ -1552,16 +1335,6 @@ int Screen::handleInputEvent(const InputEvent *event)
if (!screenOn) if (!screenOn)
return 0; return 0;
// Handle text input notifications specially - pass input to virtual keyboard
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
NotificationRenderer::inEvent = *event;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
return 0;
}
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
@@ -1599,7 +1372,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (event->inputEvent == INPUT_BROKER_SELECT) { } else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu(); menuHandler::homeBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
menuHandler::systemBaseMenu(); menuHandler::systemBaseMenu();
#if HAS_GPS #if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
@@ -1608,16 +1381,12 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
menuHandler::clockMenu(); menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu(); menuHandler::LoraRegionPicker();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (devicestate.rx_text_message.from) { if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu(); menuHandler::messageResponseMenu();
} else { } else {
#if defined(M5STACK_UNITC6L)
menuHandler::textMessageMenu();
#else
menuHandler::textMessageBaseMenu(); menuHandler::textMessageBaseMenu();
#endif
} }
} else if (framesetInfo.positions.firstFavorite != 255 && } else if (framesetInfo.positions.firstFavorite != 255 &&
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
@@ -1676,15 +1445,13 @@ bool shouldWakeOnReceivedMessage()
/* /*
The goal here is to determine when we do NOT wake up the screen on message received: The goal here is to determine when we do NOT wake up the screen on message received:
- Any ext. notifications are turned on - Any ext. notifications are turned on
- If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - If role is not client / client_mute
- If the battery level is very low - If the battery level is very low
*/ */
if (moduleConfig.external_notification.enabled) { if (moduleConfig.external_notification.enabled) {
return false; return false;
} }
if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN,
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
return false; return false;
} }
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {

View File

@@ -12,7 +12,7 @@
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics namespace graphics
{ {
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker };
struct BannerOverlayOptions { struct BannerOverlayOptions {
const char *message; const char *message;
@@ -81,8 +81,6 @@ class Screen
#include <SSD1306Wire.h> #include <SSD1306Wire.h>
#elif defined(USE_ST7789) #elif defined(USE_ST7789)
#include <ST7789Spi.h> #include <ST7789Spi.h>
#elif defined(USE_SPISSD1306)
#include <SSD1306Spi.h>
#else #else
// the SH1106/SSD1306 variant is auto-detected // the SH1106/SSD1306 variant is auto-detected
#include <AutoOLEDWire.h> #include <AutoOLEDWire.h>
@@ -259,7 +257,15 @@ class Screen : public concurrency::OSThread
void setup(); void setup();
/// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink /// Turns the screen on/off. Optionally, pass a custom screensaver frame for E-Ink
void setOn(bool on, FrameCallback einkScreensaver = NULL); void setOn(bool on, FrameCallback einkScreensaver = NULL)
{
if (!on)
// We handle off commands immediately, because they might be called because the CPU is shutting down
handleSetOn(false, einkScreensaver);
else
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
}
/** /**
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just * Prepare the display for the unit going to the lowest power mode possible. Most screens will just
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code * poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
@@ -307,8 +313,6 @@ class Screen : public concurrency::OSThread
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback); void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback);
void requestMenu(graphics::menuHandler::screenMenus menuToShow) void requestMenu(graphics::menuHandler::screenMenus menuToShow)
{ {
@@ -587,11 +591,7 @@ class Screen : public concurrency::OSThread
void setSSLFrames(); void setSSLFrames();
// Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint)
void hideCurrentFrame(); void dismissCurrentFrame();
// Menu-driven Show / Hide Toggle
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
#ifdef USE_EINK #ifdef USE_EINK
/// Draw an image to remain on E-Ink display after screen off /// Draw an image to remain on E-Ink display after screen off
@@ -653,7 +653,7 @@ class Screen : public concurrency::OSThread
uint8_t settings = 255; uint8_t settings = 255;
uint8_t wifi = 255; uint8_t wifi = 255;
uint8_t deviceFocused = 255; uint8_t deviceFocused = 255;
uint8_t system = 255; uint8_t memory = 255;
uint8_t gps = 255; uint8_t gps = 255;
uint8_t home = 255; uint8_t home = 255;
uint8_t textMessage = 255; uint8_t textMessage = 255;
@@ -663,7 +663,6 @@ class Screen : public concurrency::OSThread
uint8_t nodelist_distance = 255; uint8_t nodelist_distance = 255;
uint8_t nodelist_bearings = 255; uint8_t nodelist_bearings = 255;
uint8_t clock = 255; uint8_t clock = 255;
uint8_t chirpy = 255;
uint8_t firstFavorite = 255; uint8_t firstFavorite = 255;
uint8_t lastFavorite = 255; uint8_t lastFavorite = 255;
uint8_t lora = 255; uint8_t lora = 255;
@@ -672,29 +671,12 @@ class Screen : public concurrency::OSThread
uint8_t frameCount = 0; uint8_t frameCount = 0;
} framesetInfo; } framesetInfo;
struct hiddenFrames { struct DismissedFrames {
bool textMessage = false; bool textMessage = false;
bool waypoint = false; bool waypoint = false;
bool wifi = false; bool wifi = false;
bool system = false; bool memory = false;
bool home = false; } dismissedFrames;
bool clock = false;
#ifndef USE_EINK
bool nodelist = false;
#endif
#ifdef USE_EINK
bool nodelist_lastheard = false;
bool nodelist_hopsignal = false;
bool nodelist_distance = false;
#endif
#if HAS_GPS
bool nodelist_bearings = false;
bool gps = false;
#endif
bool lora = false;
bool show_favorites = false;
bool chirpy = true;
} hiddenFrames;
/// Try to start drawing ASAP /// Try to start drawing ASAP
void setFastFramerate(); void setFastFramerate();

View File

@@ -73,16 +73,12 @@
#endif #endif
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts // The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#elif defined(M5STACK_UNITC6L)
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13
#define FONT_LARGE FONT_SMALL_LOCAL // Height: 13
#else #else
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
#define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19 #define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19

View File

@@ -1,7 +1,6 @@
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "RTC.h" #include "RTC.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h" #include "main.h"
#include "meshtastic/config.pb.h" #include "meshtastic/config.pb.h"
#include "power.h" #include "power.h"
@@ -17,10 +16,6 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
isHighResolution = true; isHighResolution = true;
} }
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
}
// Special case for Heltec Wireless Tracker v1.1 // Special case for Heltec Wireless Tracker v1.1
if (screenwidth == 160 && screenheight == 80) { if (screenwidth == 160 && screenheight == 80) {
isHighResolution = false; isHighResolution = false;
@@ -58,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// ************************* // *************************
// * Common Header Drawing * // * Common Header Drawing *
// ************************* // *************************
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
{ {
constexpr int HEADER_OFFSET_Y = 1; constexpr int HEADER_OFFSET_Y = 1;
y += HEADER_OFFSET_Y; y += HEADER_OFFSET_Y;
@@ -74,7 +69,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
const int screenW = display->getWidth(); const int screenW = display->getWidth();
const int screenH = display->getHeight(); const int screenH = display->getHeight();
if (!force_no_invert) { if (!battery_only) {
// === Inverted Header Background === // === Inverted Header Background ===
if (isInverted) { if (isInverted) {
display->setColor(BLACK); display->setColor(BLACK);
@@ -129,7 +124,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int batteryX = 1; int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1; int batteryY = HEADER_OFFSET_Y + 1;
#if !defined(M5STACK_UNITC6L)
// === Battery Icons === // === Battery Icons ===
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
batteryX += 1; batteryX += 1;
@@ -192,28 +187,13 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
int timeX = screenW - xOffset - timeStrWidth + 4; int timeX = screenW - xOffset - timeStrWidth + 4;
if (rtc_sec > 0) { if (rtc_sec > 0 && !battery_only) {
// === Build Time String === // === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR; int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
// === Build Date String ===
char datetimeStr[25];
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
char dateLine[40];
if (isHighResolution) {
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
} else {
if (hasUnreadMessage) {
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
} else {
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
}
}
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
bool isPM = hour >= 12; bool isPM = hour >= 12;
hour %= 12; hour %= 12;
@@ -222,11 +202,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
} }
if (show_date) { timeStrWidth = display->getStringWidth(timeStr);
timeStrWidth = display->getStringWidth(dateLine);
} else {
timeStrWidth = display->getStringWidth(timeStr);
}
timeX = screenW - xOffset - timeStrWidth + 3; timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time === // === Show Mail or Mute Icon to the Left of Time ===
@@ -253,7 +229,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconW = 16, iconH = 12; int iconW = 16, iconH = 12;
int iconX = iconRightEdge - iconW; int iconX = iconRightEdge - iconW;
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
if (isInverted && !force_no_invert) { if (isInverted) {
display->setColor(WHITE); display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
display->setColor(BLACK); display->setColor(BLACK);
@@ -268,7 +244,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} else { } else {
int iconX = iconRightEdge - (mail_width - 2); int iconX = iconRightEdge - (mail_width - 2);
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
if (isInverted && !force_no_invert) { if (isInverted) {
display->setColor(WHITE); display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
display->setColor(BLACK); display->setColor(BLACK);
@@ -284,7 +260,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_big_width; int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
if (isInverted && !force_no_invert) { if (isInverted) {
display->setColor(WHITE); display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
display->setColor(BLACK); display->setColor(BLACK);
@@ -311,17 +287,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
} }
if (show_date) { // === Draw Time ===
// === Draw Date === display->drawString(timeX, textY, timeStr);
display->drawString(timeX, textY, dateLine); if (isBold)
if (isBold) display->drawString(timeX - 1, textY, timeStr);
display->drawString(timeX - 1, textY, dateLine);
} else {
// === Draw Time ===
display->drawString(timeX, textY, timeStr);
if (isBold)
display->drawString(timeX - 1, textY, timeStr);
}
} else { } else {
// === No Time Available: Mail/Mute Icon Moves to Far Right === // === No Time Available: Mail/Mute Icon Moves to Far Right ===
@@ -368,7 +337,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
} }
} }
#endif
display->setColor(WHITE); // Reset for other UI display->setColor(WHITE); // Reset for other UI
} }

View File

@@ -49,8 +49,7 @@ void determineResolution(int16_t screenheight, int16_t screenwidth);
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
// Shared battery/time/mail header // Shared battery/time/mail header
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
bool show_date = false);
const int *getTextPositions(OLEDDisplay *display); const int *getTextPositions(OLEDDisplay *display);

View File

@@ -562,91 +562,6 @@ class LGFX : public lgfx::LGFX_Device
static LGFX *tft = nullptr; static LGFX *tft = nullptr;
#elif defined(ST7796_CS)
#include <LovyanGFX.hpp> // Graphics and font library for ST7796 driver chip
class LGFX : public lgfx::LGFX_Device
{
lgfx::Panel_ST7796 _panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::Light_PWM _light_instance;
public:
LGFX(void)
{
{
auto cfg = _bus_instance.config();
// SPI
cfg.spi_host = ST7796_SPI_HOST;
cfg.spi_mode = 0;
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
// 80MHz by an integer)
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
cfg.spi_3wire = false;
cfg.use_lock = true; // Set to true to use transaction locking
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
// SPI_DMA_CH_AUTO=auto setting)
cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number
cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number
cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable)
cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable)
_bus_instance.config(cfg); // applies the set value to the bus.
_panel_instance.setBus(&_bus_instance); // set the bus on the panel.
}
{ // Set the display panel control.
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable)
cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable)
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
// cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
cfg.panel_width = TFT_WIDTH; // actual displayable width
cfg.panel_height = TFT_HEIGHT; // actual displayable height
cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
#ifdef TFT_DUMMY_READ_PIXELS
cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
#else
cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
#endif
cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
cfg.readable = true; // Set to true if data can be read
cfg.invert = true; // Set to true if the light/darkness of the panel is reversed
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
cfg.dlen_16bit =
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
_panel_instance.config(cfg);
}
#ifdef ST7796_BL
// Set the backlight control. (delete if not necessary)
{
auto cfg = _light_instance.config(); // Gets a structure for backlight settings.
cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected
cfg.invert = false; // true to invert the brightness of the backlight
cfg.freq = 44100;
cfg.pwm_channel = 7;
_light_instance.config(cfg);
_panel_instance.setLight(&_light_instance); // Set the backlight on the panel.
}
#endif
setPanel(&_panel_instance); // Sets the panel to use.
}
};
static LGFX *tft = nullptr;
#elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER)
#include <LovyanGFX.hpp> // Graphics and font library for ILI9341/ILI9342 driver chip #include <LovyanGFX.hpp> // Graphics and font library for ILI9341/ILI9342 driver chip
@@ -751,41 +666,34 @@ static LGFX *tft = nullptr;
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
#elif ARCH_PORTDUINO #elif ARCH_PORTDUINO
#include "Panel_sdl.hpp"
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip #include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
class LGFX : public lgfx::LGFX_Device class LGFX : public lgfx::LGFX_Device
{ {
lgfx::Panel_Device *_panel_instance;
lgfx::Bus_SPI _bus_instance; lgfx::Bus_SPI _bus_instance;
lgfx::ITouch *_touch_instance; lgfx::ITouch *_touch_instance;
public: public:
lgfx::Panel_Device *_panel_instance;
LGFX(void) LGFX(void)
{ {
if (portduino_config.displayPanel == st7789) if (settingsMap[displayPanel] == st7789)
_panel_instance = new lgfx::Panel_ST7789; _panel_instance = new lgfx::Panel_ST7789;
else if (portduino_config.displayPanel == st7735) else if (settingsMap[displayPanel] == st7735)
_panel_instance = new lgfx::Panel_ST7735; _panel_instance = new lgfx::Panel_ST7735;
else if (portduino_config.displayPanel == st7735s) else if (settingsMap[displayPanel] == st7735s)
_panel_instance = new lgfx::Panel_ST7735S; _panel_instance = new lgfx::Panel_ST7735S;
else if (portduino_config.displayPanel == st7796) else if (settingsMap[displayPanel] == st7796)
_panel_instance = new lgfx::Panel_ST7796; _panel_instance = new lgfx::Panel_ST7796;
else if (portduino_config.displayPanel == ili9341) else if (settingsMap[displayPanel] == ili9341)
_panel_instance = new lgfx::Panel_ILI9341; _panel_instance = new lgfx::Panel_ILI9341;
else if (portduino_config.displayPanel == ili9342) else if (settingsMap[displayPanel] == ili9342)
_panel_instance = new lgfx::Panel_ILI9342; _panel_instance = new lgfx::Panel_ILI9342;
else if (portduino_config.displayPanel == ili9488) else if (settingsMap[displayPanel] == ili9488)
_panel_instance = new lgfx::Panel_ILI9488; _panel_instance = new lgfx::Panel_ILI9488;
else if (portduino_config.displayPanel == hx8357d) else if (settingsMap[displayPanel] == hx8357d)
_panel_instance = new lgfx::Panel_HX8357D; _panel_instance = new lgfx::Panel_HX8357D;
#if defined(SDL_h_)
else if (portduino_config.displayPanel == x11)
_panel_instance = new lgfx::Panel_sdl;
#endif
else { else {
_panel_instance = new lgfx::Panel_NULL; _panel_instance = new lgfx::Panel_NULL;
LOG_ERROR("Unknown display panel configured!"); LOG_ERROR("Unknown display panel configured!");
@@ -793,67 +701,60 @@ class LGFX : public lgfx::LGFX_Device
auto buscfg = _bus_instance.config(); auto buscfg = _bus_instance.config();
buscfg.spi_mode = 0; buscfg.spi_mode = 0;
buscfg.spi_host = portduino_config.display_spi_dev_int; buscfg.spi_host = settingsMap[displayspidev];
buscfg.pin_dc = portduino_config.displayDC.pin; // Set SPI DC pin number (-1 = disable) buscfg.pin_dc = settingsMap[displayDC]; // Set SPI DC pin number (-1 = disable)
_bus_instance.config(buscfg); // applies the set value to the bus. _bus_instance.config(buscfg); // applies the set value to the bus.
if (portduino_config.displayPanel != x11) _panel_instance->setBus(&_bus_instance); // set the bus on the panel.
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight); LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable) cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = portduino_config.displayReset.pin; cfg.pin_rst = settingsMap[displayReset];
if (portduino_config.displayRotate) { if (settingsMap[displayRotate]) {
cfg.panel_width = portduino_config.displayHeight; // actual displayable width cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
cfg.panel_height = portduino_config.displayWidth; // actual displayable height cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
} else { } else {
cfg.panel_width = portduino_config.displayWidth; // actual displayable width cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
cfg.panel_height = portduino_config.displayHeight; // actual displayable height cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
} }
cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
cfg.offset_rotation = portduino_config.displayOffsetRotate; // Rotation direction value offset 0~7 (4~7 is mirrored) cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
cfg.invert = portduino_config.displayInvert; // Set to true if the light/darkness of the panel is reversed cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed
_panel_instance->config(cfg); _panel_instance->config(cfg);
// Configure settings for touch control. // Configure settings for touch control.
if (portduino_config.touchscreenModule) { if (settingsMap[touchscreenModule]) {
if (portduino_config.touchscreenModule == xpt2046) { if (settingsMap[touchscreenModule] == xpt2046) {
_touch_instance = new lgfx::Touch_XPT2046; _touch_instance = new lgfx::Touch_XPT2046;
} else if (portduino_config.touchscreenModule == stmpe610) { } else if (settingsMap[touchscreenModule] == stmpe610) {
_touch_instance = new lgfx::Touch_STMPE610; _touch_instance = new lgfx::Touch_STMPE610;
} else if (portduino_config.touchscreenModule == ft5x06) { } else if (settingsMap[touchscreenModule] == ft5x06) {
_touch_instance = new lgfx::Touch_FT5x06; _touch_instance = new lgfx::Touch_FT5x06;
} }
auto touch_cfg = _touch_instance->config(); auto touch_cfg = _touch_instance->config();
touch_cfg.pin_cs = portduino_config.touchscreenCS.pin; touch_cfg.pin_cs = settingsMap[touchscreenCS];
touch_cfg.x_min = 0; touch_cfg.x_min = 0;
touch_cfg.x_max = portduino_config.displayHeight - 1; touch_cfg.x_max = settingsMap[displayHeight] - 1;
touch_cfg.y_min = 0; touch_cfg.y_min = 0;
touch_cfg.y_max = portduino_config.displayWidth - 1; touch_cfg.y_max = settingsMap[displayWidth] - 1;
touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin; touch_cfg.pin_int = settingsMap[touchscreenIRQ];
touch_cfg.bus_shared = true; touch_cfg.bus_shared = true;
touch_cfg.offset_rotation = portduino_config.touchscreenRotate; touch_cfg.offset_rotation = settingsMap[touchscreenRotate];
if (portduino_config.touchscreenI2CAddr != -1) { if (settingsMap[touchscreenI2CAddr] != -1) {
touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr; touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr];
} else { } else {
touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int; touch_cfg.spi_host = settingsMap[touchscreenspidev];
} }
_touch_instance->config(touch_cfg); _touch_instance->config(touch_cfg);
_panel_instance->setTouch(_touch_instance); _panel_instance->setTouch(_touch_instance);
} }
#if defined(SDL_h_)
if (portduino_config.displayPanel == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
sdl_panel_->setup();
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
}
#endif
setPanel(_panel_instance); // Sets the panel to use. setPanel(_panel_instance); // Sets the panel to use.
} }
}; };
@@ -1081,9 +982,8 @@ static LGFX *tft = nullptr;
#endif #endif
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0)
(ARCH_PORTDUINO && HAS_SCREEN != 0)
#include "SPILock.h" #include "SPILock.h"
#include "TFTDisplay.h" #include "TFTDisplay.h"
#include <SPI.h> #include <SPI.h>
@@ -1114,10 +1014,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
backlightEnable = p; backlightEnable = p;
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
if (portduino_config.displayRotate) { if (settingsMap[displayRotate]) {
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth); setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
} else { } else {
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight); setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
} }
#elif defined(SCREEN_ROTATE) #elif defined(SCREEN_ROTATE)
@@ -1127,153 +1027,37 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
#endif #endif
} }
TFTDisplay::~TFTDisplay()
{
// Clean up allocated line pixel buffer to prevent memory leak
if (linePixelBuffer != nullptr) {
free(linePixelBuffer);
linePixelBuffer = nullptr;
}
}
// Write the buffer to the display memory // Write the buffer to the display memory
void TFTDisplay::display(bool fromBlank) void TFTDisplay::display(bool fromBlank)
{ {
if (fromBlank) if (fromBlank)
tft->fillScreen(TFT_BLACK); tft->fillScreen(TFT_BLACK);
// tft->clear();
concurrency::LockGuard g(spiLock); concurrency::LockGuard g(spiLock);
uint32_t x, y; uint16_t x, y;
uint32_t y_byteIndex;
uint8_t y_byteMask;
uint32_t x_FirstPixelUpdate;
uint32_t x_LastPixelUpdate;
bool isset, dblbuf_isset;
uint16_t colorTftMesh, colorTftBlack;
bool somethingChanged = false;
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step for (y = 0; y < displayHeight; y++) {
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); for (x = 0; x < displayWidth; x++) {
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7));
y = 0;
while (y < displayHeight) {
y_byteIndex = (y / 8) * displayWidth;
y_byteMask = (1 << (y & 7));
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
if (y_byteMask == 1) {
if (!fromBlank) { if (!fromBlank) {
for (x = 0; x < displayWidth; x++) { // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7));
break;
}
} else {
for (x = 0; x < displayWidth; x++) {
if (buffer[x + y_byteIndex] != 0)
break;
}
}
if (x >= displayWidth) {
// No changed pixels found in these 8 rows, fast-forward to the next 8
y = y + 8;
continue;
}
}
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
if (!fromBlank) {
// get src pixel in the page based ordering the OLED lib uses
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
if (isset != dblbuf_isset) { if (isset != dblbuf_isset) {
break; tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK);
} }
} else if (isset) { } else if (isset) {
break; tft->drawPixel(x, y, TFT_MESH);
} }
} }
// Did we find a pixel that needs updating on this row?
if (x_FirstPixelUpdate < displayWidth) {
// Quickly write out the first changed pixel (saves another array lookup)
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
x_LastPixelUpdate = x_FirstPixelUpdate;
// Step 3: copy all remaining pixels in this row into the pixel line buffer,
// while also recording the last pixel in the row that needs updating
for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) {
isset = buffer[x + y_byteIndex] & y_byteMask;
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
if (!fromBlank) {
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
if (isset != dblbuf_isset) {
x_LastPixelUpdate = x;
}
} else if (isset) {
x_LastPixelUpdate = x;
}
}
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
&linePixelBuffer[x_FirstPixelUpdate]);
somethingChanged = true;
}
y++;
} }
// Copy the Buffer to the Back Buffer // Copy the Buffer to the Back Buffer
if (somethingChanged) for (y = 0; y < (displayHeight / 8); y++) {
memcpy(buffer_back, buffer, displayBufferSize); for (x = 0; x < displayWidth; x++) {
} uint16_t pos = x + y * displayWidth;
buffer_back[pos] = buffer[pos];
void TFTDisplay::sdlLoop()
{
#if defined(SDL_h_)
static int lastPressed = 0;
static int shuttingDown = false;
if (portduino_config.displayPanel == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
if (sdl_panel_->loop() && !shuttingDown) {
LOG_WARN("Window Closed!");
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
// debounce
if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed))
return;
if (!sdl_panel_->gpio_in(37)) {
lastPressed = 37;
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else if (!sdl_panel_->gpio_in(36)) {
lastPressed = 36;
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else if (!sdl_panel_->gpio_in(38)) {
lastPressed = 38;
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else if (!sdl_panel_->gpio_in(39)) {
lastPressed = 39;
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else if (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) {
lastPressed = SDL_SCANCODE_KP_ENTER;
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
} else {
lastPressed = 0;
} }
} }
#endif
} }
// Send a command to the display (low level function) // Send a command to the display (low level function)
@@ -1286,8 +1070,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(true); backlightEnable->set(true);
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
display(true); display(true);
if (portduino_config.displayBacklight.pin > 0) if (settingsMap[displayBacklight] > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON); digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup(); tft->wakeup();
tft->powerSaveOff(); tft->powerSaveOff();
@@ -1310,8 +1094,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(false); backlightEnable->set(false);
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
tft->clear(); tft->clear();
if (portduino_config.displayBacklight.pin > 0) if (settingsMap[displayBacklight] > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON); digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) #elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep(); tft->sleep();
tft->powerSaveOn(); tft->powerSaveOn();
@@ -1422,21 +1206,13 @@ bool TFTDisplay::connect()
tft->setRotation(1); // T-Deck has the TFT in landscape tft->setRotation(1); // T-Deck has the TFT in landscape
#elif defined(T_WATCH_S3) #elif defined(T_WATCH_S3)
tft->setRotation(2); // T-Watch S3 left-handed orientation tft->setRotation(2); // T-Watch S3 left-handed orientation
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) #elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR)
tft->setRotation(0); // use config.yaml to set rotation tft->setRotation(0); // use config.yaml to set rotation
#else #else
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
#endif #endif
tft->fillScreen(TFT_BLACK); tft->fillScreen(TFT_BLACK);
if (this->linePixelBuffer == NULL) {
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
if (!this->linePixelBuffer) {
LOG_ERROR("Not enough memory to create TFT line buffer\n");
return false;
}
}
return true; return true;
} }

View File

@@ -20,13 +20,9 @@ class TFTDisplay : public OLEDDisplay
*/ */
TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
// Destructor to clean up allocated memory
~TFTDisplay();
// Write the buffer to the display memory // Write the buffer to the display memory
virtual void display() override { display(false); }; virtual void display() override { display(false); };
virtual void display(bool fromBlank); virtual void display(bool fromBlank);
void sdlLoop();
// Turn the display upside down // Turn the display upside down
virtual void flipScreenVertically(); virtual void flipScreenVertically();
@@ -61,6 +57,4 @@ class TFTDisplay : public OLEDDisplay
// Connect to the display // Connect to the display
virtual bool connect() override; virtual bool connect() override;
uint16_t *linePixelBuffer = nullptr;
}; };

View File

@@ -1,738 +0,0 @@
#include "VirtualKeyboard.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h"
#include <Arduino.h>
#include <vector>
namespace graphics
{
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
{
initializeKeyboard();
// Set cursor to H(2, 5)
cursorRow = 2;
cursorCol = 5;
}
VirtualKeyboard::~VirtualKeyboard() {}
void VirtualKeyboard::initializeKeyboard()
{
// New 4 row, 11 column keyboard layout:
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
// Derive layout dimensions and assert they match the configured keyboard grid
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
// Initialize all keys to empty first
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
}
}
// Fill keyboard from the 2D layout
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
char ch = LAYOUT[row][col];
// No empty slots in the simplified layout
VirtualKeyType type = VK_CHAR;
if (ch == '\b') {
type = VK_BACKSPACE;
} else if (ch == '\n') {
type = VK_ENTER;
} else if (ch == '\x1b') { // ESC
type = VK_ESC;
} else if (ch == ' ') {
type = VK_SPACE;
}
// Make action keys wider to fit text while keeping the last column aligned
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
}
}
}
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
{
// Repeat ticking is driven by NotificationRenderer once per frame
// Base styles
display->setColor(WHITE);
display->setFont(FONT_SMALL);
// Screen geometry
const int screenW = display->getWidth();
const int screenH = display->getHeight();
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
const bool isWide = screenW >= 200;
// Determine last-column label max width
display->setFont(FONT_SMALL);
const int wENTER = display->getStringWidth("ENTER");
int lastColLabelW = wENTER; // ENTER is usually the widest
// Smaller padding on very small screens to avoid excessive whitespace
const int lastColPad = (screenW <= 128 ? 2 : 6);
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
// Always reserve width for the rightmost text column to avoid overlap on small screens
int cellW = 0;
int leftoverW = 0;
{
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
int usableW = screenW - reservedLastColW;
if (usableW < leftCols) {
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
usableW = leftCols;
}
cellW = usableW / leftCols;
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
}
// Dynamic key geometry
int cellH = KEY_HEIGHT;
int keyboardStartY = 0;
if (screenH <= 64) {
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
const int gapBelowHeader = 0;
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
const int gapAboveKeyboard = 0;
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
if (keyboardStartY < 0)
keyboardStartY = 0;
if (keyboardStartY > screenH)
keyboardStartY = screenH;
int keyboardHeight = screenH - keyboardStartY;
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
} else if (isWide) {
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
cellH = std::max((int)KEY_HEIGHT, cellW);
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
display->setFont(FONT_SMALL);
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
const int headerToBoxGap = 1;
const int gapAboveKb = 1;
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
if (maxCellHAllowed < (int)KEY_HEIGHT)
maxCellHAllowed = KEY_HEIGHT;
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
cellH = maxCellHAllowed;
}
// Keyboard placement from bottom for wide screens
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
} else {
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
cellH = KEY_HEIGHT;
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
}
// Draw input area above keyboard
drawInputArea(display, offsetX, offsetY, keyboardStartY);
// Precompute per-column x and width with leftover distributed over left columns for even spacing
int colX[KEYBOARD_COLS];
int colW[KEYBOARD_COLS];
int runningX = offsetX;
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
int wcol = cellW + (col < leftoverW ? 1 : 0);
colX[col] = runningX;
colW[col] = wcol;
runningX += wcol;
}
// Last column
colX[KEYBOARD_COLS - 1] = runningX;
colW[KEYBOARD_COLS - 1] = reservedLastColW;
// Draw keyboard grid
for (int row = 0; row < KEYBOARD_ROWS; row++) {
for (int col = 0; col < KEYBOARD_COLS; col++) {
const VirtualKey &k = keyboard[row][col];
if (k.character != 0 || k.type != VK_CHAR) {
const bool isLastCol = (col == KEYBOARD_COLS - 1);
int x = colX[col];
int w = colW[col];
int y = offsetY + keyboardStartY + row * cellH;
int h = cellH;
bool selected = (row == cursorRow && col == cursorCol);
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
}
}
}
}
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
{
display->setColor(WHITE);
const int screenWidth = display->getWidth();
const int screenHeight = display->getHeight();
// Use the standard small font metrics for input box sizing (restore original size)
const int inputLineH = FONT_HEIGHT_SMALL;
// Header uses the standard small (which may be larger on big screens)
display->setFont(FONT_SMALL);
int headerHeight = 0;
if (!headerText.empty()) {
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
display->drawString(offsetX + 2, offsetY, headerText.c_str());
if (screenHeight <= 64) {
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
} else {
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
}
}
const int boxX = offsetX;
const int boxWidth = screenWidth;
int boxY;
int boxHeight;
if (screenHeight <= 64) {
const int gapBelowHeader = 0;
const int fixedBoxHeight = inputLineH;
const int gapAboveKeyboard = 0;
boxY = offsetY + headerHeight + gapBelowHeader;
boxHeight = fixedBoxHeight;
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
boxHeight = std::max(1, fixedBoxHeight - over);
}
} else {
const int gapBelowHeader = 1;
int gapAboveKeyboard = 1;
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
const int minBoxHeight = inputLineH + 2;
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
if (availableH < minBoxHeight)
availableH = minBoxHeight;
boxY = tmpBoxY;
boxHeight = availableH;
}
// Draw box border
display->drawRect(boxX, boxY, boxWidth, boxHeight);
display->setFont(FONT_SMALL);
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
const int textX = boxX + 2;
const int maxTextWidth = boxWidth - 4;
const int maxLines = (boxHeight - 2) / inputLineH;
if (maxLines >= 2) {
// Inner bounds for caret clamping
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Wrap text greedily into lines that fit maxTextWidth
std::vector<std::string> lines;
{
std::string remaining = inputText;
while (!remaining.empty()) {
int bestLen = 0;
for (int len = 1; len <= (int)remaining.size(); ++len) {
int w = display->getStringWidth(remaining.substr(0, len).c_str());
if (w <= maxTextWidth)
bestLen = len;
else
break;
}
if (bestLen == 0) {
// At least show one character to make progress
bestLen = 1;
}
lines.emplace_back(remaining.substr(0, bestLen));
remaining.erase(0, bestLen);
}
}
const bool scrolledUp = ((int)lines.size() > maxLines);
int caretX = textX;
int caretY = innerTop;
// Leave a small top gap to render '...' without replacing the first line
const int topInset = 2;
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
int lineY = innerTop + topInset;
if (scrolledUp) {
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
const int firstLineTop = lineY; // baseline top for the first visible line
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
const int centerX = boxX + boxWidth / 2;
const int dotSpacing = 3; // px between dots
const int dotSize = 1; // small square dot
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
display->fillRect(centerX, gapMidY, dotSize, dotSize);
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
}
// How many lines fit with our top inset and tighter step
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
const int linesToShow = std::min((int)lines.size(), linesCapacity);
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
for (int i = 0; i < linesToShow; ++i) {
const std::string &chunk = lines[startIndex + i];
display->drawString(textX, lineY, chunk.c_str());
caretX = textX + display->getStringWidth(chunk.c_str());
caretY = lineY;
lineY += lineStep;
}
// Draw caret at end of the last visible line
int caretPadY = 2;
if (boxHeight >= inputLineH + 4)
caretPadY = 3;
int cursorTop = caretY + caretPadY;
// Use lineStep so caret height matches the row spacing
int cursorH = lineStep - caretPadY * 2;
if (cursorH < 1)
cursorH = 1;
// Clamp vertical bounds to stay inside the inner rect
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
// Only draw if cursor is inside inner bounds
if (caretX >= innerLeft && caretX <= innerRight) {
display->drawVerticalLine(caretX, cursorTop, cursorH);
}
} else {
std::string displayText = inputText;
int textW = display->getStringWidth(displayText.c_str());
std::string scrolled = displayText;
if (textW > maxTextWidth) {
// Trim from the left until it fits
while (textW > maxTextWidth && !scrolled.empty()) {
scrolled.erase(0, 1);
textW = display->getStringWidth(scrolled.c_str());
}
// Add leading ellipsis and ensure it still fits
if (scrolled != displayText) {
scrolled = "..." + scrolled;
textW = display->getStringWidth(scrolled.c_str());
// If adding ellipsis causes overflow, trim more after the ellipsis
while (textW > maxTextWidth && scrolled.size() > 3) {
scrolled.erase(3, 1); // remove chars after the ellipsis
textW = display->getStringWidth(scrolled.c_str());
}
}
} else {
// Keep textW in sync with what we draw
textW = display->getStringWidth(scrolled.c_str());
}
int textY;
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
int innerH = innerBottom - innerTop + 1;
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
// Clamp fully inside the inner rect
if (textY < innerTop)
textY = innerTop;
int maxTop = innerBottom - inputLineH + 1;
if (textY > maxTop)
textY = maxTop;
}
if (!scrolled.empty()) {
display->drawString(textX, textY, scrolled.c_str());
}
int cursorX = textX + textW;
if (screenHeight > 64) {
const int innerRight = boxX + boxWidth - 2;
if (cursorX > innerRight)
cursorX = innerRight;
}
int cursorTop, cursorH;
if (screenHeight <= 64) {
cursorH = 10;
cursorTop = boxY + (boxHeight - cursorH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
cursorTop = boxY + 2;
cursorH = boxHeight - 4;
if (cursorH < 1)
cursorH = 1;
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
if (cursorX < innerLeft || cursorX > innerRight)
return;
}
display->drawVerticalLine(cursorX, cursorTop, cursorH);
}
}
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
uint8_t height, bool isLastCol)
{
// Draw key content
display->setFont(FONT_SMALL);
const int fontH = FONT_HEIGHT_SMALL;
// Build label and metrics first
std::string keyText;
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
// Keep literal text labels for the action keys on the rightmost column
keyText = (key.type == VK_BACKSPACE) ? "BACK"
: (key.type == VK_ENTER) ? "ENTER"
: (key.type == VK_SPACE) ? "SPACE"
: (key.type == VK_ESC) ? "ESC"
: "";
} else {
char c = getCharForKey(key, false);
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
}
int textWidth = display->getStringWidth(keyText.c_str());
// Label alignment
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
int textX;
if (isLastCol) {
const int rightPad = 1;
textX = x + width - textWidth - rightPad;
if (textX < x)
textX = x; // guard
} else {
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
textX = x + (width - textWidth + 1) / 2;
} else {
textX = x + (width - textWidth) / 2;
}
}
int contentTop = y;
int contentH = height;
if (selected) {
display->setColor(WHITE);
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
if (display->getHeight() <= 64 && !isAction) {
display->fillRect(x, y, width, height);
} else if (isAction) {
const int padX = 1;
const int padY = 2;
int hlW = textWidth + padX * 2;
int hlX = textX - padX;
if (hlX < x) {
hlW -= (x - hlX);
hlX = x;
}
int maxW = (x + width) - hlX;
if (hlW > maxW)
hlW = maxW;
if (hlW < 1)
hlW = 1;
int hlH = std::min(fontH + padY * 2, (int)height);
int hlY = y + (height - hlH) / 2;
display->fillRect(hlX, hlY, hlW, hlH);
contentTop = hlY;
contentH = hlH;
} else {
display->fillRect(x, y, width, height);
}
display->setColor(BLACK);
} else {
display->setColor(WHITE);
}
int centeredTextY;
if (display->getHeight() <= 64) {
centeredTextY = y + (height - fontH) / 2;
} else {
centeredTextY = contentTop + (contentH - fontH) / 2;
}
if (display->getHeight() > 64) {
if (centeredTextY < contentTop)
centeredTextY = contentTop;
if (centeredTextY + fontH > contentTop + contentH)
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
}
if (display->getHeight() <= 64 && keyText.size() == 1) {
char ch = keyText[0];
if (ch == '.' || ch == ',' || ch == ';') {
centeredTextY -= 1;
}
}
display->drawString(textX, centeredTextY, keyText.c_str());
}
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
{
if (key.type != VK_CHAR) {
return key.character;
}
char c = key.character;
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
}
return c;
}
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
{
resetTimeout();
// wrap around rows and cols in the 4x11 grid
int r = (int)cursorRow + dRow;
int c = (int)cursorCol + dCol;
if (r < 0)
r = KEYBOARD_ROWS - 1;
else if (r >= KEYBOARD_ROWS)
r = 0;
if (c < 0)
c = KEYBOARD_COLS - 1;
else if (c >= KEYBOARD_COLS)
c = 0;
cursorRow = (uint8_t)r;
cursorCol = (uint8_t)c;
}
void VirtualKeyboard::moveCursorUp()
{
moveCursorDelta(-1, 0);
}
void VirtualKeyboard::moveCursorDown()
{
moveCursorDelta(1, 0);
}
void VirtualKeyboard::moveCursorLeft()
{
resetTimeout();
if (cursorCol > 0) {
cursorCol--;
} else {
if (cursorRow > 0) {
cursorRow--;
cursorCol = KEYBOARD_COLS - 1;
} else {
cursorRow = KEYBOARD_ROWS - 1;
cursorCol = KEYBOARD_COLS - 1;
}
}
}
void VirtualKeyboard::moveCursorRight()
{
resetTimeout();
if (cursorCol < KEYBOARD_COLS - 1) {
cursorCol++;
} else {
if (cursorRow < KEYBOARD_ROWS - 1) {
cursorRow++;
cursorCol = 0;
} else {
cursorRow = 0;
cursorCol = 0;
}
}
}
void VirtualKeyboard::handlePress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert lowercase character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
return;
}
// Handle non-character keys immediately
switch (key.type) {
case VK_BACKSPACE:
deleteCharacter();
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
std::function<void(const std::string &)> callback = onTextEntered;
onTextEntered = nullptr;
inputText = "";
callback("");
}
return;
default:
break;
}
}
void VirtualKeyboard::handleLongPress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert uppercase/alternate character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
return;
}
switch (key.type) {
case VK_BACKSPACE:
// One-shot: delete up to 5 characters on long press
for (int i = 0; i < 5; ++i) {
if (inputText.empty())
break;
deleteCharacter();
}
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
onTextEntered("");
}
break;
default:
break;
}
}
void VirtualKeyboard::insertCharacter(char c)
{
if (inputText.length() < 160) { // Reasonable text length limit
inputText += c;
}
}
void VirtualKeyboard::deleteCharacter()
{
if (!inputText.empty()) {
inputText.pop_back();
}
}
void VirtualKeyboard::submitText()
{
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
// Only submit if text is not empty
if (!inputText.empty() && onTextEntered) {
// Store callback and text to submit before clearing callback
std::function<void(const std::string &)> callback = onTextEntered;
std::string textToSubmit = inputText;
onTextEntered = nullptr;
// Don't clear inputText here - let the calling module handle cleanup
// inputText = ""; // Removed: keep text visible until module cleans up
callback(textToSubmit);
} else if (inputText.empty()) {
// For empty text, just ignore the submission - don't clear callback
// This keeps the virtual keyboard responsive for further input
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
} else {
// No callback available
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
}
}
void VirtualKeyboard::setInputText(const std::string &text)
{
inputText = text;
}
std::string VirtualKeyboard::getInputText() const
{
return inputText;
}
void VirtualKeyboard::setHeader(const std::string &header)
{
headerText = header;
}
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
{
onTextEntered = callback;
}
void VirtualKeyboard::resetTimeout()
{
lastActivityTime = millis();
}
bool VirtualKeyboard::isTimedOut() const
{
return (millis() - lastActivityTime) > TIMEOUT_MS;
}
} // namespace graphics

View File

@@ -1,80 +0,0 @@
#pragma once
#include "configuration.h"
#include <OLEDDisplay.h>
#include <functional>
#include <string>
namespace graphics
{
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
struct VirtualKey {
char character;
VirtualKeyType type;
uint8_t x;
uint8_t y;
uint8_t width;
uint8_t height;
};
class VirtualKeyboard
{
public:
VirtualKeyboard();
~VirtualKeyboard();
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
void setInputText(const std::string &text);
std::string getInputText() const;
void setHeader(const std::string &header);
void setCallback(std::function<void(const std::string &)> callback);
// Navigation methods for encoder input
void moveCursorUp();
void moveCursorDown();
void moveCursorLeft();
void moveCursorRight();
void handlePress();
void handleLongPress();
// Timeout management
void resetTimeout();
bool isTimedOut() const;
private:
static const uint8_t KEYBOARD_ROWS = 4;
static const uint8_t KEYBOARD_COLS = 11;
static const uint8_t KEY_WIDTH = 9;
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
std::string inputText;
std::string headerText;
std::function<void(const std::string &)> onTextEntered;
uint8_t cursorRow;
uint8_t cursorCol;
// Timeout management for auto-exit
uint32_t lastActivityTime;
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
void initializeKeyboard();
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
bool isLastCol);
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
// Unified cursor movement helper
void moveCursorDelta(int dRow, int dCol);
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
void insertCharacter(char c);
void deleteCharacter();
void submitText();
};
} // namespace graphics

View File

@@ -8,7 +8,6 @@
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "graphics/emotes.h" #include "graphics/emotes.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "main.h" #include "main.h"
@@ -191,8 +190,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
// === Set Title, Blank for Clock // === Set Title, Blank for Clock
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true);
int line = 0;
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
@@ -296,7 +294,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
isPM ? "pm" : "am"); isPM ? "pm" : "am");
} }
#ifndef USE_EINK #ifndef USE_EINK
xOffset = (isHighResolution) ? 18 : 10; xOffset = (isHighResolution) ? 18 : 10;
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
@@ -316,8 +313,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// === Set Title, Blank for Clock // === Set Title, Blank for Clock
const char *titleStr = ""; const char *titleStr = "";
// === Header === // === Header ===
graphics::drawCommonHeader(display, x, y, titleStr, true, true); graphics::drawCommonHeader(display, x, y, titleStr, true);
int line = 0;
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { if (nimbleBluetooth && nimbleBluetooth->isConnected()) {

View File

@@ -94,8 +94,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
8, imgQuestionL1); 8, imgQuestionL1);
@@ -107,7 +106,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
#endif #endif
} else { } else {
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
8, imgSFL1); 8, imgSFL1);
@@ -122,8 +121,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
} else { } else {
// TODO: Raspberry Pi supports more than just the one screen size // TODO: Raspberry Pi supports more than just the one screen size
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
imgInfoL1); imgInfoL1);
@@ -263,6 +261,12 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
display->drawString(x + 1, y, "USB"); display->drawString(x + 1, y, "USB");
} }
// auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true);
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode);
// if (config.display.heading_bold)
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode);
uint32_t currentMillis = millis(); uint32_t currentMillis = millis();
uint32_t seconds = currentMillis / 1000; uint32_t seconds = currentMillis / 1000;
uint32_t minutes = seconds / 60; uint32_t minutes = seconds / 60;
@@ -277,13 +281,12 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
// Line 1 (Still) // Line 1 (Still)
#if !defined(M5STACK_UNITC6L)
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
if (config.display.heading_bold) if (config.display.heading_bold)
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
display->setColor(WHITE); display->setColor(WHITE);
#endif
// Setup string to assemble analogClock string // Setup string to assemble analogClock string
std::string analogClock = ""; std::string analogClock = "";
@@ -330,7 +333,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
#if HAS_GPS #if HAS_GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// Line 3 // Line 3
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude if (config.display.gps_format !=
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
// Line 4 // Line 4
@@ -386,56 +390,31 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
char shortnameble[35]; char shortnameble[35];
getMacAddr(dmac); getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
#if defined(M5STACK_UNITC6L)
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
#else
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
#endif
int textWidth = display->getStringWidth(shortnameble); int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth); int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
// === Second Row: Role === // === Second Row: Radio Preset ===
auto role = DisplayFormatters::getDeviceRole(config.device.role); auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
char device_role[25];
snprintf(device_role, sizeof(device_role), "Role: %s", role);
textWidth = display->getStringWidth(device_role);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], device_role);
// === Third Row: Radio Preset ===
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
char regionradiopreset[25]; char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL; const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) { if (region != nullptr) {
#if defined(M5STACK_UNITC6L)
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
#else
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
#endif
} }
textWidth = display->getStringWidth(regionradiopreset); textWidth = display->getStringWidth(regionradiopreset);
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
// === Fourth Row: Frequency / ChanNum === // === Third Row: Frequency / ChanNum ===
char frequencyslot[35]; char frequencyslot[35];
char freqStr[16]; char freqStr[16];
float freq = RadioLibInterface::instance->getFreq(); float freq = RadioLibInterface::instance->getFreq();
snprintf(freqStr, sizeof(freqStr), "%.3f", freq); snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
if (config.lora.channel_num == 0) { if (config.lora.channel_num == 0) {
#if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
#else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr); snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
#endif
} else { } else {
#if defined(M5STACK_UNITC6L)
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
#else
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num); snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
#endif
} }
size_t len = strlen(frequencyslot); size_t len = strlen(frequencyslot);
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
@@ -445,8 +424,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
#if !defined(M5STACK_UNITC6L) // === Fourth Row: Channel Utilization ===
// === Fifth Row: Channel Utilization ===
const char *chUtil = "ChUtil:"; const char *chUtil = "ChUtil:";
char chUtilPercentage[10]; char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
@@ -463,7 +441,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
int starting_position = centerofscreen - total_line_content_width; int starting_position = centerofscreen - total_line_content_width;
display->drawString(starting_position, getTextPositions(display)[line], chUtil); display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
// Force 56% or higher to show a full 100% bar, text would still show related percent. // Force 56% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) { if (chutil_percent >= 61) {
@@ -500,15 +478,14 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
} }
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
chUtilPercentage); chUtilPercentage);
#endif
} }
// **************************** // ****************************
// * System Screen * // * System Screen *
// **************************** // ****************************
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
display->clear(); display->clear();
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@@ -528,11 +505,8 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
#ifdef USE_EINK #ifdef USE_EINK
barsOffset -= 12; barsOffset -= 12;
#endif #endif
#if defined(M5STACK_UNITC6L)
const int barX = x + 45 + barsOffset;
#else
const int barX = x + 40 + barsOffset; const int barX = x + 40 + barsOffset;
#endif
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
if (total == 0) if (total == 0)
return; return;
@@ -557,7 +531,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
// Label // Label
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->drawString(labelX, getTextPositions(display)[line], label); display->drawString(labelX, getTextPositions(display)[line], label);
#if !defined(M5STACK_UNITC6L)
// Bar // Bar
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
display->setColor(WHITE); display->setColor(WHITE);
@@ -565,7 +539,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
display->fillRect(barX, barY, fillWidth, barHeight); display->fillRect(barX, barY, fillWidth, barHeight);
display->setColor(WHITE); display->setColor(WHITE);
#endif
// Value string // Value string
display->setTextAlignment(TEXT_ALIGN_RIGHT); display->setTextAlignment(TEXT_ALIGN_RIGHT);
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
@@ -618,16 +592,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
line += 1; line += 1;
} }
line += 1; line += 1;
char appversionstr[35]; char appversionstr[35];
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION)); snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
char appversionstr_formatted[40]; char appversionstr_formatted[40];
char *lastDot = strrchr(appversionstr, '.'); char *lastDot = strrchr(appversionstr, '.');
#if defined(M5STACK_UNITC6L)
if (lastDot != nullptr) {
*lastDot = '\0'; // truncate string
}
#else
if (lastDot) { if (lastDot) {
size_t prefixLen = lastDot - appversionstr; size_t prefixLen = lastDot - appversionstr;
strncpy(appversionstr_formatted, appversionstr, prefixLen); strncpy(appversionstr_formatted, appversionstr, prefixLen);
@@ -638,12 +606,10 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1); strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
appversionstr[sizeof(appversionstr) - 1] = '\0'; appversionstr[sizeof(appversionstr) - 1] = '\0';
} }
#endif
int textWidth = display->getStringWidth(appversionstr); int textWidth = display->getStringWidth(appversionstr);
int nameX = (SCREEN_WIDTH - textWidth) / 2; int nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line], appversionstr); display->drawString(nameX, getTextPositions(display)[line], appversionstr);
#if !defined(M5STACK_UNITC6L)
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it
line += 1; line += 1;
char uptimeStr[32] = ""; char uptimeStr[32] = "";
@@ -662,36 +628,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line], uptimeStr); display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
} }
#endif
} }
// ****************************
// * Chirpy Screen *
// ****************************
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10;
if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4;
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
} else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
}
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
display->drawString(textX, getTextPositions(display)[line++], "Hello");
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
display->drawString(textX, getTextPositions(display)[line++], "World!");
}
} // namespace DebugRenderer } // namespace DebugRenderer
} // namespace graphics } // namespace graphics
#endif #endif

View File

@@ -31,10 +31,8 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state
// LoRa information display // LoRa information display
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// System screen display // Memory screen display
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
// Chirpy screen display
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
} // namespace DebugRenderer } // namespace DebugRenderer
} // namespace graphics } // namespace graphics

View File

@@ -10,10 +10,7 @@
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h" #include "main.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h" #include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h" #include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h" #include "modules/KeyVerificationModule.h"
@@ -29,28 +26,6 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false; bool test_enabled = false;
uint8_t test_count = 0; uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
} else if (selected == device_role_picker) {
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::OnboardMessage() void menuHandler::OnboardMessage()
{ {
static const char *optionsArray[] = {"OK", "Got it!"}; static const char *optionsArray[] = {"OK", "Got it!"};
@@ -104,11 +79,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
"NP_865", "NP_865",
"BR_902"}; "BR_902"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "LoRa Region";
#else
bannerOptions.message = "Set the LoRa region"; bannerOptions.message = "Set the LoRa region";
#endif
bannerOptions.durationMs = duration; bannerOptions.durationMs = duration;
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 27; bannerOptions.optionsCount = 27;
@@ -116,8 +87,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
auto changes = SEGMENT_CONFIG;
// This is needed as we wait til picking the LoRa region to generate keys for the first time. // This is needed as we wait til picking the LoRa region to generate keys for the first time.
if (!owner.is_licensed) { if (!owner.is_licensed) {
bool keygenSuccess = false; bool keygenSuccess = false;
@@ -126,7 +95,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true; keygenSuccess = true;
} }
} else { } else {
LOG_INFO("Generate new PKI keys"); LOG_INFO("Generate new PKI keys");
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
@@ -144,101 +112,13 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
if (myRegion->dutyCycle < 100) { if (myRegion->dutyCycle < 100) {
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
} }
service->reloadConfig(SEGMENT_CONFIG);
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
// Default broker is in use, so subscribe to the appropriate MQTT root topic for this region
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
changes |= SEGMENT_MODULECONFIG;
}
service->reloadConfig(changes);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::DeviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
Back = 0,
devicerole_client = 1,
devicerole_clientmute = 2,
devicerole_lostandfound = 3,
devicerole_tracker = 4
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Device Role";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
} else if (selected == devicerole_clientmute) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
} else if (selected == devicerole_lostandfound) {
config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
} else if (selected == devicerole_tracker) {
config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
{
static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
"MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
enum optionsNumbers {
Back = 0,
radiopreset_LongSlow = 1,
radiopreset_LongModerate = 2,
radiopreset_LongFast = 3,
radiopreset_MediumSlow = 4,
radiopreset_MediumFast = 5,
radiopreset_ShortSlow = 6,
radiopreset_ShortFast = 7,
radiopreset_ShortTurbo = 8
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Radio Preset";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 9;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == radiopreset_LongSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
} else if (selected == radiopreset_LongModerate) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
} else if (selected == radiopreset_LongFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
} else if (selected == radiopreset_MediumSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
} else if (selected == radiopreset_MediumFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
} else if (selected == radiopreset_ShortSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
} else if (selected == radiopreset_ShortFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
} else if (selected == radiopreset_ShortTurbo) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::TwelveHourPicker() void menuHandler::TwelveHourPicker()
{ {
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
@@ -380,11 +260,7 @@ void menuHandler::TZPicker()
void menuHandler::clockMenu() void menuHandler::clockMenu()
{ {
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
#else
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
#endif
enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 }; enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Clock Action"; bannerOptions.message = "Clock Action";
@@ -408,11 +284,8 @@ void menuHandler::clockMenu()
void menuHandler::messageResponseMenu() void menuHandler::messageResponseMenu()
{ {
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 }; enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"}; static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset}; static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
int options = 3; int options = 3;
@@ -426,17 +299,13 @@ void menuHandler::messageResponseMenu()
optionsEnumArray[options++] = Aloud; optionsEnumArray[options++] = Aloud;
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Message";
#else
bannerOptions.message = "Message Action"; bannerOptions.message = "Message Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) { if (selected == Dismiss) {
screen->hideCurrentFrame(); screen->dismissCurrentFrame();
} else if (selected == Preset) { } else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
@@ -477,17 +346,10 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Sleep Screen"; optionsArray[options] = "Sleep Screen";
optionsEnumArray[options++] = Sleep; optionsEnumArray[options++] = Sleep;
#endif #endif
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
optionsArray[options] = "Send Position"; optionsArray[options] = "Send Position";
} else {
optionsArray[options] = "Send Node Info";
}
optionsEnumArray[options++] = Position; optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
#else
optionsArray[options] = "New Preset Msg"; optionsArray[options] = "New Preset Msg";
#endif
optionsEnumArray[options++] = Preset; optionsEnumArray[options++] = Preset;
if (kb_found) { if (kb_found) {
optionsArray[options] = "New Freetext Msg"; optionsArray[options] = "New Freetext Msg";
@@ -495,11 +357,7 @@ void menuHandler::homeBaseMenu()
} }
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Home";
#else
bannerOptions.message = "Home Action"; bannerOptions.message = "Home Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
@@ -538,11 +396,6 @@ void menuHandler::homeBaseMenu()
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::textMessageMenu()
{
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
}
void menuHandler::textMessageBaseMenu() void menuHandler::textMessageBaseMenu()
{ {
enum optionsNumbers { Back, Preset, Freetext, enumEnd }; enum optionsNumbers { Back, Preset, Freetext, enumEnd };
@@ -574,32 +427,23 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu() void menuHandler::systemBaseMenu()
{ {
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd }; enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"}; static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back}; static int optionsEnumArray[enumEnd] = {Back};
int options = 1; int options = 1;
optionsArray[options] = "Notifications"; optionsArray[options] = "Notifications";
optionsEnumArray[options++] = Notifications; optionsEnumArray[options++] = Notifications;
#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ #if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \
defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
optionsArray[options] = "Screen Options"; optionsArray[options] = "Screen Options";
optionsEnumArray[options++] = ScreenOptions; optionsEnumArray[options++] = ScreenOptions;
#endif #endif
optionsArray[options] = "Frame Visiblity Toggle";
optionsEnumArray[options++] = FrameToggles;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth";
#else
optionsArray[options] = "Bluetooth Toggle"; optionsArray[options] = "Bluetooth Toggle";
#endif
optionsEnumArray[options++] = Bluetooth; optionsEnumArray[options++] = Bluetooth;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Power";
#else
optionsArray[options] = "Reboot/Shutdown"; optionsArray[options] = "Reboot/Shutdown";
#endif
optionsEnumArray[options++] = PowerMenu; optionsEnumArray[options++] = PowerMenu;
if (test_enabled) { if (test_enabled) {
@@ -608,11 +452,7 @@ void menuHandler::systemBaseMenu()
} }
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "System";
#else
bannerOptions.message = "System Action"; bannerOptions.message = "System Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -626,9 +466,6 @@ void menuHandler::systemBaseMenu()
} else if (selected == PowerMenu) { } else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu; menuHandler::menuQueue = menuHandler::power_menu;
screen->runNow(); screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == Test) { } else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu; menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow(); screen->runNow();
@@ -648,11 +485,7 @@ void menuHandler::systemBaseMenu()
void menuHandler::favoriteBaseMenu() void menuHandler::favoriteBaseMenu()
{ {
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd }; enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
#else
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"}; static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
#endif
static int optionsEnumArray[enumEnd] = {Back, Preset}; static int optionsEnumArray[enumEnd] = {Back, Preset};
int options = 2; int options = 2;
@@ -660,19 +493,13 @@ void menuHandler::favoriteBaseMenu()
optionsArray[options] = "New Freetext Msg"; optionsArray[options] = "New Freetext Msg";
optionsEnumArray[options++] = Freetext; optionsEnumArray[options++] = Freetext;
} }
#if !defined(M5STACK_UNITC6L)
optionsArray[options] = "Trace Route"; optionsArray[options] = "Trace Route";
optionsEnumArray[options++] = TraceRoute; optionsEnumArray[options++] = TraceRoute;
#endif
optionsArray[options] = "Remove Favorite"; optionsArray[options] = "Remove Favorite";
optionsEnumArray[options++] = Remove; optionsEnumArray[options++] = Remove;
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Favorites";
#else
bannerOptions.message = "Favorites Action"; bannerOptions.message = "Favorites Action";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
@@ -695,17 +522,16 @@ void menuHandler::favoriteBaseMenu()
void menuHandler::positionBaseMenu() void menuHandler::positionBaseMenu()
{ {
enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd }; enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd };
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"}; static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"};
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu}; static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu};
int options = 4; int options = 3;
if (accelerometerThread) { if (accelerometerThread) {
optionsArray[options] = "Compass Calibrate"; optionsArray[options] = "Compass Calibrate";
optionsEnumArray[options++] = CompassCalibrate; optionsEnumArray[options++] = CompassCalibrate;
} }
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Position Action"; bannerOptions.message = "Position Action";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
@@ -715,9 +541,6 @@ void menuHandler::positionBaseMenu()
if (selected == GPSToggle) { if (selected == GPSToggle) {
menuQueue = gps_toggle_menu; menuQueue = gps_toggle_menu;
screen->runNow(); screen->runNow();
} else if (selected == GPSFormat) {
menuQueue = gps_format_menu;
screen->runNow();
} else if (selected == CompassMenu) { } else if (selected == CompassMenu) {
menuQueue = compass_point_north_menu; menuQueue = compass_point_north_menu;
screen->runNow(); screen->runNow();
@@ -731,19 +554,11 @@ void menuHandler::positionBaseMenu()
void menuHandler::nodeListMenu() void menuHandler::nodeListMenu()
{ {
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd }; enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
#if defined(M5STACK_UNITC6L)
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
#else
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"}; static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
#endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Action"; bannerOptions.message = "Node Action";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
#if defined(M5STACK_UNITC6L)
bannerOptions.optionsCount = 3;
#else
bannerOptions.optionsCount = 5; bannerOptions.optionsCount = 5;
#endif
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Favorite) { if (selected == Favorite) {
menuQueue = add_favorite; menuQueue = add_favorite;
@@ -762,31 +577,6 @@ void menuHandler::nodeListMenu()
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::nodeNameLengthMenu()
{
enum OptionsNumbers { Back, Long, Short };
static const char *optionsArray[] = {"Back", "Long", "Short"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Node Name Length";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Long) {
// Set names to long
LOG_INFO("Setting names to long");
config.display.use_long_node_name = true;
} else if (selected == Short) {
// Set names to short
LOG_INFO("Setting names to short");
config.display.use_long_node_name = false;
} else if (selected == Back) {
menuQueue = screen_options_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::resetNodeDBMenu() void menuHandler::resetNodeDBMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; static const char *optionsArray[] = {"Back", "Confirm"};
@@ -869,69 +659,13 @@ void menuHandler::GPSToggleMenu()
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2; bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
} }
void menuHandler::GPSFormatMenu()
{
static const char *optionsArray[] = {"Back",
isHighResolution ? "Decimal Degrees" : "DEC",
isHighResolution ? "Degrees Minutes Seconds" : "DMS",
isHighResolution ? "Universal Transverse Mercator" : "UTM",
isHighResolution ? "Military Grid Reference System" : "MGRS",
isHighResolution ? "Open Location Code" : "OLC",
isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
isHighResolution ? "Maidenhead Locator" : "MLS"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 8;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 3) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 4) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 5) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 6) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 7) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu;
screen->runNow();
}
};
bannerOptions.InitialSelected = uiconfig.gps_format + 1;
screen->showOverlayBanner(bannerOptions);
}
#endif #endif
void menuHandler::BluetoothToggleMenu() void menuHandler::BluetoothToggleMenu()
{ {
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Bluetooth";
#else
bannerOptions.message = "Toggle Bluetooth"; bannerOptions.message = "Toggle Bluetooth";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 3; bannerOptions.optionsCount = 3;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
@@ -946,11 +680,11 @@ void menuHandler::BluetoothToggleMenu()
void menuHandler::BuzzerModeMenu() void menuHandler::BuzzerModeMenu()
{ {
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"}; static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Buzzer Mode"; bannerOptions.message = "Buzzer Mode";
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5; bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
service->reloadConfig(SEGMENT_CONFIG); service->reloadConfig(SEGMENT_CONFIG);
@@ -991,7 +725,7 @@ void menuHandler::BrightnessPickerMenu()
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
// For HELTEC devices, use analogWrite to control backlight // For HELTEC devices, use analogWrite to control backlight
analogWrite(VTFT_LEDA, uiconfig.screen_brightness); analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
#elif defined(ST7789_CS) || defined(ST7796_CS) #elif defined(ST7789_CS)
static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
@@ -1034,7 +768,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 10; bannerOptions.optionsCount = 10;
bannerOptions.bannerCallback = [display](int selected) -> void { bannerOptions.bannerCallback = [display](int selected) -> void {
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_r = 0;
uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_g = 0;
uint8_t TFT_MESH_b = 0; uint8_t TFT_MESH_b = 0;
@@ -1123,11 +857,7 @@ void menuHandler::rebootMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Reboot";
#else
bannerOptions.message = "Reboot Device?"; bannerOptions.message = "Reboot Device?";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2; bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1147,11 +877,7 @@ void menuHandler::shutdownMenu()
{ {
static const char *optionsArray[] = {"Back", "Confirm"}; static const char *optionsArray[] = {"Back", "Confirm"};
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Shutdown";
#else
bannerOptions.message = "Shutdown Device?"; bannerOptions.message = "Shutdown Device?";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 2; bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
@@ -1168,12 +894,7 @@ void menuHandler::shutdownMenu()
void menuHandler::addFavoriteMenu() void menuHandler::addFavoriteMenu()
{ {
#if defined(M5STACK_UNITC6L)
screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void {
#else
screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void { screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void {
#endif
LOG_WARN("Nodenum: %u", nodenum); LOG_WARN("Nodenum: %u", nodenum);
nodeDB->set_favorite(true, nodenum); nodeDB->set_favorite(true, nodenum);
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
@@ -1216,33 +937,16 @@ void menuHandler::traceRouteMenu()
void menuHandler::testMenu() void menuHandler::testMenu()
{ {
enum optionsNumbers { Back, NumberPicker, ShowChirpy }; static const char *optionsArray[] = {"Back", "Number Picker"};
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
optionsEnumArray[options++] = NumberPicker;
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
bannerOptions.message = "Hidden Test Menu"; std::string message = "Test to Run?\n";
bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = 2;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.bannerCallback = [](int selected) -> void { bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) { if (selected == 1) {
menuQueue = number_test; menuQueue = number_test;
screen->runNow(); screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = system_base_menu;
screen->runNow();
} }
}; };
screen->showOverlayBanner(bannerOptions); screen->showOverlayBanner(bannerOptions);
@@ -1329,16 +1033,11 @@ void menuHandler::screenOptionsMenu()
hasSupportBrightness = false; hasSupportBrightness = false;
#endif #endif
enum optionsNumbers { Back, NodeNameLength, Brightness, ScreenColor }; enum optionsNumbers { Back, Brightness, ScreenColor };
static const char *optionsArray[5] = {"Back"}; static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[5] = {Back}; static int optionsEnumArray[4] = {Back};
int options = 1; int options = 1;
#if defined(T_DECK) || defined(T_LORA_PAGER)
optionsArray[options] = "Show Long/Short Name";
optionsEnumArray[options++] = NodeNameLength;
#endif
// Only show brightness for B&W displays // Only show brightness for B&W displays
if (hasSupportBrightness) { if (hasSupportBrightness) {
optionsArray[options] = "Brightness"; optionsArray[options] = "Brightness";
@@ -1346,7 +1045,7 @@ void menuHandler::screenOptionsMenu()
} }
// Only show screen color for TFT displays // Only show screen color for TFT displays
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
optionsArray[options] = "Screen Color"; optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor; optionsEnumArray[options++] = ScreenColor;
#endif #endif
@@ -1363,9 +1062,6 @@ void menuHandler::screenOptionsMenu()
} else if (selected == ScreenColor) { } else if (selected == ScreenColor) {
menuHandler::menuQueue = menuHandler::tftcolormenupicker; menuHandler::menuQueue = menuHandler::tftcolormenupicker;
screen->runNow(); screen->runNow();
} else if (selected == NodeNameLength) {
menuHandler::menuQueue = menuHandler::node_name_length_menu;
screen->runNow();
} else { } else {
menuQueue = system_base_menu; menuQueue = system_base_menu;
screen->runNow(); screen->runNow();
@@ -1394,11 +1090,7 @@ void menuHandler::powerMenu()
#endif #endif
BannerOverlayOptions bannerOptions; BannerOverlayOptions bannerOptions;
#if defined(M5STACK_UNITC6L)
bannerOptions.message = "Power";
#else
bannerOptions.message = "Reboot / Shutdown"; bannerOptions.message = "Reboot / Shutdown";
#endif
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options; bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.optionsEnumPtr = optionsEnumArray;
@@ -1451,132 +1143,6 @@ void menuHandler::keyVerificationFinalPrompt()
} }
} }
void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
nodelist,
nodelist_lastheard,
nodelist_hopsignal,
nodelist_distance,
nodelist_bearings,
gps,
lora,
clock,
show_favorites,
show_telemetry,
show_power,
enumEnd
};
static const char *optionsArray[enumEnd] = {"Finish"};
static int optionsEnumArray[enumEnd] = {Finish};
int options = 1;
// Track last selected index (not enum value!)
static int lastSelectedIndex = 0;
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist;
#endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal;
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance;
#endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsEnumArray[options++] = nodelist_bearings;
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
#endif
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
optionsEnumArray[options++] = lora;
optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
optionsEnumArray[options++] = clock;
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
optionsEnumArray[options++] = show_favorites;
optionsArray[options] = moduleConfig.telemetry.environment_screen_enabled ? "Hide Telemetry" : "Show Telemetry";
optionsEnumArray[options++] = show_telemetry;
optionsArray[options] = moduleConfig.telemetry.power_screen_enabled ? "Hide Power" : "Show Power";
optionsEnumArray[options++] = show_power;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Show/Hide Frames";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value
bannerOptions.bannerCallback = [options](int selected) mutable -> void {
// Find the index of selected in optionsEnumArray
int idx = 0;
for (; idx < options; ++idx) {
if (optionsEnumArray[idx] == selected)
break;
}
lastSelectedIndex = idx;
if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_lastheard) {
screen->toggleFrameVisibility("nodelist_lastheard");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_hopsignal) {
screen->toggleFrameVisibility("nodelist_hopsignal");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_distance) {
screen->toggleFrameVisibility("nodelist_distance");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_bearings) {
screen->toggleFrameVisibility("nodelist_bearings");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == gps) {
screen->toggleFrameVisibility("gps");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == lora) {
screen->toggleFrameVisibility("lora");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == clock) {
screen->toggleFrameVisibility("clock");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_favorites) {
screen->toggleFrameVisibility("show_favorites");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_telemetry) {
moduleConfig.telemetry.environment_screen_enabled = !moduleConfig.telemetry.environment_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_power) {
moduleConfig.telemetry.power_screen_enabled = !moduleConfig.telemetry.power_screen_enabled;
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display) void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{ {
if (menuQueue != menu_none) if (menuQueue != menu_none)
@@ -1584,18 +1150,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
switch (menuQueue) { switch (menuQueue) {
case menu_none: case menu_none:
break; break;
case lora_Menu:
loraMenu();
break;
case lora_picker: case lora_picker:
LoraRegionPicker(); LoraRegionPicker();
break; break;
case device_role_picker:
DeviceRolePicker();
break;
case radio_preset_picker:
RadioPresetPicker();
break;
case no_timeout_lora_picker: case no_timeout_lora_picker:
LoraRegionPicker(0); LoraRegionPicker(0);
break; break;
@@ -1621,9 +1178,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case gps_toggle_menu: case gps_toggle_menu:
GPSToggleMenu(); GPSToggleMenu();
break; break;
case gps_format_menu:
GPSFormatMenu();
break;
#endif #endif
case compass_point_north_menu: case compass_point_north_menu:
compassNorthMenu(); compassNorthMenu();
@@ -1643,9 +1197,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case brightness_picker: case brightness_picker:
BrightnessPickerMenu(); BrightnessPickerMenu();
break; break;
case node_name_length_menu:
nodeNameLengthMenu();
break;
case reboot_menu: case reboot_menu:
rebootMenu(); rebootMenu();
break; break;
@@ -1688,9 +1239,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case power_menu: case power_menu:
powerMenu(); powerMenu();
break; break;
case FrameToggles:
FrameToggles_menu();
break;
case throttle_message: case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break; break;

View File

@@ -9,10 +9,7 @@ class menuHandler
public: public:
enum screenMenus { enum screenMenus {
menu_none, menu_none,
lora_Menu,
lora_picker, lora_picker,
device_role_picker,
radio_preset_picker,
no_timeout_lora_picker, no_timeout_lora_picker,
TZ_picker, TZ_picker,
twelve_hour_picker, twelve_hour_picker,
@@ -20,7 +17,6 @@ class menuHandler
clock_menu, clock_menu,
position_base_menu, position_base_menu,
gps_toggle_menu, gps_toggle_menu,
gps_format_menu,
compass_point_north_menu, compass_point_north_menu,
reset_node_db_menu, reset_node_db_menu,
buzzermodemenupicker, buzzermodemenupicker,
@@ -43,16 +39,11 @@ class menuHandler
key_verification_final_prompt, key_verification_final_prompt,
trace_route_menu, trace_route_menu,
throttle_message, throttle_message,
node_name_length_menu,
FrameToggles
}; };
static screenMenus menuQueue; static screenMenus menuQueue;
static void OnboardMessage(); static void OnboardMessage();
static void LoraRegionPicker(uint32_t duration = 30000); static void LoraRegionPicker(uint32_t duration = 30000);
static void loraMenu();
static void DeviceRolePicker();
static void RadioPresetPicker();
static void handleMenuSwitch(OLEDDisplay *display); static void handleMenuSwitch(OLEDDisplay *display);
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm); static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
static void clockMenu(); static void clockMenu();
@@ -67,7 +58,6 @@ class menuHandler
static void positionBaseMenu(); static void positionBaseMenu();
static void compassNorthMenu(); static void compassNorthMenu();
static void GPSToggleMenu(); static void GPSToggleMenu();
static void GPSFormatMenu();
static void BuzzerModeMenu(); static void BuzzerModeMenu();
static void switchToMUIMenu(); static void switchToMUIMenu();
static void TFTColorPickerMenu(OLEDDisplay *display); static void TFTColorPickerMenu(OLEDDisplay *display);
@@ -86,9 +76,6 @@ class menuHandler
static void notificationsMenu(); static void notificationsMenu();
static void screenOptionsMenu(); static void screenOptionsMenu();
static void powerMenu(); static void powerMenu();
static void nodeNameLengthMenu();
static void FrameToggles_menu();
static void textMessageMenu();
private: private:
static void saveUIConfig(); static void saveUIConfig();

View File

@@ -181,19 +181,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
display->clear(); display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
#if defined(M5STACK_UNITC6L)
const int fixedTopHeight = 24;
const int windowX = 0;
const int windowY = fixedTopHeight;
const int windowWidth = 64;
const int windowHeight = SCREEN_HEIGHT - fixedTopHeight;
#else
const int navHeight = FONT_HEIGHT_SMALL; const int navHeight = FONT_HEIGHT_SMALL;
const int scrollBottom = SCREEN_HEIGHT - navHeight; const int scrollBottom = SCREEN_HEIGHT - navHeight;
const int usableHeight = scrollBottom; const int usableHeight = scrollBottom;
const int textWidth = SCREEN_WIDTH; const int textWidth = SCREEN_WIDTH;
#endif
bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
bool isBold = config.display.heading_bold; bool isBold = config.display.heading_bold;
@@ -208,11 +201,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
const char *messageString = "No messages"; const char *messageString = "No messages";
int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
#if defined(M5STACK_UNITC6L)
display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString);
#else
display->drawString(center_text, getTextPositions(display)[2], messageString); display->drawString(center_text, getTextPositions(display)[2], messageString);
#endif
return; return;
} }
@@ -220,10 +209,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
char headerStr[80]; char headerStr[80];
const char *sender = "???"; const char *sender = "???";
#if defined(M5STACK_UNITC6L)
if (node && node->has_user)
sender = node->user.short_name;
#else
if (node && node->has_user) { if (node && node->has_user) {
if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
sender = node->user.long_name; sender = node->user.long_name;
@@ -231,7 +216,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender = node->user.short_name; sender = node->user.short_name;
} }
} }
#endif
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
uint8_t timestampHours, timestampMinutes; uint8_t timestampHours, timestampMinutes;
int32_t daysAgo; int32_t daysAgo;
@@ -251,61 +235,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
sender); sender);
} }
} else { } else {
#if defined(M5STACK_UNITC6L)
snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
sender);
#else
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
sender); sender);
#endif
} }
#if defined(M5STACK_UNITC6L)
graphics::drawCommonHeader(display, x, y, titleStr);
int headerY = getTextPositions(display)[1];
display->drawString(x, headerY, headerStr);
for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) {
display->setPixel(separatorX, fixedTopHeight - 1);
}
cachedLines.clear();
std::string fullMsg(messageBuf);
std::string currentLine;
for (size_t i = 0; i < fullMsg.size();) {
unsigned char c = fullMsg[i];
size_t charLen = 1;
if ((c & 0xE0) == 0xC0)
charLen = 2;
else if ((c & 0xF0) == 0xE0)
charLen = 3;
else if ((c & 0xF8) == 0xF0)
charLen = 4;
std::string nextChar = fullMsg.substr(i, charLen);
std::string testLine = currentLine + nextChar;
if (display->getStringWidth(testLine.c_str()) > windowWidth) {
cachedLines.push_back(currentLine);
currentLine = nextChar;
} else {
currentLine = testLine;
}
i += charLen;
}
if (!currentLine.empty())
cachedLines.push_back(currentLine);
cachedHeights = calculateLineHeights(cachedLines, emotes);
int yOffset = windowY;
int linesDrawn = 0;
for (size_t i = 0; i < cachedLines.size(); ++i) {
if (linesDrawn >= 2)
break;
int lineHeight = cachedHeights[i];
if (yOffset + lineHeight > windowY + windowHeight)
break;
drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes);
yOffset += lineHeight;
linesDrawn++;
}
screen->forceDisplay();
#else
uint32_t now = millis(); uint32_t now = millis();
#ifndef EXCLUDE_EMOJI #ifndef EXCLUDE_EMOJI
// === Bounce animation setup === // === Bounce animation setup ===
@@ -422,7 +355,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// Draw header at the end to sort out overlapping elements // Draw header at the end to sort out overlapping elements
graphics::drawCommonHeader(display, x, y, titleStr); graphics::drawCommonHeader(display, x, y, titleStr);
#endif
} }
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)

View File

@@ -21,10 +21,6 @@ extern bool haveGlyphs(const char *str);
// Global screen instance // Global screen instance
extern graphics::Screen *screen; extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0;
#else
#endif
namespace graphics namespace graphics
{ {
namespace NodeListRenderer namespace NodeListRenderer
@@ -55,32 +51,26 @@ static int scrollIndex = 0;
const char *getSafeNodeName(meshtastic_NodeInfoLite *node) const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
{ {
const char *name = NULL;
static char nodeName[16] = "?"; static char nodeName[16] = "?";
if (config.display.use_long_node_name == true) { if (node->has_user && strlen(node->user.short_name) > 0) {
if (node->has_user && strlen(node->user.long_name) > 0) { bool valid = true;
name = node->user.long_name; const char *name = node->user.short_name;
for (size_t i = 0; i < strlen(name); i++) {
uint8_t c = (uint8_t)name[i];
if (c < 32 || c > 126) {
valid = false;
break;
}
}
if (valid) {
strncpy(nodeName, name, sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
} else { } else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
} }
} else {
if (node->has_user && strlen(node->user.short_name) > 0) {
name = node->user.short_name;
} else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
}
}
// Use sanitizeString() function and copy directly into nodeName
std::string sanitized_name = sanitizeString(name ? name : "");
if (!sanitized_name.empty()) {
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
nodeName[sizeof(nodeName) - 1] = '\0';
} else { } else {
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF)); snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
} }
return nodeName; return nodeName;
} }
@@ -403,11 +393,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
{ {
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int rowYOffset = FONT_HEIGHT_SMALL - 3;
#if defined(M5STACK_UNITC6L)
int columnWidth = display->getWidth();
#else
int columnWidth = display->getWidth() / 2; int columnWidth = display->getWidth() / 2;
#endif
display->clear(); display->clear();
// Draw the battery/time header // Draw the battery/time header
@@ -420,11 +408,8 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
int visibleNodeRows = totalRowsAvailable; int visibleNodeRows = totalRowsAvailable;
#if defined(M5STACK_UNITC6L)
int totalColumns = 1;
#else
int totalColumns = 2; int totalColumns = 2;
#endif
int startIndex = scrollIndex * visibleNodeRows * totalColumns; int startIndex = scrollIndex * visibleNodeRows * totalColumns;
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
startIndex++; // skip own node startIndex++; // skip own node
@@ -460,14 +445,12 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
} }
} }
#if !defined(M5STACK_UNITC6L)
// Draw column separator // Draw column separator
if (shownCount > 0) { if (shownCount > 0) {
const int firstNodeY = y + 3; const int firstNodeY = y + 3;
drawColumnSeparator(display, x, firstNodeY, lastNodeY); drawColumnSeparator(display, x, firstNodeY, lastNodeY);
} }
#endif
const int scrollStartY = y + 3; const int scrollStartY = y + 3;
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
} }
@@ -485,13 +468,6 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
unsigned long now = millis(); unsigned long now = millis();
#if defined(M5STACK_UNITC6L)
display->clear();
if (now - lastSwitchTime >= 3000) {
display->display();
lastSwitchTime = now;
}
#endif
// On very first call (on boot or state enter) // On very first call (on boot or state enter)
if (lastRenderedMode == MODE_COUNT) { if (lastRenderedMode == MODE_COUNT) {
currentMode = MODE_LAST_HEARD; currentMode = MODE_LAST_HEARD;
@@ -546,14 +522,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
double lat = DegD(ourNode->position.latitude_i); double lat = DegD(ourNode->position.latitude_i);
double lon = DegD(ourNode->position.longitude_i); double lon = DegD(ourNode->position.longitude_i);
#if defined(M5STACK_UNITC6L)
display->clear();
uint32_t now = millis();
if (now - lastSwitchTime >= 2000) {
display->display();
lastSwitchTime = now;
}
#endif
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) { if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
#if HAS_GPS #if HAS_GPS
if (screen->hasHeading()) { if (screen->hasHeading()) {

View File

@@ -7,18 +7,10 @@
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#if HAS_BUTTON
#include "input/ButtonThread.h"
#endif
#include "main.h" #include "main.h"
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include <vector> #include <vector>
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
#include "esp_task_wdt.h" #include "esp_task_wdt.h"
@@ -26,11 +18,6 @@
using namespace meshtastic; using namespace meshtastic;
#if HAS_BUTTON
// Global button thread pointer defined in main.cpp
extern ::ButtonThread *UserButtonThread;
#endif
// External references to global variables from Screen.cpp // External references to global variables from Screen.cpp
extern std::vector<std::string> functionSymbol; extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString; extern std::string functionSymbolString;
@@ -51,8 +38,6 @@ bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0; uint32_t NotificationRenderer::currentNumber = 0;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
uint32_t pow_of_10(uint32_t n) uint32_t pow_of_10(uint32_t n)
{ {
@@ -104,33 +89,14 @@ void NotificationRenderer::resetBanner()
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
{ {
// Handle text_input notifications first - they have their own timeout/banner logic if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
if (current_notification_type == notificationTypeEnum::text_input) {
// Check for timeout and reset if needed for text input
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner();
return;
}
drawTextInput(display, state);
return;
}
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner(); resetBanner();
} if (!isOverlayBannerShowing() || pauseBanner)
// Exit if no banner is showing or banner is paused
if (!isOverlayBannerShowing() || pauseBanner) {
return; return;
}
switch (current_notification_type) { switch (current_notification_type) {
case notificationTypeEnum::none: case notificationTypeEnum::none:
// Do nothing - no notification to display // Do nothing - no notification to display
break; break;
case notificationTypeEnum::text_input:
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
break;
case notificationTypeEnum::text_banner: case notificationTypeEnum::text_banner:
case notificationTypeEnum::selection_picker: case notificationTypeEnum::selection_picker:
drawAlertBannerOverlay(display, state); drawAlertBannerOverlay(display, state);
@@ -250,11 +216,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
// Handle input // Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum); alertBannerCallback(selectedNodenum);
@@ -303,9 +267,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else { } else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
} }
// make temp buffer for name
// fi
if (i == curSelected) { if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) { if (isHighResolution) {
@@ -319,8 +286,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
scratchLineBuffer[scratchLineNum][39] = '\0'; scratchLineBuffer[scratchLineNum][39] = '\0';
} else { } else {
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
scratchLineBuffer[scratchLineNum][39] = '\0';
} }
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
} }
@@ -367,11 +333,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input // Handle input
if (alertBannerOptions > 0) { if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT || if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--; curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT || } else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++; curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) { if (optionsEnumPtr != nullptr) {
@@ -495,135 +459,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
// count lines // count lines
uint16_t boxWidth = hPadding * 2 + maxWidth; uint16_t boxWidth = hPadding * 2 + maxWidth;
#if defined(M5STACK_UNITC6L)
if (needs_bell) {
if (isHighResolution && boxWidth <= 150)
boxWidth += 26;
if (!isHighResolution && boxWidth <= 100)
boxWidth += 20;
}
uint16_t screenHeight = display->height();
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
uint16_t boxHeight = contentHeight + vPadding * 2;
if (visibleTotalLines == 1)
boxHeight += (isHighResolution ? 4 : 3);
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
if (totalLines > visibleTotalLines)
boxWidth += (isHighResolution ? 4 : 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
if (visibleTotalLines == 1) {
boxTop += 25;
}
if (alertBannerOptions < 3) {
int missingLines = 3 - alertBannerOptions;
int moveUp = missingLines * (effectiveLineHeight / 2);
boxTop -= moveUp;
if (boxTop < 0)
boxTop = 0;
}
// === Draw Box ===
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
display->setColor(WHITE);
int16_t lineY = boxTop + vPadding;
int swingRange = 8;
static int swingOffset = 0;
static bool swingRight = true;
static unsigned long lastSwingTime = 0;
unsigned long now = millis();
int swingSpeedMs = 10 / (swingRange * 2);
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
lastSwingTime = now;
if (swingRight) {
swingOffset++;
if (swingOffset >= swingRange)
swingRight = false;
} else {
swingOffset--;
if (swingOffset <= 0)
swingRight = true;
}
}
for (int i = 0; i < lineCount; i++) {
bool isTitle = (i == 0);
int globalOptionIndex = (i - 1) + firstOptionToShow;
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
uint16_t visibleWidth = 64 - hPadding * 2;
if (totalLines > visibleTotalLines)
visibleWidth -= 6;
char lineBuffer[lineLengths[i] + 1];
strncpy(lineBuffer, lines[i], lineLengths[i]);
lineBuffer[lineLengths[i]] = '\0';
if (isTitle) {
if (visibleTotalLines == 1) {
display->setColor(BLACK);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
} else {
display->setColor(WHITE);
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
display->setColor(WHITE);
if (needs_bell) {
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
}
}
lineY = boxTop + effectiveLineHeight + 1;
} else if (isSelectedOption) {
display->setColor(WHITE);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(BLACK);
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
int textX = boxLeft + hPadding + swingOffset;
display->drawString(textX, lineY - 1, lineBuffer);
} else {
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
}
display->setColor(WHITE);
lineY += effectiveLineHeight;
} else {
display->setColor(BLACK);
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
display->setColor(WHITE);
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
lineY += effectiveLineHeight;
}
}
if (totalLines > visibleTotalLines) {
const uint8_t scrollBarWidth = 5;
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
float ratio = (float)visibleTotalLines / totalLines;
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
}
#else
if (needs_bell) { if (needs_bell) {
if (isHighResolution && boxWidth <= 150) if (isHighResolution && boxWidth <= 150)
boxWidth += 26; boxWidth += 26;
@@ -712,7 +547,6 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
} }
#endif
} }
/// Draw the last text message we received /// Draw the last text message we received
@@ -741,99 +575,6 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
"Please be patient and do not power off."); "Please be patient and do not power off.");
} }
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (virtualKeyboard) {
// Check for timeout and auto-exit if needed
if (virtualKeyboard->isTimedOut()) {
LOG_INFO("Virtual keyboard timeout - auto-exiting");
// Cancel virtual keyboard - call callback with empty string to indicate timeout
auto callback = textInputCallback; // Store callback before clearing
// Clean up first to prevent re-entry
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
// Call callback after cleanup
if (callback) {
callback("");
}
// Restore normal overlays
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
if (inEvent.inputEvent == INPUT_BROKER_UP) {
// high frequency for move cursor left/right than up/down with encoders
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorLeft();
} else {
virtualKeyboard->moveCursorUp();
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorRight();
} else {
virtualKeyboard->moveCursorDown();
}
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
virtualKeyboard->moveCursorUp();
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
virtualKeyboard->moveCursorDown();
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
virtualKeyboard->handlePress();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
virtualKeyboard->handleLongPress();
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback;
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
if (callback) {
callback("");
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
// Consume the event after processing for virtual keyboard
inEvent.inputEvent = INPUT_BROKER_NONE;
}
// Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
// Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
resetBanner();
}
}
bool NotificationRenderer::isOverlayBannerShowing() bool NotificationRenderer::isOverlayBannerShowing()
{ {
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);

View File

@@ -3,9 +3,6 @@
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h"
#include <functional>
#include <string>
#define MAX_LINES 5 #define MAX_LINES 5
namespace graphics namespace graphics
@@ -25,8 +22,6 @@ class NotificationRenderer
static std::function<void(int)> alertBannerCallback; static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits; static uint32_t numDigits;
static uint32_t currentNumber; static uint32_t currentNumber;
static VirtualKeyboard *virtualKeyboard;
static std::function<void(const std::string &)> textInputCallback;
static bool pauseBanner; static bool pauseBanner;
@@ -35,7 +30,6 @@ class NotificationRenderer
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);

View File

@@ -20,9 +20,7 @@
// External variables // External variables
extern graphics::Screen *screen; extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0;
#endif
namespace graphics namespace graphics
{ {
NodeNum UIRenderer::currentFavoriteNodeNum = 0; NodeNum UIRenderer::currentFavoriteNodeNum = 0;
@@ -118,124 +116,64 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con
} }
// Draw GPS status coordinates // Draw GPS status coordinates
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps, void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
const char *mode)
{ {
auto gpsFormat = uiconfig.gps_format; auto gpsFormat = config.display.gps_format;
char displayLine[32]; char displayLine[32];
if (!gps->getIsConnected() && !config.position.fixed_position) { if (!gps->getIsConnected() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) { strcpy(displayLine, "No GPS present");
strcpy(displayLine, "No GPS present"); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
display->drawString(x, y, displayLine);
}
} else if (!gps->getHasLock() && !config.position.fixed_position) { } else if (!gps->getHasLock() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) { strcpy(displayLine, "No GPS Lock");
strcpy(displayLine, "No GPS Lock"); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
display->drawString(x, y, displayLine);
}
} else { } else {
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) { if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) {
char coordinateLine_1[22]; char coordinateLine[22];
char coordinateLine_2[22]; if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7,
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7); geoCoord.getLongitude() * 1e-7);
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(),
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(), geoCoord.getUTMEasting(), geoCoord.getUTMNorthing());
geoCoord.getUTMBand(), geoCoord.getUTMEasting()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing()); snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(),
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(),
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(), geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing());
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(), geoCoord.getOLCCode(coordinateLine);
geoCoord.getMGRSNorthing()); } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region
geoCoord.getOLCCode(coordinateLine_1); snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary");
coordinateLine_2[0] = '\0'; else
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(),
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing());
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary"); }
coordinateLine_2[0] = '\0';
// If fixed position, display text "Fixed GPS" alternating with the coordinates.
if (config.position.fixed_position) {
if ((millis() / 10000) % 2) {
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y,
coordinateLine);
} else { } else {
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(), display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS");
geoCoord.getOSGRN100k());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(),
geoCoord.getOSGRNorthing());
} }
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System } else {
double lat = geoCoord.getLatitude() * 1e-7; display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine);
double lon = geoCoord.getLongitude() * 1e-7;
// Normalize
if (lat > 90.0)
lat = 90.0;
if (lat < -90.0)
lat = -90.0;
while (lon < -180.0)
lon += 360.0;
while (lon >= 180.0)
lon -= 360.0;
double adjLon = lon + 180.0;
double adjLat = lat + 90.0;
char maiden[10]; // enough for 8-char + null
// Field (2 letters)
int lonField = int(adjLon / 20.0);
int latField = int(adjLat / 10.0);
adjLon -= lonField * 20.0;
adjLat -= latField * 10.0;
// Square (2 digits)
int lonSquare = int(adjLon / 2.0);
int latSquare = int(adjLat / 1.0);
adjLon -= lonSquare * 2.0;
adjLat -= latSquare * 1.0;
// Subsquare (2 letters)
double lonUnit = 2.0 / 24.0;
double latUnit = 1.0 / 24.0;
int lonSub = int(adjLon / lonUnit);
int latSub = int(adjLat / latUnit);
snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare,
'A' + lonSub, 'A' + latSub);
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden);
coordinateLine_2[0] = '\0'; // only need one line
} }
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
} else if (strcmp(mode, "combined") == 0) {
display->drawString(x, y, coordinateLine_1);
if (coordinateLine_2[0] != '\0') {
display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2);
}
}
} else { } else {
char coordinateLine_1[22]; char latLine[22];
char coordinateLine_2[22]; char lonLine[22];
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(),
geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(),
geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
if (strcmp(mode, "line1") == 0) { display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1,
display->drawString(x, y, coordinateLine_1); latLine);
} else if (strcmp(mode, "line2") == 0) { display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine);
display->drawString(x, y, coordinateLine_2);
} else { // both
display->drawString(x, y, coordinateLine_1);
display->drawString(x, y + 10, coordinateLine_2);
}
} }
} }
} }
@@ -256,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
} }
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) { if (isHighResolution) {
@@ -280,6 +218,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
// ********************** // **********************
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
if (favoritedNodes.empty()) if (favoritedNodes.empty())
return; return;
@@ -291,15 +230,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
return; return;
display->clear(); display->clear();
#if defined(M5STACK_UNITC6L)
uint32_t now = millis();
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
{
display->display();
lastSwitchTime = now;
}
#endif
currentFavoriteNodeNum = node->num; currentFavoriteNodeNum = node->num;
// === Create the shortName and title string === // === Create the shortName and title string ===
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
@@ -318,13 +250,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// List of available macro Y positions in order, from top to bottom. // List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next int line = 1; // which slot to use next
std::string usernameStr; std::string usernameStr;
// === 1. Long Name (always try to show first) ===
#if defined(M5STACK_UNITC6L)
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
#else
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
#endif
// === 1. Long Name (always try to show first) ===
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
if (username) { if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// Print node's long name (e.g. "Backpack Node") // Print node's long name (e.g. "Backpack Node")
@@ -379,7 +307,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
if (seenStr[0] && line < 5) { if (seenStr[0] && line < 5) {
display->drawString(x, getTextPositions(display)[line++], seenStr); display->drawString(x, getTextPositions(display)[line++], seenStr);
} }
#if !defined(M5STACK_UNITC6L)
// === 4. Uptime (only show if metric is present) === // === 4. Uptime (only show if metric is present) ===
char uptimeStr[32] = ""; char uptimeStr[32] = "";
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
@@ -551,7 +479,6 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
} }
// else show nothing // else show nothing
} }
#endif
} }
// **************************** // ****************************
@@ -565,11 +492,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int line = 1; int line = 1;
// === Header === // === Header ===
#if defined(M5STACK_UNITC6L)
graphics::drawCommonHeader(display, x, y, "Home");
#else
graphics::drawCommonHeader(display, x, y, ""); graphics::drawCommonHeader(display, x, y, "");
#endif
// === Content below header === // === Content below header ===
@@ -584,25 +507,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
config.display.heading_bold = false; config.display.heading_bold = false;
// Display Region and Channel Utilization // Display Region and Channel Utilization
#if defined(M5STACK_UNITC6L)
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
#else
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
#endif
char uptimeStr[32] = ""; char uptimeStr[32] = "";
uint32_t uptime = millis() / 1000; uint32_t uptime = millis() / 1000;
uint32_t days = uptime / 86400; uint32_t days = uptime / 86400;
uint32_t hours = (uptime % 86400) / 3600; uint32_t hours = (uptime % 86400) / 3600;
uint32_t mins = (uptime % 3600) / 60; uint32_t mins = (uptime % 3600) / 60;
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
#if !defined(M5STACK_UNITC6L)
if (days) if (days)
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours);
else if (hours) else if (hours)
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins);
else else
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins);
#endif
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
// === Second Row: Satellites and Voltage === // === Second Row: Satellites and Voltage ===
@@ -631,21 +549,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} }
#endif #endif
#if defined(M5STACK_UNITC6L)
line += 1;
// === Node Identity ===
int textWidth = 0;
int nameX = 0;
char shortnameble[35];
snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
// === ShortName Centered ===
textWidth = display->getStringWidth(shortnameble);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
#else
if (powerStatus->getHasBattery()) { if (powerStatus->getHasBattery()) {
char batStr[20]; char batStr[20];
int batV = powerStatus->getBatteryVoltageMv() / 1000; int batV = powerStatus->getBatteryVoltageMv() / 1000;
@@ -738,6 +641,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0; int textWidth = 0;
int nameX = 0; int nameX = 0;
int yOffset = (isHighResolution) ? 0 : 5; int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
std::string longNameStr; std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -770,7 +674,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
nameX = (SCREEN_WIDTH - textWidth) / 2; nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], shortnameble); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
} }
#endif
} }
// Start Functions to write date/time to the screen // Start Functions to write date/time to the screen
@@ -929,28 +832,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
// needs to be drawn relative to x and y // needs to be drawn relative to x and y
// draw centered icon left to right and centered above the one line of app text // draw centered icon left to right and centered above the one line of app text
#if defined(M5STACK_UNITC6L)
display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits);
display->setFont(FONT_MEDIUM);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
// Draw region in upper left
if (upperMsg) {
int msgWidth = display->getStringWidth(upperMsg);
int msgX = x + (SCREEN_WIDTH - msgWidth) / 2;
int msgY = y;
display->drawString(msgX, msgY, upperMsg);
}
// Draw version and short name in bottom middle
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
#else
display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2,
icon_width, icon_height, icon_bits); icon_width, icon_height, icon_bits);
@@ -959,6 +840,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
const char *title = "meshtastic.org"; const char *title = "meshtastic.org";
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
// Draw region in upper left // Draw region in upper left
if (upperMsg) if (upperMsg)
display->drawString(x + 0, y + 0, upperMsg); display->drawString(x + 0, y + 0, upperMsg);
@@ -973,7 +855,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
screen->forceDisplay(); screen->forceDisplay();
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
#endif
} }
// **************************** // ****************************
@@ -998,26 +879,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false; config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
if (usePhoneGPS) {
// Phone-provided GPS is active
displayLine = "Phone GPS";
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// GPS disabled / not present
if (config.position.fixed_position) { if (config.position.fixed_position) {
displayLine = "Fixed GPS"; displayLine = "Fixed GPS";
} else { } else {
@@ -1034,7 +896,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
int xOffset = (isHighResolution) ? 6 : 0; int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else { } else {
// Onboard GPS
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
} }
@@ -1061,64 +922,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// If GPS is off, no need to display these parts // If GPS is off, no need to display these parts
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
// === Second Row: Last GPS Fix ===
if (gpsStatus->getLastFixMillis() > 0) {
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
uint32_t days = delta / 86400;
uint32_t hours = (delta % 86400) / 3600;
uint32_t mins = (delta % 3600) / 60;
uint32_t secs = delta % 60;
char buf[32]; // === Second Row: Date ===
#if defined(USE_EINK) uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
// E-Ink: skip seconds, show only days/hours/mins char datetimeStr[25];
if (days > 0) { bool showTime = false; // set to true for full datetime
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
} else if (hours > 0) { char fullLine[40];
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
} else { display->drawString(0, getTextPositions(display)[line++], fullLine);
snprintf(buf, sizeof(buf), "Last: %um", mins);
}
#else
// Non E-Ink: include seconds where useful
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else if (mins > 0) {
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
} else {
snprintf(buf, sizeof(buf), "Last: %us", secs);
}
#endif
display->drawString(0, getTextPositions(display)[line++], buf); // === Third Row: Latitude ===
} else { char latStr[32];
display->drawString(0, getTextPositions(display)[line++], "Last: ?"); snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
} display->drawString(x, getTextPositions(display)[line++], latStr);
// === Third Row: Line 1 GPS Info === // === Fourth Row: Longitude ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1"); char lonStr[32];
snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++], lonStr);
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC && // === Fifth Row: Altitude ===
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { char DisplayLineTwo[32] = {0};
// === Fourth Row: Line 2 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
}
// === Final Row: Altitude ===
char altitudeLine[32] = {0};
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
? ourNode->position.altitude
: geoCoord.getAltitude();
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET); snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
} else { } else {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt); snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
} }
display->drawString(x, getTextPositions(display)[line++], altitudeLine); display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
} }
#if !defined(M5STACK_UNITC6L)
// === Draw Compass if heading is valid === // === Draw Compass if heading is valid ===
if (validHeading) { if (validHeading) {
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- // --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
@@ -1201,7 +1034,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} }
} }
#endif #endif
#endif // HAS_GPS
} }
#ifdef USERPREFS_OEM_TEXT #ifdef USERPREFS_OEM_TEXT
@@ -1294,13 +1126,14 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2; const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
#if defined(USE_EINK) #if defined(USE_EINK)
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
static bool navBarPrevVisible = false; static bool navBarPrevVisible = false;
if (navBarVisible && !navBarPrevVisible) { if (navBarVisible && !navBarPrevVisible) {
@@ -1357,6 +1190,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(WHITE); display->setColor(WHITE);
} }
} }
// Knock the corners off the square // Knock the corners off the square
display->setColor(BLACK); display->setColor(BLACK);
display->drawRect(rectX, y - 2, 1, 1); display->drawRect(rectX, y - 2, 1, 1);

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