Compare commits

..

14 Commits

Author SHA1 Message Date
Thomas Göttgens
45ecd139e5 Merge branch 'master' into indicator-comms 2025-07-06 14:38:50 +02:00
Thomas Göttgens
79db7a5208 WIP: GPS works 2025-07-04 00:24:33 +02:00
Thomas Göttgens
7289b2a972 Update src/main.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 00:15:52 +02:00
Thomas Göttgens
9356be3f8f Update src/mesh/comms/FakeI2C.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 00:15:32 +02:00
Thomas Göttgens
cce1b050c8 Update src/mesh/comms/FakeUART.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 00:15:24 +02:00
Thomas Göttgens
fd5ff679f3 WIP 2025-07-02 23:41:51 +02:00
Thomas Göttgens
7a7ef5f0c9 Merge branch 'master' into indicator-comms 2025-07-02 17:21:23 +02:00
Thomas Göttgens
f084a8a11d Merge branch 'master' into indicator-comms 2025-06-05 14:35:04 +02:00
Thomas Göttgens
37857941bf Merge branch 'master' into indicator-comms 2025-05-23 15:53:16 +02:00
Thomas Göttgens
d544b41ab7 Merge branch 'master' into indicator-comms 2025-03-31 11:09:44 +02:00
Thomas Göttgens
8c53ce82f2 don't build FakeUART on other platforms 2025-03-04 11:11:25 +01:00
Thomas Göttgens
7ee95f2a0c Merge branch 'indicator-comms' of github.com:meshtastic/firmware into indicator-comms 2025-03-04 10:39:35 +01:00
Thomas Göttgens
65b50babee get sensor and NMEA data from indicator. Will test tomorrow, don't merge yet. 2025-03-04 01:56:56 +01:00
Thomas Göttgens
3c30821337 get sensor and NMEA data from indicator. Will test tomorrow, don't merge yet. 2025-03-04 01:53:18 +01:00
705 changed files with 4412 additions and 16635 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

@@ -5,7 +5,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
@@ -23,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

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
path: meshtasticd path: meshtasticd

37
.github/workflows/build_esp32.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build ESP32
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-esp32:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ESP32
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
remove-debug-flags: >-
./arch/esp32/esp32.ini
./arch/esp32/esp32s2.ini
./arch/esp32/esp32s3.ini
./arch/esp32/esp32c3.ini
./arch/esp32/esp32c6.ini
build-script-path: bin/build-esp32.sh
ota-firmware-source: firmware.bin
ota-firmware-target: release/bleota.bin
artifact-paths: |
release/*.bin
release/*.elf
#include-web-ui: true
arch: esp32

37
.github/workflows/build_esp32_c3.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build ESP32-C3
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-esp32-c3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ESP32-C3
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
remove-debug-flags: >-
./arch/esp32/esp32.ini
./arch/esp32/esp32s2.ini
./arch/esp32/esp32s3.ini
./arch/esp32/esp32c3.ini
./arch/esp32/esp32c6.ini
build-script-path: bin/build-esp32.sh
ota-firmware-source: firmware-c3.bin
ota-firmware-target: release/bleota-c3.bin
artifact-paths: |
release/*.bin
release/*.elf
#include-web-ui: true
arch: esp32c3

37
.github/workflows/build_esp32_c6.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build ESP32-C6
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-esp32-c6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ESP32-C6
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
remove-debug-flags: >-
./arch/esp32/esp32.ini
./arch/esp32/esp32s2.ini
./arch/esp32/esp32s3.ini
./arch/esp32/esp32c3.ini
./arch/esp32/esp32c6.ini
build-script-path: bin/build-esp32.sh
ota-firmware-source: firmware-c3.bin
ota-firmware-target: release/bleota-c3.bin
artifact-paths: |
release/*.bin
release/*.elf
#include-web-ui: true
arch: esp32c6

37
.github/workflows/build_esp32_s3.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build ESP32-S3
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-esp32-s3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ESP32-S3
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
remove-debug-flags: >-
./arch/esp32/esp32.ini
./arch/esp32/esp32s2.ini
./arch/esp32/esp32s3.ini
./arch/esp32/esp32c3.ini
./arch/esp32/esp32c6.ini
build-script-path: bin/build-esp32.sh
ota-firmware-source: firmware-s3.bin
ota-firmware-target: release/bleota-s3.bin
artifact-paths: |
release/*.bin
release/*.elf
#include-web-ui: true
arch: esp32s3

View File

@@ -1,66 +0,0 @@
name: Build
on:
workflow_call:
inputs:
version:
required: true
type: string
platform:
required: true
type: string
pio_env:
required: true
type: string
permissions: read-all
jobs:
pio-build:
name: build-${{ inputs.platform }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Set OTA firmware source and target
if: startsWith(inputs.platform, 'esp32')
id: ota_dir
env:
PIO_PLATFORM: ${{ inputs.platform }}
run: |
if [ "$PIO_PLATFORM" = "esp32s3" ]; then
echo "src=firmware-s3.bin" >> $GITHUB_OUTPUT
echo "tgt=release/bleota-s3.bin" >> $GITHUB_OUTPUT
elif [ "$PIO_PLATFORM" = "esp32c3" ] || [ "$PIO_PLATFORM" = "esp32c6" ]; then
echo "src=firmware-c3.bin" >> $GITHUB_OUTPUT
echo "tgt=release/bleota-c3.bin" >> $GITHUB_OUTPUT
elif [ "$PIO_PLATFORM" = "esp32" ]; then
echo "src=firmware.bin" >> $GITHUB_OUTPUT
echo "tgt=release/bleota.bin" >> $GITHUB_OUTPUT
fi
- name: Build ${{ inputs.platform }}
id: build
uses: meshtastic/gh-action-firmware@main
with:
pio_platform: ${{ inputs.platform }}
pio_env: ${{ inputs.pio_env }}
pio_target: build
ota_firmware_source: ${{ steps.ota_dir.outputs.src || '' }}
ota_firmware_target: ${{ steps.ota_dir.outputs.tgt || '' }}
- name: Store binaries as an artifact
uses: actions/upload-artifact@v4
with:
name: firmware-${{ inputs.platform }}-${{ inputs.pio_env }}-${{ inputs.version }}.zip
overwrite: true
path: |
release/*.bin
release/*.elf
release/*.uf2
release/*.hex
release/*-ota.zip

30
.github/workflows/build_nrf52.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Build NRF52
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-nrf52:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build NRF52
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
build-script-path: bin/build-nrf52.sh
artifact-paths: |
release/*.hex
release/*.uf2
release/*.elf
release/*.zip
arch: nrf52840

28
.github/workflows/build_rpi2040.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build RPI2040
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-rpi2040:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Raspberry Pi 2040
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
build-script-path: bin/build-rpi2040.sh
artifact-paths: |
release/*.uf2
release/*.elf
arch: rp2040

29
.github/workflows/build_stm32.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Build STM32
on:
workflow_call:
inputs:
board:
required: true
type: string
permissions: read-all
jobs:
build-stm32:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build STM32WL
id: build
uses: ./.github/actions/build-variant
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
board: ${{ inputs.board }}
build-script-path: bin/build-stm32.sh
artifact-paths: |
release/*.hex
release/*.bin
release/*.elf
arch: stm32

View File

@@ -30,11 +30,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
series: series: [plucky, oracular, noble, jammy]
- jammy # 22.04
- noble # 24.04
- plucky # 25.04
- questing # 25.10
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
with: with:
ppa_repo: ppa:meshtastic/daily ppa_repo: ppa:meshtastic/daily

View File

@@ -47,7 +47,7 @@ jobs:
runs-on: ${{ inputs.runs-on }} runs-on: ${{ inputs.runs-on }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}

View File

@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
ref: ${{ github.ref }} ref: ${{ github.ref }}

View File

@@ -30,35 +30,18 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32, check]
- esp32 runs-on: ubuntu-latest
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- check
runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v5 - id: checkout
- uses: actions/setup-python@v6 uses: actions/checkout@v4
with: name: Checkout base
python-version: 3.x - id: jsonStep
cache: pip
- run: pip install -U platformio
- name: Uncomment build epoch
shell: bash
run: |
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: Generate matrix
id: jsonStep
run: | run: |
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}} pr) TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} quick)
fi fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS" echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF Targets: $TARGETS"
echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT echo "${{matrix.arch}}=$(jq -cn --argjson environments "$TARGETS" '{board: $environments}')" >> $GITHUB_OUTPUT
@@ -69,25 +52,9 @@ jobs:
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }} esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }} nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }} rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }} stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }} 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: check:
needs: setup needs: setup
strategy: strategy:
@@ -97,7 +64,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }} if: ${{ github.event_name != 'workflow_dispatch' }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Build base - name: Build base
id: base id: base
uses: ./.github/actions/setup-base uses: ./.github/actions/setup-base
@@ -105,95 +72,69 @@ jobs:
run: bin/check-all.sh ${{ matrix.board }} run: bin/check-all.sh ${{ matrix.board }}
build-esp32: build-esp32:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32) }} matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_esp32.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: esp32
build-esp32s3: build-esp32-s3:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }} matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_esp32_s3.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: esp32s3
build-esp32c3: build-esp32-c3:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }} matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_esp32_c3.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: esp32c3
build-esp32c6: build-esp32-c6:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }} matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_esp32_c6.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: esp32c6
build-nrf52840: build-nrf52:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }} matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_nrf52.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: nrf52840
build-rp2040: build-rpi2040:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }} matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_rpi2040.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
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: build-stm32:
needs: [setup, version] needs: setup
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.stm32) }} matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
uses: ./.github/workflows/build_firmware.yml uses: ./.github/workflows/build_stm32.yml
with: with:
version: ${{ needs.version.outputs.long }} board: ${{ matrix.board }}
pio_env: ${{ matrix.board }}
platform: stm32
build-debian-src: build-debian-src:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/build_debian_src.yml uses: ./.github/workflows/build_debian_src.yml
with: with:
series: UNRELEASED series: UNRELEASED
@@ -268,36 +209,26 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
[ [
version,
build-esp32, build-esp32,
build-esp32s3, build-esp32-s3,
build-esp32c3, build-esp32-c3,
build-esp32c6, build-esp32-c6,
build-nrf52840, build-nrf52,
build-rp2040, build-rpi2040,
build-rp2350,
build-stm32, build-stm32,
] ]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
path: ./ path: ./
pattern: firmware-${{matrix.arch}}-* pattern: firmware-${{matrix.arch}}-*
@@ -306,13 +237,17 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R run: ls -R
- name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- name: Move files up - name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip - name: Repackage in single firmware zip
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
overwrite: true overwrite: true
path: | path: |
./firmware-*.bin ./firmware-*.bin
@@ -326,9 +261,9 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2 ./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30 retention-days: 30
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./output path: ./output
@@ -342,12 +277,12 @@ jobs:
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip - name: Repackage in single elfs zip
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
overwrite: true overwrite: true
path: ./*.elf path: ./*.elf
retention-days: 30 retention-days: 30
@@ -355,8 +290,8 @@ jobs:
- uses: scruplelesswizard/comment-artifact@main - uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
with: with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} name: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation" description: "Download firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
release-artifacts: release-artifacts:
@@ -365,49 +300,56 @@ jobs:
outputs: outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
needs: needs:
- version
- gather-artifacts - gather-artifacts
- build-debian-src - build-debian-src
- package-pio-deps-native-tft - package-pio-deps-native-tft
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- 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
- 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
- name: Create release - name: Create release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
id: create_release id: create_release
with: with:
draft: true draft: true
prerelease: true prerelease: true
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha name: Meshtastic Firmware ${{ steps.version.outputs.long }} Alpha
tag_name: v${{ needs.version.outputs.long }} tag_name: v${{ steps.version.outputs.long }}
body: | body: |
Autogenerated by github action, developer should edit as required before publishing... Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb - name: Download source deb
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src pattern: firmware-debian-${{ steps.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true merge-multiple: true
path: ./output/debian-src path: ./output/debian-src
- name: Download `native-tft` pio deps - name: Download `native-tft` pio deps
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }} pattern: platformio-deps-native-tft-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./output/pio-deps-native-tft path: ./output/pio-deps-native-tft
- name: Zip Linux sources - name: Zip Linux sources
working-directory: output working-directory: output
run: | run: |
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src zip -j -9 -r ./meshtasticd-${{ steps.version.outputs.deb }}-src.zip ./debian-src
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft zip -9 -r ./platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip ./pio-deps-native-tft
# For diagnostics # For diagnostics
- name: Display structure of downloaded files - name: Display structure of downloaded files
@@ -417,8 +359,8 @@ jobs:
# Only run when targeting master branch with workflow_dispatch # Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
run: | run: |
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip gh release upload v${{ steps.version.outputs.long }} ./output/meshtasticd-${{ steps.version.outputs.deb }}-src.zip
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip gh release upload v${{ steps.version.outputs.long }} ./output/platformio-deps-native-tft-${{ steps.version.outputs.long }}.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -426,30 +368,26 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: arch: [esp32, esp32s3, esp32c3, esp32c6, nrf52840, rp2040, stm32]
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version] needs: [release-artifacts]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- 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
- uses: actions/download-artifact@v5 - name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- uses: actions/download-artifact@v4
with: with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }} pattern: firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./output path: ./output
@@ -462,16 +400,16 @@ jobs:
chmod +x ./output/device-update.sh chmod +x ./output/device-update.sh
- name: Zip firmware - name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v5 - uses: actions/download-artifact@v4
with: with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip name: debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
path: ./elfs path: ./elfs
- name: Zip debug elfs - name: Zip debug elfs
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip ./elfs
# For diagnostics # For diagnostics
- name: Display structure of downloaded files - name: Display structure of downloaded files
@@ -481,30 +419,33 @@ jobs:
# Only run when targeting master branch with workflow_dispatch # Only run when targeting master branch with workflow_dispatch
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
run: | run: |
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip gh release upload v${{ steps.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip gh release upload v${{ steps.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ steps.version.outputs.long }}.zip
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-firmware: publish-firmware:
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-firmware, version] needs: [release-firmware]
env: env:
targets: |- targets: esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,stm32
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- 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
- uses: actions/download-artifact@v5 - name: Get release version string
run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
id: version
- uses: actions/download-artifact@v4
with: with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }} pattern: firmware-{${{ env.targets }}}-${{ steps.version.outputs.long }}
merge-multiple: true merge-multiple: true
path: ./publish path: ./publish
@@ -518,9 +459,9 @@ jobs:
external_repository: meshtastic/meshtastic.github.io external_repository: meshtastic/meshtastic.github.io
publish_branch: master publish_branch: master
publish_dir: ./publish publish_dir: ./publish
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }} destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ steps.version.outputs.long }}
keep_files: true keep_files: true
user_name: github-actions[bot] user_name: github-actions[bot]
user_email: github-actions[bot]@users.noreply.github.com user_email: github-actions[bot]@users.noreply.github.com
commit_message: ${{ needs.version.outputs.long }} commit_message: ${{ steps.version.outputs.long }}
enable_jekyll: true enable_jekyll: true

View File

@@ -8,13 +8,12 @@ permissions: read-all
jobs: jobs:
trunk_check: trunk_check:
if: github.repository == 'meshtastic/firmware'
name: Trunk Check and Upload name: Trunk Check and Upload
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Trunk Check - name: Trunk Check
uses: trunk-io/trunk-action@v1 uses: trunk-io/trunk-action@v1
@@ -22,7 +21,6 @@ jobs:
trunk-token: ${{ secrets.TRUNK_TOKEN }} trunk-token: ${{ secrets.TRUNK_TOKEN }}
trunk_upgrade: trunk_upgrade:
if: github.repository == 'meshtastic/firmware'
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades # See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
name: Trunk Upgrade (PR) name: Trunk Upgrade (PR)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -31,7 +29,7 @@ jobs:
pull-requests: write # For trunk to create PRs pull-requests: write # For trunk to create PRs
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Trunk Upgrade - name: Trunk Upgrade
uses: trunk-io/trunk-action/upgrade@v1 uses: trunk-io/trunk-action/upgrade@v1

View File

@@ -34,7 +34,7 @@ jobs:
needs: build-debian-src needs: build-debian-src
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
path: meshtasticd path: meshtasticd
@@ -58,7 +58,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true

View File

@@ -24,14 +24,14 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
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: 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

@@ -32,7 +32,7 @@ jobs:
needs: build-debian-src needs: build-debian-src
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
path: meshtasticd path: meshtasticd
@@ -60,7 +60,7 @@ jobs:
id: version id: version
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
merge-multiple: true merge-multiple: true

View File

@@ -1,24 +0,0 @@
name: Check PR Labels
on:
pull_request:
types: [opened, edited, labeled, unlabeled, synchronize, reopened]
permissions:
pull-requests: read
contents: read
jobs:
check-label:
runs-on: ubuntu-24.04
steps:
- name: Check for PR labels
uses: actions/github-script@v8
with:
script: |
const labels = context.payload.pull_request.labels.map(label => label.name);
const requiredLabels = ['bugfix', 'enhancement', 'hardware-support', 'dependencies', 'submodules', 'github_actions', 'trunk'];
const hasRequiredLabel = labels.some(label => requiredLabels.includes(label));
if (!hasRequiredLabel) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
}

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

@@ -20,11 +20,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
series: series: [plucky, oracular, noble, jammy]
- jammy # 22.04
- noble # 24.04
- plucky # 25.04
# - questing # 25.10
uses: ./.github/workflows/package_ppa.yml uses: ./.github/workflows/package_ppa.yml
with: with:
ppa_repo: |- ppa_repo: |-
@@ -60,10 +56,10 @@ jobs:
shell: bash shell: bash
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- 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
@@ -103,9 +99,8 @@ jobs:
with: with:
base: ${{ github.event.repository.default_branch }} base: ${{ github.event.repository.default_branch }}
branch: create-pull-request/bump-version branch: create-pull-request/bump-version
labels: github_actions
title: Bump release version title: Bump release version
commit-message: Automated version bumps commit-message: automated bumps
add-paths: | add-paths: |
version.properties version.properties
debian/changelog debian/changelog

View File

@@ -13,7 +13,6 @@ permissions:
jobs: jobs:
semgrep-full: semgrep-full:
if: github.repository == 'meshtastic/firmware'
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
container: container:
image: semgrep/semgrep image: semgrep/semgrep
@@ -21,7 +20,7 @@ jobs:
steps: steps:
# step 1 # step 1
- name: clone application source code - name: clone application source code
uses: actions/checkout@v5 uses: actions/checkout@v4
# step 2 # step 2
- name: full scan - name: full scan

View File

@@ -13,7 +13,7 @@ jobs:
steps: steps:
# step 1 # step 1
- name: clone application source code - name: clone application source code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -11,13 +11,12 @@ permissions:
jobs: jobs:
stale_issues: stale_issues:
if: github.repository == 'meshtastic/firmware'
name: Close Stale Issues name: Close Stale Issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Stale PR+Issues - name: Stale PR+Issues
uses: actions/stale@v10.0.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

@@ -14,7 +14,7 @@ jobs:
name: Native Simulator Tests name: Native Simulator Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
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}}
@@ -70,7 +70,7 @@ jobs:
name: Native PlatformIO Tests name: Native PlatformIO Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
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}}
@@ -127,7 +127,7 @@ jobs:
- platformio-tests - platformio-tests
if: always() if: always()
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
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}}
@@ -137,20 +137,20 @@ jobs:
id: version id: version
- name: Download test artifacts - name: Download test artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
name: platformio-test-report-${{ steps.version.outputs.long }}.zip name: platformio-test-report-${{ steps.version.outputs.long }}.zip
merge-multiple: true merge-multiple: true
- name: Test Report - name: Test Report
uses: dorny/test-reporter@v2.1.1 uses: dorny/test-reporter@v2.1.0
with: with:
name: PlatformIO Tests name: PlatformIO Tests
path: testreport.xml path: testreport.xml
reporter: java-junit reporter: java-junit
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip pattern: lcov-coverage-info-native-*-${{ steps.version.outputs.long }}.zip
path: code-coverage-report path: code-coverage-report

View File

@@ -12,15 +12,13 @@ permissions:
jobs: jobs:
native-tests: native-tests:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/test_native.yml uses: ./.github/workflows/test_native.yml
hardware-tests: hardware-tests:
if: github.repository == 'meshtastic/firmware'
runs-on: test-runner runs-on: test-runner
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
# - uses: actions/setup-python@v5 # - uses: actions/setup-python@v5
# with: # with:
@@ -47,7 +45,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

@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Trunk Check - name: Trunk Check
uses: trunk-io/trunk-action@v1 uses: trunk-io/trunk-action@v1

View File

@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Trunk Check - name: Trunk Check
uses: trunk-io/trunk-action@v1 uses: trunk-io/trunk-action@v1

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
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}}
@@ -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

@@ -11,7 +11,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
@@ -34,9 +34,7 @@ jobs:
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:
branch: create-pull-request/update-protobufs branch: create-pull-request/update-protobufs
labels: submodules
title: Update protobufs and classes title: Update protobufs and classes
commit-message: Update protobufs
add-paths: | add-paths: |
protobufs protobufs
src/mesh src/mesh

View File

@@ -1,34 +1,34 @@
version: 0.1 version: 0.1
cli: cli:
version: 1.25.0 version: 1.24.0
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.2 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.469 - checkov@3.2.447
- renovate@41.94.0 - renovate@41.17.2
- prettier@3.6.2 - prettier@3.6.2
- trufflehog@3.90.5 - trufflehog@3.89.2
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.8.6 - bandit@1.8.5
- trivy@0.66.0 - trivy@0.64.0
- taplo@0.10.0 - taplo@0.9.3
- ruff@0.12.11 - ruff@0.12.1
- isort@6.0.1 - 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.13.1 - hadolint@2.12.1-beta
- shfmt@3.6.0 - shfmt@3.6.0
- shellcheck@0.11.0 - shellcheck@0.10.0
- black@25.1.0 - black@25.1.0
- git-diff-check - git-diff-check
- gitleaks@8.28.0 - gitleaks@8.27.2
- clang-format@16.0.3 - clang-format@16.0.3
ignore: ignore:
- linters: [ALL] - linters: [ALL]

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.13-slim-trixie AS builder FROM python:3.13-bookworm 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
@@ -36,7 +36,7 @@ RUN curl -L "https://github.com/meshtastic/web/releases/download/v$(cat /tmp/fir
##### PRODUCTION BUILD ############# ##### PRODUCTION BUILD #############
FROM debian:trixie-slim FROM debian:bookworm-slim
LABEL org.opencontainers.image.title="Meshtastic" \ LABEL org.opencontainers.image.title="Meshtastic" \
org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \ org.opencontainers.image.description="Debian Meshtastic daemon and web interface" \
org.opencontainers.image.url="https://meshtastic.org" \ org.opencontainers.image.url="https://meshtastic.org" \
@@ -51,8 +51,8 @@ ENV TZ=Etc/UTC
USER root USER root
RUN apt-get update && apt-get --no-install-recommends -y install \ RUN apt-get update && apt-get --no-install-recommends -y install \
libc-bin libc6 libgpiod3 libyaml-cpp0.8 libi2c0 libuv1t64 libusb-1.0-0-dev \ libc-bin libc6 libgpiod2 libyaml-cpp0.7 libi2c0 libuv1 libusb-1.0-0-dev \
liborcania2.3 libulfius2.7t64 libssl3t64 \ liborcania2.3 libulfius2.7 libssl3 \
libx11-6 libinput10 libxkbcommon-x11-0 \ libx11-6 libinput10 libxkbcommon-x11-0 \
&& apt-get clean && rm -rf /var/lib/apt/lists/* \ && apt-get clean && rm -rf /var/lib/apt/lists/* \
&& mkdir -p /var/lib/meshtasticd \ && mkdir -p /var/lib/meshtasticd \
@@ -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

@@ -54,8 +54,8 @@ lib_deps =
h2zero/NimBLE-Arduino@^1.4.3 h2zero/NimBLE-Arduino@^1.4.3
# 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=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
https://github.com/lewisxhe/XPowersLib/archive/v0.3.0.zip 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

@@ -23,7 +23,7 @@ build_flags =
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
build_src_filter = build_src_filter =
${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp> -<serialization/> ${arduino_base.build_src_filter} -<platform/esp32/> -<platform/stm32wl> -<nimble/> -<mesh/wifi/> -<mesh/api/> -<mesh/http/> -<modules/esp32> -<platform/rp2xx0> -<mesh/eth/> -<mesh/raspihttp>
lib_deps= lib_deps=
${arduino_base.lib_deps} ${arduino_base.lib_deps}

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/c490bcd019e0658404088a61b96e653c9da22c45.zip https://github.com/meshtastic/platform-native/archive/681ee029207e9fd040afa223df6e54074cbbe084.zip
framework = arduino framework = arduino
build_src_filter = build_src_filter =
@@ -17,6 +17,7 @@ build_src_filter =
+<mesh/raspihttp/> +<mesh/raspihttp/>
-<mesh/eth/> -<mesh/eth/>
-<modules/esp32> -<modules/esp32>
+<../variants/portduino>
lib_deps = lib_deps =
${env.lib_deps} ${env.lib_deps}
@@ -29,19 +30,14 @@ lib_deps =
lovyan03/LovyanGFX@^1.2.0 lovyan03/LovyanGFX@^1.2.0
# renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
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
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}
-D ARCH_PORTDUINO
-fPIC -fPIC
-Isrc/platform/portduino -Isrc/platform/portduino
-DRADIOLIB_EEPROM_UNSUPPORTED -DRADIOLIB_EEPROM_UNSUPPORTED
-DPORTDUINO_LINUX_HARDWARE -DPORTDUINO_LINUX_HARDWARE
-DHAS_UDP_MULTICAST=1 -DHAS_UDP_MULTICAST
-lpthread -lpthread
-lstdc++fs -lstdc++fs
-lbluetooth -lbluetooth
@@ -52,7 +48,4 @@ build_flags =
-std=gnu17 -std=gnu17
-std=c++17 -std=c++17
lib_ignore = lib_ignore = Adafruit NeoPixel
Adafruit NeoPixel
Adafruit ST7735 and ST7789 Library
SD

View File

@@ -2,7 +2,7 @@
extends = arduino_base extends = arduino_base
platform = platform =
# renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32
platformio/ststm32@19.3.0 platformio/ststm32@19.2.0
platform_packages = platform_packages =
# TODO renovate # TODO renovate
platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip
@@ -23,20 +23,14 @@ build_flags =
-DMESHTASTIC_EXCLUDE_SCREEN=1 -DMESHTASTIC_EXCLUDE_SCREEN=1
-DMESHTASTIC_EXCLUDE_MQTT=1 -DMESHTASTIC_EXCLUDE_MQTT=1
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1 -DMESHTASTIC_EXCLUDE_BLUETOOTH=1
-DMESHTASTIC_EXCLUDE_GPS=1
-DMESHTASTIC_EXCLUDE_WIFI=1 -DMESHTASTIC_EXCLUDE_WIFI=1
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space. -DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
-DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small.
-DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported.
-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized. -DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized.
-DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs. ;-DDEBUG_MUTE
-fmerge-all-constants -fmerge-all-constants
-ffunction-sections -ffunction-sections
-fdata-sections -fdata-sections
-DRADIOLIB_EXCLUDE_SX128X=1
-DRADIOLIB_EXCLUDE_SX127X=1
-DRADIOLIB_EXCLUDE_LR11X0=1
-DHAL_DAC_MODULE_ONLY
-DHAL_RNG_MODULE_ENABLED
build_src_filter = build_src_filter =
${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<mesh/api/> -<mesh/wifi/> -<mesh/http/> -<modules/esp32> -<mesh/eth/> -<input> -<buzz> -<modules/RemoteHardwareModule.cpp> -<platform/nrf52> -<platform/portduino> -<platform/rp2xx0> -<mesh/raspihttp> ${arduino_base.build_src_filter} -<platform/esp32/> -<nimble/> -<mesh/api/> -<mesh/wifi/> -<mesh/http/> -<modules/esp32> -<mesh/eth/> -<input> -<buzz> -<modules/RemoteHardwareModule.cpp> -<platform/nrf52> -<platform/portduino> -<platform/rp2xx0> -<mesh/raspihttp>
@@ -50,7 +44,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 mathertel/OneButton@2.6.1

View File

@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
platformio pkg install -e $1 platformio pkg update -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f .pio/build/$1/firmware.*

View File

@@ -11,7 +11,7 @@ elif (echo $2 | grep -q "nrf52"); then
elif (echo $2 | grep -q "stm32"); then elif (echo $2 | grep -q "stm32"); then
bin/build-stm32.sh $1 bin/build-stm32.sh $1
elif (echo $2 | grep -q "rpi2040"); then elif (echo $2 | grep -q "rpi2040"); then
bin/build-rp2xx0.sh $1 bin/build-rpi2040.sh $1
else else
echo "Unknown target $2" echo "Unknown target $2"
exit 1 exit 1

View File

@@ -25,7 +25,7 @@ mkdir -p $OUTDIR/
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
pio pkg install --environment "$PIO_ENV" || platformioFailed pio pkg update --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed pio run --environment "$PIO_ENV" || platformioFailed
cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)" cp ".pio/build/$PIO_ENV/program" "$OUTDIR/meshtasticd_linux_$(uname -m)"
cp bin/native-install.* $OUTDIR cp bin/native-install.* $OUTDIR

View File

@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
platformio pkg install -e $1 platformio pkg update -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f .pio/build/$1/firmware.*

View File

@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
platformio pkg install -e $1 platformio pkg update -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f .pio/build/$1/firmware.*

View File

@@ -11,7 +11,7 @@ rm -f $OUTDIR/firmware*
rm -r $OUTDIR/* || true rm -r $OUTDIR/* || true
# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale # Important to pull latest version of libs into all device flavors, otherwise some devices might be stale
platformio pkg install -e $1 platformio pkg update -e $1
echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS"
rm -f .pio/build/$1/firmware.* rm -f .pio/build/$1/firmware.*

View File

@@ -199,10 +199,6 @@ HostMetrics:
# UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString # UserStringCommand: cat /sys/firmware/devicetree/base/serial-number # Command to execute, to send the results as the userString
Config:
# DisplayMode: TWOCOLOR # uncomment to force BaseUI
# DisplayMode: COLOR # uncomment to force MUI
General: General:
MaxNodes: 200 MaxNodes: 200
MaxMessageQueue: 100 MaxMessageQueue: 100

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

@@ -10,3 +10,12 @@ Lora:
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

@@ -1,52 +0,0 @@
# https://www.waveshare.com/pico-lora-sx1262-868m.htm
# http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-Zero-3.html
#
# See Orange Pi Zero3 manual, chapter 3.16, page 124 for 26-pin header pinout
#
# Pin Connection
# Waveshare Orange Pi Zero3
# 36 3.3V 17
# 15 MOSI 19
# 16 MISO 21
# 14 CLK 23
# 38 GND 25
# 4 BUSY 18
# 20 RESET 22
# 5 CS 24
# 26 DIO1/IRQ 26
Lora:
Module: sx1262 # Waveshare Raspberry Pico Lora module
DIO2_AS_RF_SWITCH: true
DIO3_TCXO_VOLTAGE: true
# Specify either the spidev1_1 or the CS below, not both!
# On DietPi Linux, when using the user overlay dietpi-spi1_1.dtbo, CS will be configured with spidev1.1
spidev: spidev1.1 # See Orange Pi Zero3 manual, chapter 3.18.3, page 130
# CS: # CS PIN_24 -> chip 1, line 233
# pin: 24
# gpiochip: 1
# line: 233
SCK: # SCK PIN_23 -> chip 1, line 230
pin: 23
gpiochip: 1
line: 230
Busy: # BUSY PIN_18 -> chip 1, line 78
pin: 18
gpiochip: 1
line: 78
MOSI: # MOSI PIN_19 -> chip 1, line 231
pin: 19
gpiochip: 1
line: 231
MISO: # MISO PIN_21 -> chip 1, line 232
pin: 21
gpiochip: 1
line: 232
Reset: # NRST PIN_22 -> chip 1, line 71
pin: 22
gpiochip: 1
line: 71
IRQ: # DIO1 PIN_26 -> chip 1, line 74
pin: 26
gpiochip: 1
line: 74

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

@@ -5,12 +5,16 @@ 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=(
# Check if FILENAME contains "-tft-" and set target partitionScheme accordingly.
if [[ $FILENAME == *"-tft-"* ]]; then
TFT_BUILD=true
fi
# Extract BASENAME from %FILENAME% for later use.r-s3"
"unphone"
"seeed-sensecap-indicator"
"crowpanel-esp32s3" "crowpanel-esp32s3"
"heltec_capsule_sensor_v3" "heltec_capsule_sensor_v3"
"heltec-v3" "heltec-v3"
@@ -25,11 +29,6 @@ BIGDB_8MB=(
"tbeam-s3-core" "tbeam-s3-core"
"tracksenger" "tracksenger"
) )
MUIDB_8MB=(
"picomputer-s3"
"unphone"
"seeed-sensecap-indicator"
)
BIGDB_16MB=( BIGDB_16MB=(
"t-deck" "t-deck"
"mesh-tab" "mesh-tab"
@@ -39,9 +38,10 @@ BIGDB_16MB=(
"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 +52,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"
@@ -127,7 +126,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 +163,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 +206,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 @@
#!/bin/bash #!/bin/sh
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"
@@ -36,16 +31,17 @@ EOF
} }
# Check for --change-mode and remove it from arguments # Check for --change-mode and remove it from arguments
NEW_ARGS=() NEW_ARGS=""
for arg in "$@"; do for arg in "$@"; do
if [ "$arg" = "--change-mode" ]; then if [ "$arg" = "--change-mode" ]; then
CHANGE_MODE=true CHANGE_MODE=true
else else
NEW_ARGS+=("$arg") NEW_ARGS="$NEW_ARGS \"\$arg\""
fi fi
done done
set -- "${NEW_ARGS[@]}" # Reset positional parameters to filtered list
eval set -- $NEW_ARGS
while getopts ":hp:P:f:" opt; do while getopts ":hp:P:f:" opt; do
case "${opt}" in case "${opt}" in
@@ -69,7 +65,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 +76,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

@@ -2,11 +2,13 @@
"""Generate the CI matrix.""" """Generate the CI matrix."""
import configparser
import json import json
import os
import sys import sys
import random import random
import re
from platformio.project.config import ProjectConfig rootdir = "variants/"
options = sys.argv[1:] options = sys.argv[1:]
@@ -14,59 +16,36 @@ outlist = []
if len(options) < 1: if len(options) < 1:
print(json.dumps(outlist)) print(json.dumps(outlist))
exit(1) exit()
cfg = ProjectConfig.get_instance() for subdir, dirs, files in os.walk(rootdir):
pio_envs = cfg.envs() for file in files:
if file == "platformio.ini":
# Gather all PlatformIO environments for filtering later config = configparser.ConfigParser()
all_envs = [] config.read(subdir + "/" + file)
for pio_env in pio_envs: for c in config.sections():
env_build_flags = cfg.get(f"env:{pio_env}", 'build_flags') if c.startswith("env:"):
env_platform = None section = config[c].name[4:]
for flag in env_build_flags: if "extends" in config[config[c].name]:
# Extract the platform from the build flags if options[0] + "_base" in config[config[c].name]["extends"]:
# Example flag: -I variants/esp32s3/heltec-v3 if "board_level" in config[config[c].name]:
match = re.search(r"-I\s?variants/([^/]+)", flag) if (
if match: config[config[c].name]["board_level"] == "extra"
env_platform = match.group(1) ) & ("extra" in options):
break outlist.append(section)
# Intentionally fail if platform cannot be determined
if not env_platform:
print(f"Error: Could not determine platform for environment '{pio_env}'")
exit(1)
# Store env details as a dictionary, and add to 'all_envs' list
env = {
'name': pio_env,
'platform': env_platform,
'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)
# Filter outputs based on options
# Check is mutually exclusive with other options (except 'pr')
if "check" in options:
for env in all_envs:
if env['board_check']:
if "pr" in options:
if env['board_level'] == 'pr':
outlist.append(env['name'])
else: else:
outlist.append(env['name']) outlist.append(section)
# Filter (non-check) builds by platform # Add the TFT variants if the base variant is selected
elif section.replace("-tft", "") in outlist and config[config[c].name].get("board_level") != "extra":
outlist.append(section)
elif section.replace("-inkhud", "") in outlist and config[config[c].name].get("board_level") != "extra":
outlist.append(section)
if "board_check" in config[config[c].name]:
if (config[config[c].name]["board_check"] == "true") & (
"check" in options
):
outlist.append(section)
if ("quick" in options) & (len(outlist) > 3):
print(json.dumps(random.sample(outlist, 3)))
else: else:
for env in all_envs:
if options[0] == env['platform']:
# Always include board_level = 'pr'
if env['board_level'] == 'pr':
outlist.append(env['name'])
# Include board_level = 'extra' when requested
elif "extra" in options and env['board_level'] == "extra":
outlist.append(env['name'])
# If no board level is specified, include in release builds (not PR)
elif "pr" not in options and not env['board_level']:
outlist.append(env['name'])
# Return as a JSON list
print(json.dumps(outlist)) print(json.dumps(outlist))

View File

@@ -87,27 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<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">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
</release>
<release version="2.7.5" date="2025-08-09">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.5</url>
</release>
<release version="2.7.4" date="2025-07-19">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.4</url>
</release>
<release version="2.7.3" date="2025-07-10">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.3</url>
</release>
<release version="2.7.2" date="2025-07-04"> <release version="2.7.2" date="2025-07-04">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.2</url>
</release> </release>

View File

@@ -3,7 +3,6 @@
# trunk-ignore-all(flake8/F821): For SConstruct imports # trunk-ignore-all(flake8/F821): For SConstruct imports
import sys import sys
from os.path import join from os.path import join
import subprocess
import json import json
import re import re
@@ -93,17 +92,6 @@ prefsLoc = projenv["PROJECT_DIR"] + "/version.properties"
verObj = readProps(prefsLoc) verObj = readProps(prefsLoc)
print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV")) print("Using meshtastic platformio-custom.py, firmware version " + verObj["long"] + " on " + env.get("PIOENV"))
# get repository owner if git is installed
try:
r_owner = (
subprocess.check_output(["git", "config", "--get", "remote.origin.url"])
.decode("utf-8")
.strip().split("/")
)
repo_owner = r_owner[-2] + "/" + r_owner[-1].replace(".git", "")
except subprocess.CalledProcessError:
repo_owner = "unknown"
jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc" jsonLoc = env["PROJECT_DIR"] + "/userPrefs.jsonc"
with open(jsonLoc) as f: with open(jsonLoc) as f:
jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE) jsonStr = re.sub("//.*","", f.read(), flags=re.MULTILINE)
@@ -129,7 +117,6 @@ 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,
] + pref_flags ] + pref_flags
print ("Using flags:") print ("Using flags:")

View File

@@ -10,8 +10,7 @@
"hwids": [ "hwids": [
["0x239A", "0x4405"], ["0x239A", "0x4405"],
["0x239A", "0x0029"], ["0x239A", "0x0029"],
["0x239A", "0x002A"], ["0x239A", "0x002A"]
["0x2886", "0x1667"]
], ],
"usb_product": "HT-n5262", "usb_product": "HT-n5262",
"mcu": "nrf52840", "mcu": "nrf52840",

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,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

@@ -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

@@ -1,43 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_qspi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "LilyGo T-Deck Pro S3 (16M Flash 8M QSPI PSRAM )",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"monitor": {
"speed": 115200
},
"url": "https://lilygo.cc/products/t-deck-pro",
"vendor": "LilyGo"
}

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",

23
debian/changelog vendored
View File

@@ -1,4 +1,4 @@
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium meshtasticd (2.7.2.0) UNRELEASED; urgency=medium
[ Austin Lane ] [ Austin Lane ]
* Initial packaging * Initial packaging
@@ -28,23 +28,4 @@ meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
[ ] [ ]
* GitHub Actions Automatic version bump * GitHub Actions Automatic version bump
[ Ubuntu ] -- <github-actions[bot]@users.noreply.github.com> Fri, 04 Jul 2025 11:58:01 +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
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
-- <github-actions[bot]@users.noreply.github.com> Wed, 03 Sep 2025 23:39:17 +0000

View File

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

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
# 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

@@ -6,8 +6,7 @@ default_envs = tbeam
extra_configs = extra_configs =
arch/*/*.ini arch/*/*.ini
variants/*/*/platformio.ini variants/*/platformio.ini
variants/*/diy/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini src/graphics/niche/InkHUD/PlatformioConfig.ini
description = Meshtastic description = Meshtastic
@@ -60,9 +59,9 @@ 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/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.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=custom.pio depName=OneButton packageName=mathertel/library/OneButton
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip mathertel/OneButton@2.6.1
# 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
https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip https://github.com/meshtastic/arduino-fsm/archive/7db3702bf0cfe97b783d6c72595e3f38e0b19159.zip
# 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
@@ -102,29 +101,21 @@ lib_deps =
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog # renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0 arcao/Syslog@2.0.0
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
[nrf52_networking_base]
lib_deps =
# renovate: datasource=custom.pio depName=TBPubSubClient packageName=thingsboard/library/TBPubSubClient
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
[radiolib_base] [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.2.1 jgromes/RadioLib@7.2.0
[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/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip https://github.com/meshtastic/device-ui/archive/8c7092c73425adfda1aac8c6960df06cd85f6d92.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.2 adafruit/Adafruit BusIO@1.17.1
# 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
@@ -138,7 +129,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library # renovate: datasource=custom.pio depName=Adafruit MCP9808 packageName=adafruit/library/Adafruit MCP9808 Library
adafruit/Adafruit MCP9808 Library@2.0.2 adafruit/Adafruit MCP9808 Library@2.0.2
# renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library # renovate: datasource=custom.pio depName=Adafruit INA260 packageName=adafruit/library/Adafruit INA260 Library
adafruit/Adafruit INA260 Library@1.5.3 adafruit/Adafruit INA260 Library@1.5.2
# renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219 # renovate: datasource=custom.pio depName=Adafruit INA219 packageName=adafruit/library/Adafruit INA219
adafruit/Adafruit INA219@1.2.3 adafruit/Adafruit INA219@1.2.3
# renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor # renovate: datasource=custom.pio depName=Adafruit PM25 AQI Sensor packageName=adafruit/library/Adafruit PM25 AQI Sensor
@@ -157,8 +148,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
@@ -177,8 +168,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]
@@ -188,7 +177,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X # renovate: datasource=custom.pio depName=Adafruit MAX1704X packageName=adafruit/library/Adafruit MAX1704X
adafruit/Adafruit MAX1704X@1.0.3 adafruit/Adafruit MAX1704X@1.0.3
# renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library # renovate: datasource=custom.pio depName=Adafruit SHTC3 packageName=adafruit/library/Adafruit SHTC3 Library
adafruit/Adafruit SHTC3 Library@1.0.2 adafruit/Adafruit SHTC3 Library@1.0.1
# renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X # renovate: datasource=custom.pio depName=Adafruit LPS2X packageName=adafruit/library/Adafruit LPS2X
adafruit/Adafruit LPS2X@2.0.6 adafruit/Adafruit LPS2X@2.0.6
# renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library # renovate: datasource=custom.pio depName=Adafruit SHT31 packageName=adafruit/library/Adafruit SHT31 Library

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

@@ -89,22 +89,14 @@ class BluetoothStatus : public Status
case ConnectionState::CONNECTED: case ConnectionState::CONNECTED:
LOG_DEBUG("BluetoothStatus CONNECTED"); LOG_DEBUG("BluetoothStatus CONNECTED");
#ifdef BLE_LED #ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, LOW);
#else
digitalWrite(BLE_LED, HIGH); digitalWrite(BLE_LED, HIGH);
#endif
#endif #endif
break; break;
case ConnectionState::DISCONNECTED: case ConnectionState::DISCONNECTED:
LOG_DEBUG("BluetoothStatus DISCONNECTED"); LOG_DEBUG("BluetoothStatus DISCONNECTED");
#ifdef BLE_LED #ifdef BLE_LED
#ifdef BLE_LED_INVERTED
digitalWrite(BLE_LED, HIGH);
#else
digitalWrite(BLE_LED, LOW); digitalWrite(BLE_LED, LOW);
#endif
#endif #endif
break; break;
} }

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

@@ -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";
@@ -39,45 +32,3 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
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_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;
case meshtastic_Config_DeviceConfig_Role_REPEATER:
return "Repeater";
break;
default:
return "Unknown";
break;
}
}

View File

@@ -4,9 +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);
public:
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

@@ -20,11 +20,6 @@
#include "meshUtils.h" #include "meshUtils.h"
#include "sleep.h" #include "sleep.h"
#if defined(ARCH_PORTDUINO)
#include "api/WiFiServerAPI.h"
#include "input/LinuxInputImpl.h"
#endif
// Working USB detection for powered/charging states on the RAK platform // Working USB detection for powered/charging states on the RAK platform
#ifdef NRF_APM #ifdef NRF_APM
#include "nrfx_power.h" #include "nrfx_power.h"
@@ -125,16 +120,6 @@ NullSensor max17048Sensor;
RAK9154Sensor rak9154Sensor; RAK9154Sensor rak9154Sensor;
#endif #endif
#ifdef HAS_PPM
// note: XPOWERS_CHIP_XXX must be defined in variant.h
#include <XPowersLib.h>
XPowersPPM *PPM = NULL;
#endif
#ifdef HAS_BQ27220
#include "bq27220.h"
#endif
#ifdef HAS_PMU #ifdef HAS_PMU
XPowersLibInterface *PMU = NULL; XPowersLibInterface *PMU = NULL;
#else #else
@@ -680,10 +665,6 @@ bool Power::setup()
found = true; found = true;
} else if (lipoInit()) { } else if (lipoInit()) {
found = true; found = true;
} else if (lipoChargerInit()) {
found = true;
} else if (meshSolarInit()) {
found = true;
} else if (analogInit()) { } else if (analogInit()) {
found = true; found = true;
} }
@@ -698,65 +679,9 @@ bool Power::setup()
return found; return found;
} }
void Power::powerCommandsCheck()
{
if (rebootAtMsec && millis() > rebootAtMsec) {
LOG_INFO("Rebooting");
reboot();
}
if (shutdownAtMsec && millis() > shutdownAtMsec) {
shutdownAtMsec = 0;
shutdown();
}
}
void Power::reboot()
{
notifyReboot.notifyObservers(NULL);
#if defined(ARCH_ESP32)
ESP.restart();
#elif defined(ARCH_NRF52)
NVIC_SystemReset();
#elif defined(ARCH_RP2040)
rp2040.reboot();
#elif defined(ARCH_PORTDUINO)
deInitApiServer();
if (aLinuxInputImpl)
aLinuxInputImpl->deInit();
SPI.end();
Wire.end();
Serial1.end();
if (screen) {
delete screen;
screen = nullptr;
}
LOG_DEBUG("final reboot!");
::reboot();
#elif defined(ARCH_STM32WL)
HAL_NVIC_SystemReset();
#else
rebootAtMsec = -1;
LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied");
#endif
}
void Power::shutdown() void Power::shutdown()
{ {
LOG_INFO("Shutting Down");
#if HAS_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
#endif
}
#endif
#if !defined(ARCH_STM32WL)
playShutdownMelody();
#endif
nodeDB->saveToDisk();
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040)
#ifdef PIN_LED1 #ifdef PIN_LED1
@@ -768,11 +693,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, false);
#elif defined(ARCH_PORTDUINO)
exit(EXIT_SUCCESS);
#else
LOG_WARN("FIXME implement shutdown for this platform");
#endif #endif
} }
@@ -833,25 +754,16 @@ 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_DEBUG(threadlist); LOG_DEBUG(threadlist.c_str());
LOG_DEBUG("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();
@@ -865,19 +777,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
@@ -1329,213 +1237,3 @@ bool Power::lipoInit()
return false; return false;
} }
#endif #endif
#if defined(HAS_PPM) && HAS_PPM
/**
* Adapter class for BQ25896/BQ27220 Lipo battery charger.
*/
class LipoCharger : public HasBatteryLevel
{
private:
BQ27220 *bq = nullptr;
public:
/**
* Init the I2C BQ25896 Lipo battery charger
*/
bool runOnce()
{
if (PPM == nullptr) {
PPM = new XPowersPPM;
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
if (result) {
LOG_INFO("PPM BQ25896 init succeeded");
// Set the minimum operating voltage. Below this voltage, the PPM will protect
// PPM->setSysPowerDownVoltage(3100);
// Set input current limit, default is 500mA
// PPM->setInputCurrentLimit(800);
// Disable current limit pin
// PPM->disableCurrentLimitPin();
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
PPM->setChargeTargetVoltage(4288);
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
// PPM->setPrechargeCurr(64);
// The premise is that limit pin is disabled, or it will
// only follow the maximum charging current set by limit pin.
// Set the charging current , Range:0~5056mA ,step:64mA
PPM->setChargerConstantCurr(1024);
// To obtain voltage data, the ADC must be enabled first
PPM->enableMeasure();
// Turn on charging function
// If there is no battery connected, do not turn on the charging function
PPM->enableCharge();
} else {
LOG_WARN("PPM BQ25896 init failed");
delete PPM;
PPM = nullptr;
return false;
}
}
if (bq == nullptr) {
bq = new BQ27220;
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
bool result = bq->init();
if (result) {
LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
return true;
} else {
LOG_WARN("BQ27220 init failed");
delete bq;
bq = nullptr;
return false;
}
}
return false;
}
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override
{
return -1;
// return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated
}
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return bq->getVoltage(); }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; }
/**
* return true if there is an external power source detected
*/
virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; }
/**
* return true if the battery is currently charging
*/
virtual bool isCharging() override
{
bool isCharging = PPM->isCharging();
if (isCharging) {
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
} else {
if (!PPM->isVbusIn()) {
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
}
}
return isCharging;
}
};
LipoCharger lipoCharger;
/**
* Init the Lipo battery charger
*/
bool Power::lipoChargerInit()
{
bool result = lipoCharger.runOnce();
LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &lipoCharger;
return true;
}
#else
/**
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::lipoChargerInit()
{
return false;
}
#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

@@ -72,7 +72,7 @@ extern Power *power;
static void shutdownEnter() static void shutdownEnter()
{ {
LOG_DEBUG("State: SHUTDOWN"); LOG_DEBUG("State: SHUTDOWN");
shutdownAtMsec = millis(); power->shutdown();
} }
#include "error.h" #include "error.h"

View File

@@ -57,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
@@ -99,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
@@ -288,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 {
@@ -297,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

@@ -64,14 +64,6 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
int32_t SerialConsole::runOnce() int32_t SerialConsole::runOnce()
{ {
#ifdef HELTEC_MESH_SOLAR
//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
return runOncePart(); return runOncePart();
} }

View File

@@ -28,14 +28,11 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
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
@@ -50,6 +47,10 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
playComboTune(); // Ping sent feedback playComboTune(); // Ping sent feedback
break; break;
case INPUT_BROKER_SHUTDOWN:
playShutdownMelody(); // Shutdown feedback
break;
default: default:
// For other events, check if it's a printable character // For other events, check if it's a printable character
if (event->kbchar >= 32 && event->kbchar <= 126) { if (event->kbchar >= 32 && event->kbchar <= 126) {
@@ -68,7 +69,10 @@ int32_t BuzzerFeedbackThread::runOnce()
// This thread is primarily event-driven, but we can use runOnce // This thread is primarily event-driven, but we can use runOnce
// for any periodic tasks if needed in the future // for any periodic tasks if needed in the future
if (needsUpdate) {
needsUpdate = false; needsUpdate = false;
// Could add any periodic processing here
}
// Run every 100ms when active, less frequently when idle // Run every 100ms when active, less frequently when idle
return needsUpdate ? 100 : 1000; return needsUpdate ? 100 : 1000;

View File

@@ -140,10 +140,6 @@ bool playNextLeadUpNote()
playTones(&note, 1); // Play single note using existing playTones function playTones(&note, 1); // Play single note using existing playTones function
leadUpNoteIndex++; leadUpNoteIndex++;
if (leadUpNoteIndex >= leadUpNotesCount) {
return false; // this was the final note
}
return true; // Note was played (playTones handles buzzer availability internally) return true; // Note was played (playTones handles buzzer availability internally)
} }

View File

@@ -26,10 +26,10 @@ 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
@@ -135,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
@@ -150,12 +150,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Define if screen should be mirrored left to right // Define if screen should be mirrored left to right
// #define SCREEN_MIRROR // #define SCREEN_MIRROR
// I2C Keyboards (M5Stack, RAK14004, T-Deck, T-Deck Pro, T-Lora Pager, CardKB, BBQ10, MPR121, TCA8418) // I2C Keyboards (M5Stack, RAK14004, T-Deck)
#define CARDKB_ADDR 0x5F #define CARDKB_ADDR 0x5F
#define TDECK_KB_ADDR 0x55 #define TDECK_KB_ADDR 0x55
#define BBQ10_KB_ADDR 0x1F #define BBQ10_KB_ADDR 0x1F
#define MPR121_KB_ADDR 0x5A #define MPR121_KB_ADDR 0x5A
#define TCA8418_KB_ADDR 0x34
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// SENSOR // SENSOR
@@ -194,11 +193,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MLX90614_ADDR_DEF 0x5A #define MLX90614_ADDR_DEF 0x5A
#define CGRADSENS_ADDR 0x66 #define CGRADSENS_ADDR 0x66
#define LTR390UV_ADDR 0x53 #define LTR390UV_ADDR 0x53
#define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418_KB #define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418
#define PCT2075_ADDR 0x37 #define PCT2075_ADDR 0x37
#define BQ27220_ADDR 0x55 // same address as TDECK_KB
#define BQ25896_ADDR 0x6B
#define LTR553ALS_ADDR 0x23
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ACCELEROMETER // ACCELEROMETER
@@ -212,7 +208,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define BMX160_ADDR 0x69 #define BMX160_ADDR 0x69
#define ICM20948_ADDR 0x69 #define ICM20948_ADDR 0x69
#define ICM20948_ADDR_ALT 0x68 #define ICM20948_ADDR_ALT 0x68
#define BHI260AP_ADDR 0x28
#define BMM150_ADDR 0x13 #define BMM150_ADDR 0x13
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -235,7 +230,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Touchscreen // Touchscreen
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#define FT6336U_ADDR 0x48 #define FT6336U_ADDR 0x48
#define CST328_ADDR 0x1A
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) // RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected)
@@ -431,7 +425,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define MESHTASTIC_EXCLUDE_SERIAL 1 #define MESHTASTIC_EXCLUDE_SERIAL 1
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1 #define MESHTASTIC_EXCLUDE_POWERSTRESS 1
#define MESHTASTIC_EXCLUDE_ADMIN 1 #define MESHTASTIC_EXCLUDE_ADMIN 1
#define MESHTASTIC_EXCLUDE_AMBIENTLIGHTING 1
#endif #endif
// // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled) // // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled)

View File

@@ -74,14 +74,7 @@ class ScanI2C
RAK12035, RAK12035,
TCA8418KB, TCA8418KB,
PCT2075, PCT2075,
CST328,
BQ25896,
BQ27220,
LTR553ALS,
BHI260AP,
BMM150, BMM150,
TSL2561,
DRV2605
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;

View File

@@ -184,13 +184,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
type = RTC_RV3028; type = RTC_RV3028;
logFoundDevice("RV3028", (uint8_t)addr.address); logFoundDevice("RV3028", (uint8_t)addr.address);
rtc.initI2C(*i2cBus); rtc.initI2C(*i2cBus);
// Update RTC EEPROM settings, if necessary rtc.writeToRegister(0x35, 0x07); // no Clkout
if (rtc.readEEPROMRegister(0x35) != 0x07) { rtc.writeToRegister(0x37, 0xB4);
rtc.writeEEPROMRegister(0x35, 0x07); // no Clkout
}
if (rtc.readEEPROMRegister(0x37) != 0xB4) {
rtc.writeEEPROMRegister(0x37, 0xB4);
}
break; break;
#endif #endif
@@ -211,17 +206,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
} }
break; break;
case TDECK_KB_ADDR: SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard", (uint8_t)addr.address);
// Do we have the T-Deck keyboard or the T-Deck Pro battery sensor?
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x04), 1);
if (registerValue != 0) {
logFoundDevice("BQ27220", (uint8_t)addr.address);
type = BQ27220;
} else {
logFoundDevice("TDECKKB", (uint8_t)addr.address);
type = TDECKKB;
}
break;
SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address); SCAN_SIMPLE_CASE(BBQ10_KB_ADDR, BBQ10KB, "BB Q10", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address);
@@ -411,12 +396,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
logFoundDevice("BQ24295", (uint8_t)addr.address); logFoundDevice("BQ24295", (uint8_t)addr.address);
break; break;
} }
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x14), 1); // get ID
if ((registerValue & 0b00000011) == 0b00000010) {
type = BQ25896;
logFoundDevice("BQ25896", (uint8_t)addr.address);
break;
}
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x0F), 1); // get ID
if (registerValue == 0x6A) { if (registerValue == 0x6A) {
type = LSM6DS3; type = LSM6DS3;
@@ -461,26 +440,13 @@ 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);
SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(CST328_ADDR, CST328, "CST328", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(LTR553ALS_ADDR, LTR553ALS, "LTR553ALS", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(BHI260AP_ADDR, BHI260AP, "BHI260AP", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address); SCAN_SIMPLE_CASE(SCD4X_ADDR, SCD4X, "SCD4X", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address);
#ifdef HAS_TPS65233 #ifdef HAS_TPS65233
@@ -492,16 +458,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (registerValue == 0x5a) { if (registerValue == 0x5a) {
type = MLX90614; type = MLX90614;
logFoundDevice("MLX90614", (uint8_t)addr.address); logFoundDevice("MLX90614", (uint8_t)addr.address);
} else {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS
if (registerValue == 0xe0) {
type = DRV2605;
logFoundDevice("DRV2605", (uint8_t)addr.address);
} else { } else {
type = MPR121KB; type = MPR121KB;
logFoundDevice("MPR121KB", (uint8_t)addr.address); logFoundDevice("MPR121KB", (uint8_t)addr.address);
} }
}
break; break;
case ICM20948_ADDR: // same as BMX160_ADDR case ICM20948_ADDR: // same as BMX160_ADDR

View File

@@ -39,9 +39,11 @@ template <typename T, std::size_t N> std::size_t array_count(const T (&)[N])
return N; return N;
} }
#if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL) #if defined(NRF52840_XXAA) || defined(NRF52833_XXAA) || defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
#if defined(GPS_SERIAL_PORT) #if defined(SENSECAP_INDICATOR)
HardwareSerial *GPS::_serial_gps = &GPS_SERIAL_PORT; FakeUART *GPS::_serial_gps = FakeSerial;
#elif defined(RAK2560)
HardwareSerial *GPS::_serial_gps = &Serial2;
#else #else
HardwareSerial *GPS::_serial_gps = &Serial1; HardwareSerial *GPS::_serial_gps = &Serial1;
#endif #endif
@@ -643,16 +645,8 @@ bool GPS::setup()
delay(250); delay(250);
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) { } else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC _serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 0 1 0 0 1
} else {
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
// GPS GLONASS GALILEO BDS QZSS NAVIC
// 1 1 1 1 0 0
}
// Configure NMEA (sentences will output once per fix) // Configure NMEA (sentences will output once per fix)
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON _serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF _serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
@@ -808,14 +802,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;
} }
@@ -851,6 +837,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:
@@ -868,7 +857,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
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW); digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
@@ -880,7 +871,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
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW); digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
} }
@@ -1063,8 +1056,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;
} }
} }
@@ -1087,6 +1078,7 @@ void GPS::publishUpdate()
int32_t GPS::runOnce() int32_t GPS::runOnce()
{ {
#if !defined(SENSECAP_INDICATOR)
if (!GPSInitFinished) { if (!GPSInitFinished) {
if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) { if (!_serial_gps || config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT) {
LOG_INFO("GPS set to not-present. Skip probe"); LOG_INFO("GPS set to not-present. Skip probe");
@@ -1102,6 +1094,7 @@ int32_t GPS::runOnce()
GPSInitFinished = true; GPSInitFinished = true;
publishUpdate(); publishUpdate();
} }
#endif
// Repeaters have no need for GPS // Repeaters have no need for GPS
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
@@ -1124,19 +1117,11 @@ int32_t GPS::runOnce()
shouldPublish = true; shouldPublish = true;
} }
uint8_t prev_fixQual = fixQual;
bool gotLoc = lookForLocation(); bool gotLoc = lookForLocation();
if (gotLoc && !hasValidLocation) { // declare that we have location ASAP if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
LOG_DEBUG("hasValidLocation RISING EDGE"); LOG_DEBUG("hasValidLocation RISING EDGE");
hasValidLocation = true; hasValidLocation = true;
shouldPublish = true; shouldPublish = true;
// Hold for 20secs after getting a lock to download ephemeris etc
fixHoldEnds = millis() + 20000;
}
if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on.
fixHoldEnds = millis() + 20000;
shouldPublish = true; // Publish immediately, since next publish is at end of hold
} }
bool tooLong = scheduling.searchedTooLong(); bool tooLong = scheduling.searchedTooLong();
@@ -1145,7 +1130,8 @@ int32_t GPS::runOnce()
// Once we get a location we no longer desperately want an update // Once we get a location we no longer desperately want an update
if ((gotLoc && gotTime) || tooLong) { if ((gotLoc && gotTime) || tooLong) {
if (tooLong && !gotLoc) {
if (tooLong) {
// 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) {
LOG_DEBUG("hasValidLocation FALLING EDGE"); LOG_DEBUG("hasValidLocation FALLING EDGE");
@@ -1153,15 +1139,9 @@ int32_t GPS::runOnce()
p = meshtastic_Position_init_default; p = meshtastic_Position_init_default;
hasValidLocation = false; hasValidLocation = false;
} }
if (millis() > fixHoldEnds) {
shouldPublish = true; // publish our update at the end of the lock hold
publishUpdate();
down(); down();
#ifdef GPS_DEBUG shouldPublish = true; // publish our update for this just finished acquisition window
} else {
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
#endif
}
} }
// If state has changed do a publish // If state has changed do a publish
@@ -1248,15 +1228,9 @@ GnssModel_t GPS::probe(int serialSpeed)
_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");
delay(20);
// 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);
std::vector<ChipInfo> atgm = { std::vector<ChipInfo> atgm = {
@@ -1436,11 +1410,13 @@ 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 !defined(SENSECAP_INDICATOR)
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
return nullptr; return nullptr;
#endif
GPS *new_gps = new GPS; GPS *new_gps = new GPS;
new_gps->rx_gpio = _rx_gpio; new_gps->rx_gpio = _rx_gpio;
@@ -1526,10 +1502,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 > 0) {
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
@@ -1546,13 +1540,10 @@ 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()); perhapsSetRTC(RTCQualityGPS, t);
return true; return true;
} else {
return false;
}
} else } else
return false; return false;
} else } else
@@ -1567,6 +1558,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 > 0) {
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

@@ -11,6 +11,10 @@
#include "input/UpDownInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h"
#include "modules/PositionModule.h" #include "modules/PositionModule.h"
#ifdef SENSECAP_INDICATOR
#include "mesh/comms/FakeUART.h"
#endif
// Allow defining the polarity of the ENABLE output. default is active high // Allow defining the polarity of the ENABLE output. default is active high
#ifndef GPS_EN_ACTIVE #ifndef GPS_EN_ACTIVE
#define GPS_EN_ACTIVE 1 #define GPS_EN_ACTIVE 1
@@ -31,8 +35,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 {
@@ -160,7 +163,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;
@@ -189,7 +192,9 @@ class GPS : private concurrency::OSThread
CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep); CallbackObserver<GPS, void *> notifyDeepSleepObserver = CallbackObserver<GPS, void *>(this, &GPS::prepareDeepSleep);
/** If !NULL we will use this serial port to construct our GPS */ /** If !NULL we will use this serial port to construct our GPS */
#if defined(ARCH_RP2040) #if defined(SENSECAP_INDICATOR)
static FakeUART *_serial_gps;
#elif defined(ARCH_RP2040)
static SerialUART *_serial_gps; static SerialUART *_serial_gps;
#else #else
static HardwareSerial *_serial_gps; static HardwareSerial *_serial_gps;

View File

@@ -23,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
@@ -44,23 +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) {
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);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) {
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) {
@@ -83,23 +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) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
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);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) {
currentQuality = RTCQualityDevice; currentQuality = RTCQualityDevice;
} }
return RTCSetResultSuccess;
} }
#else #else
if (!gettimeofday(&tv, NULL)) { if (!gettimeofday(&tv, NULL)) {
@@ -108,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;
} }
/** /**
@@ -119,27 +101,19 @@ 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
*/ */
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate)
{ {
static uint32_t lastSetMsec = 0; static uint32_t lastSetMsec = 0;
uint32_t now = millis(); uint32_t now = millis();
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) {
#ifdef GPS_DEBUG
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
#endif return false;
return RTCSetResultInvalidTime;
} else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
#ifdef GPS_DEBUG
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
BUILD_EPOCH + FORTY_YEARS);
#endif
return RTCSetResultInvalidTime;
} }
#endif #endif
@@ -210,9 +184,9 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
readFromRTC(); readFromRTC();
#endif #endif
return RTCSetResultSuccess; return true;
} else { } else {
return RTCSetResultNotSet; // RTC was already set with a higher quality time return false;
} }
} }
@@ -241,7 +215,7 @@ const char *RtcName(RTCQuality quality)
* @param t The time to potentially set the RTC to. * @param t The time to potentially set the RTC to.
* @return True if the RTC was set to the provided time, false otherwise. * @return True if the RTC was set to the provided time, false otherwise.
*/ */
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) bool perhapsSetRTC(RTCQuality q, struct tm &t)
{ {
/* Convert to unix time /* Convert to unix time
The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970 The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of seconds that have elapsed since January 1, 1970
@@ -253,26 +227,11 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
struct timeval tv; struct timeval tv;
tv.tv_sec = res; tv.tv_sec = res;
tv.tv_usec = 0; // time.centisecond() * (10 / 1000); tv.tv_usec = 0; // time.centisecond() * (10 / 1000);
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) {
#ifdef GPS_DEBUG
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
#endif
return RTCSetResultInvalidTime;
} else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
#ifdef GPS_DEBUG
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
BUILD_EPOCH + FORTY_YEARS);
#endif
return RTCSetResultInvalidTime;
}
#endif
// LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); // LOG_DEBUG("Got time from GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
if (t.tm_year < 0 || t.tm_year >= 300) { if (t.tm_year < 0 || t.tm_year >= 300) {
// LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec); // LOG_DEBUG("Ignore invalid GPS month=%d, year=%d, unixtime=%ld", t.tm_mon, t.tm_year, tv.tv_sec);
return RTCSetResultInvalidTime; return false;
} else { } else {
return perhapsSetRTC(q, &tv); return perhapsSetRTC(q, &tv);
} }

View File

@@ -22,22 +22,13 @@ enum RTCQuality {
RTCQualityGPS = 4 RTCQualityGPS = 4
}; };
/// The RTC set result codes
/// Used to indicate the result of an attempt to set the RTC.
enum RTCSetResult {
RTCSetResultNotSet = 0, ///< RTC was set successfully
RTCSetResultSuccess = 1, ///< RTC was set successfully
RTCSetResultInvalidTime = 3, ///< The provided time was invalid (e.g., before the build epoch)
RTCSetResultError = 4 ///< An error occurred while setting the RTC
};
RTCQuality getRTCQuality(); RTCQuality getRTCQuality();
extern uint32_t lastSetFromPhoneNtpOrGps; extern uint32_t lastSetFromPhoneNtpOrGps;
/// 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
RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false);
RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t); bool perhapsSetRTC(RTCQuality q, struct tm &t);
/// Return a string name for the quality /// Return a string name for the quality
const char *RtcName(RTCQuality quality); const char *RtcName(RTCQuality quality);
@@ -48,13 +39,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
#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware
#endif

View File

@@ -6,10 +6,6 @@
#include "main.h" #include "main.h"
#include <SPI.h> #include <SPI.h>
#ifdef GXEPD2_DRIVER_0
#include "einkDetect.h"
#endif
/* /*
The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini
Previously, these macros were defined at the top of this file. Previously, these macros were defined at the top of this file.
@@ -67,28 +63,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");
@@ -148,32 +136,17 @@ bool EInkDisplay::connect()
#endif #endif
#endif #endif
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) #if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1)
{ {
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1); auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel); adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(); adafruitDisplay->init();
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) #ifdef ELECROW_ThinkNode_M1
adafruitDisplay->setRotation(4); adafruitDisplay->setRotation(4);
#else #else
adafruitDisplay->setRotation(3); adafruitDisplay->setRotation(3);
#endif #endif
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
}
#elif defined(ELECROW_ThinkNode_M5)
{
// Start HSPI
hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init();
adafruitDisplay->setRotation(4);
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
} }
#elif defined(MESHLINK) #elif defined(MESHLINK)
@@ -201,8 +174,9 @@ bool EInkDisplay::connect()
} }
} }
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \ #elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) || \
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
{ {
// Start HSPI // Start HSPI
hspi = new SPIClass(HSPI); hspi = new SPIClass(HSPI);
@@ -229,7 +203,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(0); adafruitDisplay->setRotation(0);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(M5_COREINK) || defined(T_DECK_PRO) #elif defined(M5_COREINK)
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel); adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
@@ -243,7 +217,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) #elif defined(HELTEC_MESH_POCKET)
{ {
spi1 = &SPI1; spi1 = &SPI1;
spi1->begin(); spi1->begin();
@@ -257,25 +231,7 @@ 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)
// Detect display model, before starting SPI
EInkDetectionResult displayModel = detectEInk();
// Start HSPI
hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
// Create GxEPD2 object
adafruitDisplay = new GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1>((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC,
PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
#endif #endif
return true; return true;

View File

@@ -5,10 +5,6 @@
#include "GxEPD2_BW.h" #include "GxEPD2_BW.h"
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models
#include "GxEPD2Multi.h"
#endif
/** /**
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation. * An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
* *
@@ -67,24 +63,17 @@ class EInkDisplay : public OLEDDisplay
// Connect to the display // Connect to the display
virtual bool connect() override; virtual bool connect() override;
#ifdef GXEPD2_DRIVER_0 // AdafruitGFX display object - instantiated in connect(), variant specific
// AdafruitGFX display object - wrapper for multiple drivers
// Allows runtime detection of multiple displays
// Avoid this situation if possible!
GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1> *adafruitDisplay = NULL;
#else
// AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL; GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL;
#endif
// If display uses HSPI // If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \ #if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \ defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER)
SPIClass *hspi = NULL; SPIClass *hspi = NULL;
#endif #endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) #if defined(HELTEC_MESH_POCKET)
SPIClass *spi1 = NULL; SPIClass *spi1 = NULL;
#endif #endif

View File

@@ -1,135 +0,0 @@
// Wrapper class for GxEPD2_BW
// Generic signature at build-time, so that we can detect display model at run-time
// Workaround for issue of GxEPD2_BW objects not having a shared base class
// Only exposes methods which we are actually using
template <typename Driver0, typename Driver1> class GxEPD2_Multi
{
public:
void drawPixel(int16_t x, int16_t y, uint16_t color)
{
if (which == 0)
driver0->drawPixel(x, y, color);
else
driver1->drawPixel(x, y, color);
}
bool nextPage()
{
if (which == 0)
return driver0->nextPage();
else
return driver1->nextPage();
}
void hibernate()
{
if (which == 0)
driver0->hibernate();
else
driver1->hibernate();
}
void init(uint32_t serial_diag_bitrate = 0)
{
if (which == 0)
driver0->init(serial_diag_bitrate);
else
driver1->init(serial_diag_bitrate);
}
void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false)
{
if (which == 0)
driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
else
driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
}
void setRotation(uint8_t x)
{
if (which == 0)
driver0->setRotation(x);
else
driver1->setRotation(x);
}
void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
if (which == 0)
driver0->setPartialWindow(x, y, w, h);
else
driver1->setPartialWindow(x, y, w, h);
}
void setFullWindow()
{
if (which == 0)
driver0->setFullWindow();
else
driver1->setFullWindow();
}
int16_t width()
{
if (which == 0)
return driver0->width();
else
return driver1->width();
}
int16_t height()
{
if (which == 0)
return driver0->height();
else
return driver1->height();
}
void clearScreen(uint8_t value = 0xFF)
{
if (which == 0)
driver0->clearScreen();
else
driver1->clearScreen();
}
void endAsyncFull()
{
if (which == 0)
driver0->endAsyncFull();
else
driver1->endAsyncFull();
}
// Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd
class Epd2Wrapper
{
public:
bool isBusy() { return m_epd2->isBusy(); }
GxEPD2_EPD *m_epd2;
} epd2;
// Constructor
// Select driver by passing whichDriver as 0 or 1
GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi)
{
assert(whichDriver == 0 || whichDriver == 1);
which = whichDriver;
LOG_DEBUG("GxEPD2_Multi driver: %d", which);
if (which == 0) {
driver0 = new GxEPD2_BW<Driver0, Driver0::HEIGHT>(Driver0(cs, dc, rst, busy, spi));
epd2.m_epd2 = &(driver0->epd2);
} else if (which == 1) {
driver1 = new GxEPD2_BW<Driver1, Driver1::HEIGHT>(Driver1(cs, dc, rst, busy, spi));
epd2.m_epd2 = &(driver1->epd2);
}
}
private:
uint8_t which;
GxEPD2_BW<Driver0, Driver0::HEIGHT> *driver0;
GxEPD2_BW<Driver1, Driver1::HEIGHT> *driver1;
};

View File

@@ -21,7 +21,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "Screen.h" #include "Screen.h"
#include "NodeDB.h"
#include "PowerMon.h" #include "PowerMon.h"
#include "Throttle.h" #include "Throttle.h"
#include "configuration.h" #include "configuration.h"
@@ -45,6 +44,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#endif #endif
#include "FSCommon.h" #include "FSCommon.h"
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h"
#include "RadioLibInterface.h" #include "RadioLibInterface.h"
#include "error.h" #include "error.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
@@ -149,11 +149,6 @@ void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
// Called to trigger a banner with custom message and duration // Called to trigger a banner with custom message and duration
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options) void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
{ {
// Don't show overlay banner if virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK #ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif #endif
@@ -176,13 +171,8 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
} }
// Called to trigger a banner with custom message and duration // Called to trigger a banner with custom message and duration
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback) void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback)
{ {
// Don't show node picker if virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK #ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif #endif
@@ -206,11 +196,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
std::function<void(uint32_t)> bannerCallback) std::function<void(uint32_t)> bannerCallback)
{ {
// Don't show number picker if virtual keyboard is active LOG_WARN("Show Number Picker");
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
#ifdef USE_EINK #ifdef USE_EINK
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
#endif #endif
@@ -231,30 +217,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);
// Start OnScreenKeyboardModule session (non-touch variant)
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
NotificationRenderer::virtualKeyboard = OnScreenKeyboardModule::instance().getKeyboard();
NotificationRenderer::textInputCallback = textCallback;
// 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;
@@ -332,13 +294,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color);
int32_t rawRGB = uiconfig.screen_rgb_color; int32_t rawRGB = uiconfig.screen_rgb_color;
if (rawRGB > 0 && rawRGB <= 255255255) { if (rawRGB > 0 && rawRGB <= 255255255) {
uint8_t TFT_MESH_r = (rawRGB >> 16) & 0xFF; uint8_t r = (rawRGB >> 16) & 0xFF;
uint8_t TFT_MESH_g = (rawRGB >> 8) & 0xFF; uint8_t g = (rawRGB >> 8) & 0xFF;
uint8_t TFT_MESH_b = rawRGB & 0xFF; uint8_t b = rawRGB & 0xFF;
LOG_INFO("Values of r,g,b: %d, %d, %d", TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); LOG_INFO("Values of r,g,b: %d, %d, %d", r, g, b);
if (TFT_MESH_r <= 255 && TFT_MESH_g <= 255 && TFT_MESH_b <= 255) { if (r <= 255 && g <= 255 && b <= 255) {
TFT_MESH = COLOR565(TFT_MESH_r, TFT_MESH_g, TFT_MESH_b); TFT_MESH = COLOR565(r, g, b);
} }
} }
@@ -351,13 +313,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
ST7789_MISO, ST7789_SCK); ST7789_MISO, ST7789_SCK);
#else #else
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
#endif
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH); static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
#endif
#elif defined(USE_SSD1306) #elif defined(USE_SSD1306)
dispdev = new SSD1306Wire(address.address, -1, -1, geometry, dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#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)
@@ -371,7 +333,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);
@@ -404,6 +366,9 @@ void Screen::doDeepSleep()
{ {
#ifdef USE_EINK #ifdef USE_EINK
setOn(false, graphics::UIRenderer::drawDeepSleepFrame); setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
#ifdef PIN_EINK_EN
digitalWrite(PIN_EINK_EN, LOW); // power off backlight
#endif
#else #else
// Without E-Ink display: // Without E-Ink display:
setOn(false); setOn(false);
@@ -422,19 +387,13 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
PMU->enablePowerOutput(XPOWERS_ALDO2); PMU->enablePowerOutput(XPOWERS_ALDO2);
#endif #endif
#ifdef HELTEC_TRACKER_V1_X
uint8_t tft_vext_enabled = digitalRead(VEXT_ENABLE);
#endif
#if !ARCH_PORTDUINO #if !ARCH_PORTDUINO
dispdev->displayOn(); dispdev->displayOn();
#endif #endif
#ifdef PIN_EINK_EN
if (uiconfig.screen_brightness == 1)
digitalWrite(PIN_EINK_EN, HIGH);
#elif defined(PCA_PIN_EINK_EN)
if (uiconfig.screen_brightness == 1)
io.digitalWrite(PCA_PIN_EINK_EN, HIGH);
#endif
#if defined(ST7789_CS) && \ #if defined(ST7789_CS) && \
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here. !defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
@@ -442,7 +401,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
dispdev->displayOn(); dispdev->displayOn();
#ifdef HELTEC_TRACKER_V1_X #ifdef HELTEC_TRACKER_V1_X
// If the TFT VEXT power is not enabled, initialize the UI.
if (!tft_vext_enabled) {
ui->init(); ui->init();
}
#endif #endif
#ifdef USE_ST7789 #ifdef USE_ST7789
pinMode(VTFT_CTRL, OUTPUT); pinMode(VTFT_CTRL, OUTPUT);
@@ -464,13 +426,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead // eInkScreensaver parameter is usually NULL (default argument), default frame used instead
setScreensaverFrames(einkScreensaver); setScreensaverFrames(einkScreensaver);
#endif #endif
#ifdef ELECROW_ThinkNode_M1
#ifdef PIN_EINK_EN if (digitalRead(PIN_EINK_EN) == HIGH) {
digitalWrite(PIN_EINK_EN, LOW); digitalWrite(PIN_EINK_EN, LOW);
#elif defined(PCA_PIN_EINK_EN) }
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
#endif #endif
dispdev->displayOff(); dispdev->displayOff();
#ifdef USE_ST7789 #ifdef USE_ST7789
SPI1.end(); SPI1.end();
@@ -589,7 +549,7 @@ 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();
@@ -619,13 +579,13 @@ 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();
} }
} }
#elif HAS_TOUCHSCREEN && !defined(USE_EINK) #elif HAS_TOUCHSCREEN
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();
@@ -681,11 +641,6 @@ void Screen::forceDisplay(bool forceUiUpdate)
// Tell EInk class to update the display // Tell EInk class to update the display
static_cast<EInkDisplay *>(dispdev)->forceDisplay(); static_cast<EInkDisplay *>(dispdev)->forceDisplay();
#else
// No delay between UI frame rendering
if (forceUiUpdate) {
setFastFramerate();
}
#endif #endif
} }
@@ -731,7 +686,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) {
menuHandler::OnboardMessage(); menuHandler::LoraRegionPicker(0);
} }
#endif #endif
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
@@ -752,19 +707,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
@@ -786,9 +735,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;
@@ -824,7 +771,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
@@ -915,17 +861,10 @@ 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
graphics::UIRenderer::rebuildFavoritedNodes();
LOG_DEBUG("Show standard frames"); LOG_DEBUG("Show standard frames");
showingNormalScreen = true; showingNormalScreen = true;
@@ -942,91 +881,87 @@ 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 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
// 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);
}
if (!hiddenFrames.nodelist_hopsignal) {
fsi.positions.nodelist_hopsignal = numframes; fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal); indicatorIcons.push_back(icon_signal);
}
if (!hiddenFrames.nodelist_distance) {
fsi.positions.nodelist_distance = numframes; fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance); 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);
}
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes; fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass); 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; // We don't show the node info of our node (if we have it yet - we should)
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; size_t numMeshNodes = nodeDB->getNumMeshNodes();
indicatorIcons.push_back(small_chirpy); if (numMeshNodes > 0)
numMeshNodes--;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
if (fsi.positions.firstFavorite == 255)
fsi.positions.firstFavorite = numframes;
fsi.positions.lastFavorite = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo;
indicatorIcons.push_back(icon_node);
}
} }
#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);
@@ -1034,7 +969,7 @@ void Screen::setFrames(FrameFocus focus)
#endif #endif
// Beware of what changes you make in this code! // Beware of what changes you make in this code!
// We pass numframes into GetMeshModulesWithUIFrames() which is highly important! // We pass numfames into GetMeshModulesWithUIFrames() which is highly important!
// Inside of that callback, goes over to MeshModule.cpp and we run // Inside of that callback, goes over to MeshModule.cpp and we run
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
// entries until we're ready to start building the matching entries. // entries until we're ready to start building the matching entries.
@@ -1063,36 +998,6 @@ void Screen::setFrames(FrameFocus focus)
LOG_DEBUG("Added modules. numframes: %d", numframes); LOG_DEBUG("Added modules. numframes: %d", numframes);
// We don't show the node info of our node (if we have it yet - we should)
size_t numMeshNodes = nodeDB->getNumMeshNodes();
if (numMeshNodes > 0)
numMeshNodes--;
if (!hiddenFrames.show_favorites) {
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
}
}
// Insert favorite frames *after* collecting them all
if (!favoriteFrames.empty()) {
fsi.positions.firstFavorite = numframes;
for (const auto &f : favoriteFrames) {
normalFrames[numframes++] = f;
indicatorIcons.push_back(icon_node);
}
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
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
LOG_DEBUG("Finished build frames. numframes: %d", numframes); LOG_DEBUG("Finished build frames. numframes: %d", numframes);
@@ -1104,7 +1009,8 @@ void Screen::setFrames(FrameFocus focus)
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list
// just changed)
// Focus on a specific frame, in the frame set we just created // Focus on a specific frame, in the frame set we just created
switch (focus) { switch (focus) {
@@ -1129,7 +1035,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:
@@ -1157,101 +1063,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;
} }
@@ -1403,7 +1238,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
@@ -1411,28 +1246,17 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Incoming message // Incoming message
devicestate.has_rx_text_message = true; // Needed to include the message frame devicestate.has_rx_text_message = true; // Needed to include the message frame
hasUnreadMessage = true; // Enables mail icon in the header hasUnreadMessage = true; // Enables mail icon in the header
// Always update frame list to include new message, but defer UI updates if virtual keyboard is active
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
// Only wake/force display if the configuration allows it
if (shouldWakeOnReceivedMessage()) {
setOn(true); // Wake up the screen first
forceDisplay(); // Forces screen redraw forceDisplay(); // Forces screen redraw
}
} else {
// Virtual keyboard is active - just mark that frames need regeneration when keyboard closes
// The devicestate and hasUnreadMessage are already set above, so message will appear later
LOG_DEBUG("Virtual keyboard active - deferring frame list update for new message");
}
// Show message alert - either as normal banner or as keyboard popup // === Prepare banner content ===
// === Common variables ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
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];
// Check for bell character in message to determine alert type // 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++) { for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
@@ -1442,9 +1266,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
} }
} }
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
// === Normal banner mode ===
char banner[256];
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);
@@ -1458,41 +1279,8 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
strcpy(banner, "New Message"); strcpy(banner, "New Message");
} }
} }
screen->showSimpleBanner(banner, 3000); screen->showSimpleBanner(banner, 3000);
} else {
// === Virtual keyboard popup mode ===
char title[64];
if (isAlert) {
if (longName && longName[0]) {
snprintf(title, sizeof(title), "Alert from %s", longName);
} else {
strcpy(title, "Alert Received");
}
} else {
if (longName && longName[0]) {
snprintf(title, sizeof(title), "%s", longName);
} else {
strcpy(title, "New Message");
}
}
// Prepare content - clean the message content
char content[200];
size_t contentLen = 0;
for (size_t i = 0; i < packet->decoded.payload.size && i < sizeof(content) - 1; i++) {
if (msgRaw[i] != '\x07' && msgRaw[i] != '\0') { // Skip bell character and null
content[contentLen++] = msgRaw[i];
}
}
content[contentLen] = '\0';
// Show popup with title and content on virtual keyboard
NotificationRenderer::showKeyboardMessagePopupWithTitle(title, content, 5000);
// Force display update to show the popup immediately
setFastFramerate();
forceDisplay();
}
} }
} }
@@ -1502,11 +1290,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)
@@ -1529,16 +1312,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
@@ -1546,7 +1319,7 @@ int Screen::handleInputEvent(const InputEvent *event)
setFastFramerate(); // Draw ASAP setFastFramerate(); // Draw ASAP
#endif #endif
if (NotificationRenderer::isOverlayBannerShowing()) { if (NotificationRenderer::isOverlayBannerShowing()) {
NotificationRenderer::inEvent = *event; NotificationRenderer::inEvent = event->inputEvent;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP setFastFramerate(); // Draw ASAP
@@ -1576,7 +1349,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) {
@@ -1585,13 +1358,10 @@ 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) { devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu(); menuHandler::messageResponseMenu();
} else {
menuHandler::textMessageBaseMenu();
}
} 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 &&
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
@@ -1643,23 +1413,3 @@ bool Screen::isOverlayBannerShowing()
#else #else
graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {}
#endif // HAS_SCREEN #endif // HAS_SCREEN
bool shouldWakeOnReceivedMessage()
{
/*
The goal here is to determine when we do NOT wake up the screen on message received:
- Any ext. notifications are turned on
- If role is not client / client_mute
- If the battery level is very low
*/
if (moduleConfig.external_notification.enabled) {
return false;
}
if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
return false;
}
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {
return false;
}
return true;
}

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;
@@ -26,8 +26,6 @@ struct BannerOverlayOptions {
}; };
} // namespace graphics } // namespace graphics
bool shouldWakeOnReceivedMessage();
#if !HAS_SCREEN #if !HAS_SCREEN
#include "power.h" #include "power.h"
namespace graphics namespace graphics
@@ -94,7 +92,6 @@ class Screen
#include "commands.h" #include "commands.h"
#include "concurrency/LockGuard.h" #include "concurrency/LockGuard.h"
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "graphics/draw/MenuHandler.h"
#include "input/InputBroker.h" #include "input/InputBroker.h"
#include "mesh/MeshModule.h" #include "mesh/MeshModule.h"
#include "modules/AdminModule.h" #include "modules/AdminModule.h"
@@ -311,16 +308,8 @@ class Screen : public concurrency::OSThread
void showSimpleBanner(const char *message, uint32_t durationMs = 0); void showSimpleBanner(const char *message, uint32_t durationMs = 0);
void showOverlayBanner(BannerOverlayOptions); void showOverlayBanner(BannerOverlayOptions);
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(int)> 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)
{
graphics::menuHandler::menuQueue = menuToShow;
runNow();
}
void startFirmwareUpdateScreen() void startFirmwareUpdateScreen()
{ {
@@ -593,11 +582,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
@@ -659,7 +644,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;
@@ -669,7 +654,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;
@@ -678,29 +662,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

@@ -16,7 +16,7 @@
#include "graphics/fonts/OLEDDisplayFontsCS.h" #include "graphics/fonts/OLEDDisplayFontsCS.h"
#endif #endif
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #ifdef CROWPANEL_ESP32S3_5_EPAPER
#include "graphics/fonts/EinkDisplayFonts.h" #include "graphics/fonts/EinkDisplayFonts.h"
#endif #endif
@@ -40,9 +40,6 @@
#ifdef OLED_PL #ifdef OLED_PL
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19 #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_PL // Height: 19
#else #else
#ifdef OLED_RU
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_RU // Height: 19
#else
#ifdef OLED_UA #ifdef OLED_UA
#define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19 #define FONT_MEDIUM_LOCAL ArialMT_Plain_16_UA // Height: 19
#else #else
@@ -53,13 +50,9 @@
#endif #endif
#endif #endif
#endif #endif
#endif
#ifdef OLED_PL #ifdef OLED_PL
#define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28 #define FONT_LARGE_LOCAL ArialMT_Plain_24_PL // Height: 28
#else #else
#ifdef OLED_RU
#define FONT_LARGE_LOCAL ArialMT_Plain_24_RU // Height: 28
#else
#ifdef OLED_UA #ifdef OLED_UA
#define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28 #define FONT_LARGE_LOCAL ArialMT_Plain_24_UA // Height: 28
#else #else
@@ -70,10 +63,9 @@
#endif #endif
#endif #endif
#endif #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
@@ -85,7 +77,7 @@
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
#endif #endif
#if defined(CROWPANEL_ESP32S3_5_EPAPER) && defined(USE_EINK) #if defined(CROWPANEL_ESP32S3_5_EPAPER)
#undef FONT_SMALL #undef FONT_SMALL
#undef FONT_MEDIUM #undef FONT_MEDIUM
#undef FONT_LARGE #undef FONT_LARGE

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);
@@ -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,15 +202,11 @@ 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(dateLine);
} else {
timeStrWidth = display->getStringWidth(timeStr); 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 ===
int iconRightEdge = timeX - 2; int iconRightEdge = timeX - 1;
bool showMail = false; bool showMail = false;
@@ -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);
@@ -311,17 +287,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} }
} }
if (show_date) {
// === Draw Date ===
display->drawString(timeX, textY, dateLine);
if (isBold)
display->drawString(timeX - 1, textY, dateLine);
} else {
// === Draw Time === // === Draw Time ===
display->drawString(timeX, textY, timeStr); display->drawString(timeX, textY, timeStr);
if (isBold) if (isBold)
display->drawString(timeX - 1, textY, timeStr); 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 ===

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
@@ -752,42 +667,33 @@ 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 <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip #include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
#if defined(LGFX_SDL)
#include <lgfx/v1/platforms/sdl/Panel_sdl.hpp>
#endif
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(LGFX_SDL)
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!");
@@ -795,66 +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.
_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(LGFX_SDL)
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.
} }
}; };
@@ -949,29 +849,9 @@ static LGFX *tft = nullptr;
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp> #include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp> #include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
class PanelInit_ST7701 : public lgfx::Panel_ST7701
{
public:
const uint8_t *getInitCommands(uint8_t listno) const override
{
// 180 degree hw rotation: vertical flip, horizontal flip
static constexpr const uint8_t list1[] = {0x36, 1, 0x10, // MADCTL for vertical flip
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x10, // Command2 BK0 SEL
0xC7, 1, 0x04, // SDIR: X-direction Control (Horizontal Flip)
0xFF, 5, 0x77, 0x01, 0x00, 0x00, 0x00, // Command2 BK0 DIS
0xFF, 0xFF};
switch (listno) {
case 1:
return list1;
default:
return lgfx::Panel_ST7701::getInitCommands(listno);
}
}
};
class LGFX : public lgfx::LGFX_Device class LGFX : public lgfx::LGFX_Device
{ {
PanelInit_ST7701 _panel_instance; lgfx::Panel_ST7701 _panel_instance;
lgfx::Bus_RGB _bus_instance; lgfx::Bus_RGB _bus_instance;
lgfx::Light_PWM _light_instance; lgfx::Light_PWM _light_instance;
lgfx::Touch_FT5x06 _touch_instance; lgfx::Touch_FT5x06 _touch_instance;
@@ -1082,9 +962,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>
@@ -1115,10 +994,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)
@@ -1128,154 +1007,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);
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
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) {
for (x = 0; x < displayWidth; x++) { for (x = 0; x < displayWidth; x++) {
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) auto isset = buffer[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) { if (!fromBlank) {
// get src pixel in the page based ordering the OLED lib uses // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7));
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(LGFX_SDL)
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 && !lgfx::v1::gpio_in(lastPressed))
return;
if (!lgfx::v1::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 (!lgfx::v1::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 (!lgfx::v1::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 (!lgfx::v1::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 (!lgfx::v1::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)
@@ -1288,8 +1050,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();
@@ -1312,8 +1074,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,23 +1184,15 @@ bool TFTDisplay::connect()
attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING); attachInterrupt(digitalPinToInterrupt(SCREEN_TOUCH_INT), rak14014_tpIntHandle, FALLING);
#elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2) #elif defined(T_DECK) || defined(PICOMPUTER_S3) || defined(CHATTER_2)
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) || defined(SENSECAP_INDICATOR)
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
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

@@ -2,6 +2,7 @@
#if HAS_SCREEN #if HAS_SCREEN
#include "ClockRenderer.h" #include "ClockRenderer.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "UIRenderer.h"
#include "configuration.h" #include "configuration.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "gps/RTC.h" #include "gps/RTC.h"
@@ -185,11 +186,11 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
{ {
display->clear(); display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
int line = 1;
// === 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);
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
@@ -217,6 +218,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
hour %= 12; hour %= 12;
if (hour == 0) if (hour == 0)
hour = 12; hour = 12;
bool isPM = hour >= 12;
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
} else { } else {
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
@@ -228,8 +230,6 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
float scale = 1.5; float scale = 1.5;
#elif defined(CHATTER_2)
float scale = 1.1;
#else #else
float scale = 0.75; float scale = 0.75;
if (isHighResolution) { if (isHighResolution) {
@@ -285,15 +285,11 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
int yOffset = (isHighResolution) ? 3 : 1; int yOffset = (isHighResolution) ? 3 : 1;
#ifdef SENSECAP_INDICATOR #ifdef SENSECAP_INDICATOR
yOffset -= 3; yOffset -= 3;
#endif
#ifdef T_DECK
yOffset -= 5;
#endif #endif
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
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,
@@ -313,7 +309,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);
#ifdef T_WATCH_S3 #ifdef T_WATCH_S3
if (nimbleBluetooth && nimbleBluetooth->isConnected()) { if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
@@ -366,7 +362,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
// hour hand radius and y coordinate // hour hand radius and y coordinate
int16_t hourHandRadius = radius * 0.35; int16_t hourHandRadius = radius * 0.35;
if (isHighResolution) { if (isHighResolution) {
hourHandRadius = radius * 0.55; int16_t hourHandRadius = radius * 0.55;
} }
int16_t hourHandNoonY = centerY - hourHandRadius; int16_t hourHandNoonY = centerY - hourHandRadius;
@@ -385,7 +381,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
bool isPM = hour >= 12; bool isPM = hour >= 12;
if (config.display.use_12h_clock) { if (config.display.use_12h_clock) {
isPM = hour >= 12; bool isPM = hour >= 12;
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
int yOffset = isHighResolution ? 1 : 0; int yOffset = isHighResolution ? 1 : 0;
#ifdef USE_EINK #ifdef USE_EINK

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