Compare commits

..

40 Commits

Author SHA1 Message Date
mverch67
6b05f090dc reduce PSRAM allocated to lvgl 2025-09-18 10:17:58 +02:00
Manuel
e9b87f7a81 Merge branch 'master' into arduino-esp32-v3.2 2025-09-18 09:48:51 +02:00
Manuel
d47d249550 Merge branch 'master' into arduino-esp32-v3.2 2025-09-12 21:15:43 +02:00
Manuel
ff28355e16 Merge branch 'master' into arduino-esp32-v3.2 2025-09-08 19:20:15 +02:00
Manuel
351cbd723b Merge branch 'master' into arduino-esp32-v3.2 2025-09-08 10:41:14 +02:00
Manuel
be1a724ab4 Merge branch 'master' into arduino-esp32-v3.2 2025-08-10 00:11:27 +02:00
mverch67
04f98cf20e update config; include wifi 2025-08-09 23:49:16 +02:00
mverch67
75a1a41d8c fix duplicate Syslog classes 2025-08-09 23:43:45 +02:00
mverch67
344efa6675 update device-ui (fix native compilation) 2025-08-08 17:29:02 +02:00
mverch67
e919b3da9c cleanup 2025-08-06 10:36:04 +02:00
Manuel
155783c782 Merge branch 'master' into arduino-esp32-v3.2 2025-08-06 09:15:44 +02:00
Manuel
76bcd05259 Merge branch 'master' into arduino-esp32-v3.2 2025-08-06 00:09:37 +02:00
mverch67
2d29cbc34c device-ui: use fast mem for critical draw functions 2025-08-06 00:08:54 +02:00
mverch67
2b77864192 fix attachInterrupt issue 2025-08-05 23:59:37 +02:00
mverch67
10aa4cbfee don't exclude I2C 2025-08-02 19:07:34 +02:00
mverch67
c24a174490 don't exclude I2C 2025-08-02 19:05:07 +02:00
Manuel
3e5e19efec Merge branch 'master' into arduino-esp32-v3.2 2025-08-01 21:50:40 +02:00
mverch67
ea2c247024 pr: update device-ui for indicator 2025-08-01 21:45:40 +02:00
mverch67
0b463b6443 update references to fixed io expander 2025-08-01 20:54:57 +02:00
mverch67
7d3c804279 update wdt task 2025-08-01 20:54:15 +02:00
mverch67
1fa7d9f40e try-fix watchdog trigger 2025-08-01 09:23:01 +02:00
mverch67
aa7abb1d3e use ng-io-expander 3.3.0 2025-07-30 23:37:09 +02:00
mverch67
effb454af0 comment out assert on watchdog creation 2025-07-30 23:24:31 +02:00
mverch67
27bb42ed3b git ignore new toolchain tmp files 2025-07-29 22:08:07 +02:00
mverch67
05dc8caed8 platformio updates 2025-07-29 22:07:18 +02:00
mverch67
f3734d407d remove OTA partition for now until fw size got smaller again 2025-07-29 22:06:21 +02:00
mverch67
bc8e509a8c fix copy&paste 2025-07-29 22:05:29 +02:00
mverch67
74d0472834 fix merge again 2025-07-29 21:55:34 +02:00
mverch67
5afc43ebc1 try-out WDT timer issue --> needs revert 2025-07-29 21:07:28 +02:00
mverch67
6cdb92b7a8 fix merge error 2025-07-29 21:06:19 +02:00
Thomas Göttgens
0e1872398a Merge remote-tracking branch 'remotes/origin/master' into arduino-esp32-v3.2
# Conflicts:
#	arch/esp32/esp32.ini
#	src/graphics/draw/MenuHandler.cpp
#	src/modules/Modules.cpp
#	variants/esp32s3/seeed-sensecap-indicator/platformio.ini
2025-07-29 13:06:13 +02:00
mverch67
f25544c7e0 updated platform references 2025-07-18 03:16:54 +02:00
mverch67
f40b2ba153 temp workaround for nimble compile 2025-07-18 03:14:45 +02:00
mverch67
5033fd1f9f fix MESHTASTIC_EXCLUDE_I2C compile errors 2025-07-17 12:40:02 +02:00
mverch67
cd170fb011 boot_freq 120MHz 2025-07-16 20:50:25 +02:00
mverch67
fa169a5e43 compare IDF versions 2025-07-14 17:14:21 +02:00
mverch67
c6e2a53a02 exclude not needed modules 2025-07-03 17:08:29 +02:00
Ben Meadors
a286c06271 Merge branch 'master' into arduino-esp32-v3.2 2025-07-02 06:00:44 -05:00
Manuel
dda4b90e34 Merge branch 'master' into arduino-esp32-v3.2 2025-07-01 00:56:56 +02:00
mverch67
cc5dc5046c indicator: adaptations for arduino-esp32 v3.2 2025-07-01 00:48:11 +02:00
224 changed files with 2329 additions and 7740 deletions

View File

@@ -76,7 +76,7 @@ bool loopCanSleep()
// Called just prior to starting Meshtastic. Allows for setting config values before startup.
void lateInitVariant()
{
portduino_config.logoutputlevel = level_error;
settingsMap[logoutputlevel] = level_error;
channelFile.channels[0] = meshtastic_Channel{
.has_settings = true,
.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.
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
portduino_config.maxtophone = 5;
settingsMap[maxtophone] = 5;
meshtasticThread = std::thread([program = *argv[0]]() {
char nodeIdStr[12];

View File

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

View File

@@ -1,232 +0,0 @@
name: Build One Arch
on:
workflow_dispatch:
inputs:
# trunk-ignore(checkov/CKV_GHA_7)
arch:
type: choice
options:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- native
permissions: read-all
env:
INPUT_ARCH: ${{ github.event.inputs.arch }}
jobs:
setup:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.x
cache: pip
- run: pip install -U platformio
- name: Generate matrix
id: jsonStep
run: |
TARGETS=$(./bin/generate_ci_matrix.py $INPUT_ARCH --level extra)
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "selected_arch=$TARGETS" >> $GITHUB_OUTPUT
outputs:
selected_arch: ${{ steps.jsonStep.outputs.selected_arch }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
id: version
env:
BUILD_LOCATION: local
outputs:
long: ${{ steps.version.outputs.long }}
deb: ${{ steps.version.outputs.deb }}
build:
if: ${{ github.event_name != 'workflow_dispatch' }}
needs: [setup, version]
strategy:
fail-fast: false
matrix:
build: ${{ fromJson(needs.setup.outputs.selected_arch) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.arch }}
build-debian-src:
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/build_debian_src.yml
with:
series: UNRELEASED
build_location: local
secrets: inherit
package-pio-deps-native-tft:
if: ${{ inputs.arch == 'native' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.event_name != 'workflow_dispatch' || !contains(github.ref_name, 'event/') && inputs.arch == 'native' }}
uses: ./.github/workflows/test_native.yml
docker-deb-amd64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-deb-amd64-tft:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-alp-amd64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
docker-alp-amd64-tft:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
platform: linux/amd64
runs-on: ubuntu-24.04
push: false
pio_env: native-tft
docker-deb-arm64:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm64
runs-on: ubuntu-24.04-arm
push: false
docker-deb-armv7:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.arch == 'native' }}
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
platform: linux/arm/v7
runs-on: ubuntu-24.04-arm
push: false
gather-artifacts:
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
arch:
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
runs-on: ubuntu-latest
needs: [version, build]
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
with:
path: ./
pattern: firmware-${{inputs.arch}}-*
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R
- name: Move files up
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
- name: Repackage in single firmware zip
uses: actions/upload-artifact@v4
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
overwrite: true
path: |
./firmware-*.bin
./firmware-*.uf2
./firmware-*.hex
./firmware-*-ota.zip
./device-*.sh
./device-*.bat
./littlefs-*.bin
./bleota*bin
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
path: ./output
# For diagnostics
- name: Show artifacts
run: ls -lR
- name: Device scripts permissions
run: |
chmod +x ./output/device-install.sh
chmod +x ./output/device-update.sh
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip ./output
- name: Repackage in single elfs zip
uses: actions/upload-artifact@v4
with:
name: debug-elfs-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip
overwrite: true
path: ./*.elf
retention-days: 30
- uses: scruplelesswizard/comment-artifact@main
if: ${{ github.event_name == 'pull_request' }}
with:
name: firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}
description: "Download firmware-${{inputs.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

View File

@@ -27,12 +27,18 @@ on:
jobs:
setup:
if: github.repository == 'meshtastic/firmware'
strategy:
fail-fast: true
fail-fast: false
matrix:
arch:
- all
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- check
runs-on: ubuntu-24.04
steps:
@@ -42,27 +48,28 @@ jobs:
python-version: 3.x
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: |
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
else
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr)
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
echo "$TARGETS" >> $GITHUB_STEP_SUMMARY
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
outputs:
all: ${{ steps.jsonStep.outputs.all }}
esp32: ${{ steps.jsonStep.outputs.esp32 }}
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }}
version:
if: github.repository == 'meshtastic/firmware'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -81,30 +88,105 @@ jobs:
needs: setup
strategy:
fail-fast: false
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
matrix: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- name: Build base
id: base
uses: ./.github/actions/setup-base
- name: Check ${{ matrix.check.board }}
run: bin/check-all.sh ${{ matrix.check.board }}
- name: Check ${{ matrix.board }}
run: bin/check-all.sh ${{ matrix.board }}
build:
build-esp32:
needs: [setup, version]
strategy:
fail-fast: false
matrix:
build: ${{ fromJson(needs.setup.outputs.all) }}
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.platform }}
pio_env: ${{ matrix.board }}
platform: esp32
build-esp32s3:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32s3
build-esp32c3:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c3
build-esp32c6:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c6
build-nrf52840:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: nrf52840
build-rp2040:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2040
build-rp2350:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2350
build-stm32:
needs: [setup, version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: stm32
build-debian-src:
if: github.repository == 'meshtastic/firmware'
@@ -115,18 +197,17 @@ jobs:
secrets: inherit
package-pio-deps-native-tft:
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/package_pio_deps.yml
with:
pio_env: native-tft
secrets: inherit
test-native:
if: ${{ !contains(github.ref_name, 'event/') && github.repository == 'meshtastic/firmware' }}
if: ${{ !contains(github.ref_name, 'event/') }}
uses: ./.github/workflows/test_native.yml
docker-deb-amd64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -135,7 +216,6 @@ jobs:
push: false
docker-deb-amd64-tft:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -145,7 +225,6 @@ jobs:
pio_env: native-tft
docker-alp-amd64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
@@ -154,7 +233,6 @@ jobs:
push: false
docker-alp-amd64-tft:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: alpine
@@ -164,7 +242,6 @@ jobs:
pio_env: native-tft
docker-deb-arm64:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -173,7 +250,6 @@ jobs:
push: false
docker-deb-armv7:
if: github.repository == 'meshtastic/firmware'
uses: ./.github/workflows/docker_build.yml
with:
distro: debian
@@ -183,7 +259,6 @@ jobs:
gather-artifacts:
# trunk-ignore(checkov/CKV2_GHA_1)
if: github.repository == 'meshtastic/firmware'
permissions:
contents: write
pull-requests: write
@@ -200,7 +275,18 @@ jobs:
- rp2350
- stm32
runs-on: ubuntu-latest
needs: [version, build]
needs:
[
version,
build-esp32,
build-esp32s3,
build-esp32c3,
build-esp32c6,
build-nrf52840,
build-rp2040,
build-rp2350,
build-stm32,
]
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -272,7 +358,7 @@ jobs:
release-artifacts:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware' }}
if: ${{ github.event_name == 'workflow_dispatch' }}
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
needs:
@@ -347,7 +433,7 @@ jobs:
- rp2350
- stm32
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' && github.repository == 'meshtastic/firmware'}}
if: ${{ github.event_name == 'workflow_dispatch' }}
needs: [release-artifacts, version]
steps:
- name: Checkout

View File

@@ -7,18 +7,28 @@ on:
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
merge_group:
env:
FAIL_FAST_PER_ARCH: true
jobs:
setup:
strategy:
fail-fast: true
matrix:
arch:
- all
- esp32
- esp32s3
- esp32c3
- esp32c6
- nrf52840
- rp2040
- rp2350
- stm32
- check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
cache: pip
@@ -29,18 +39,25 @@ jobs:
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
else
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} --level pr)
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
fi
echo "Name: $GITHUB_REF_NAME Base: $GITHUB_BASE_REF Ref: $GITHUB_REF"
echo "${{matrix.arch}}=$TARGETS" >> $GITHUB_OUTPUT
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
outputs:
all: ${{ steps.jsonStep.outputs.all }}
esp32: ${{ steps.jsonStep.outputs.esp32 }}
esp32s3: ${{ steps.jsonStep.outputs.esp32s3 }}
esp32c3: ${{ steps.jsonStep.outputs.esp32c3 }}
esp32c6: ${{ steps.jsonStep.outputs.esp32c6 }}
nrf52840: ${{ steps.jsonStep.outputs.nrf52840 }}
rp2040: ${{ steps.jsonStep.outputs.rp2040 }}
rp2350: ${{ steps.jsonStep.outputs.rp2350 }}
stm32: ${{ steps.jsonStep.outputs.stm32 }}
check: ${{ steps.jsonStep.outputs.check }}
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Get release version string
run: |
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
@@ -56,29 +73,105 @@ jobs:
needs: setup
strategy:
fail-fast: true
matrix:
check: ${{ fromJson(needs.setup.outputs.check) }}
matrix: ${{ fromJson(needs.setup.outputs.check) }}
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Build base
id: base
uses: ./.github/actions/setup-base
- name: Check ${{ matrix.check.board }}
run: bin/check-all.sh ${{ matrix.check.board }}
- name: Check ${{ matrix.board }}
run: bin/check-all.sh ${{ matrix.board }}
build:
build-esp32:
needs: [setup, version]
strategy:
matrix:
build: ${{ fromJson(needs.setup.outputs.all) }}
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.esp32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.build.board }}
platform: ${{ matrix.build.platform }}
pio_env: ${{ matrix.board }}
platform: esp32
build-esp32s3:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.esp32s3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32s3
build-esp32c3:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.esp32c3) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c3
build-esp32c6:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.esp32c6) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: esp32c6
build-nrf52840:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.nrf52840) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: nrf52840
build-rp2040:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.rp2040) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2040
build-rp2350:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.rp2350) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: rp2350
build-stm32:
needs: [setup, version]
strategy:
fail-fast: ${{ vars.FAIL_FAST_PER_ARCH == true }}
matrix: ${{ fromJson(needs.setup.outputs.stm32) }}
uses: ./.github/workflows/build_firmware.yml
with:
version: ${{ needs.version.outputs.long }}
pio_env: ${{ matrix.board }}
platform: stm32
build-debian-src:
if: github.repository == 'meshtastic/firmware'
@@ -167,15 +260,26 @@ jobs:
- rp2350
- stm32
runs-on: ubuntu-latest
needs: [version, build]
needs:
[
version,
build-esp32,
build-esp32s3,
build-esp32c3,
build-esp32c6,
build-nrf52840,
build-rp2040,
build-rp2350,
build-stm32,
]
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
path: ./
pattern: firmware-${{matrix.arch}}-*
@@ -204,7 +308,7 @@ jobs:
./Meshtastic_nRF52_factory_erase*.uf2
retention-days: 30
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -249,10 +353,10 @@ jobs:
- package-pio-deps-native-tft
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.x
@@ -268,14 +372,14 @@ jobs:
Autogenerated by github action, developer should edit as required before publishing...
- name: Download source deb
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
pattern: firmware-debian-${{ needs.version.outputs.deb }}~UNRELEASED-src
merge-multiple: true
path: ./output/debian-src
- name: Download `native-tft` pio deps
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -318,14 +422,14 @@ jobs:
needs: [release-artifacts, version]
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.x
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
merge-multiple: true
@@ -342,7 +446,7 @@ jobs:
- name: Zip firmware
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
merge-multiple: true
@@ -373,14 +477,14 @@ jobs:
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.x
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
merge-multiple: true

View File

@@ -10,7 +10,7 @@ permissions:
jobs:
check-label:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Check for PR labels
uses: actions/github-script@v8

View File

@@ -21,10 +21,10 @@ jobs:
fail-fast: false
matrix:
series:
- jammy # 22.04 LTS
- noble # 24.04 LTS
- jammy # 22.04
- noble # 24.04
- plucky # 25.04
- questing # 25.10
# - questing # 25.10
uses: ./.github/workflows/package_ppa.yml
with:
ppa_repo: |-

View File

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

View File

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

9
.gitignore vendored
View File

@@ -39,5 +39,14 @@ release/
src/mesh/raspihttp/certificate.pem
src/mesh/raspihttp/private_key.pem
# pioarduino platform
managed_components/*
arduino-lib-builder*
dependencies.lock
idf_component.yml
CMakeLists.txt
sdkconfig.*
.dummy/*
# Ignore logo (set at build time with platformio-custom.py)
data/boot/logo.*

View File

@@ -8,25 +8,25 @@ plugins:
uri: https://github.com/trunk-io/plugins
lint:
enabled:
- checkov@3.2.473
- renovate@41.132.5
- checkov@3.2.471
- renovate@41.115.2
- prettier@3.6.2
- trufflehog@3.90.8
- trufflehog@3.90.6
- yamllint@1.37.1
- bandit@1.8.6
- trivy@0.67.0
- trivy@0.66.0
- taplo@0.10.0
- ruff@0.13.3
- isort@6.1.0
- ruff@0.13.0
- isort@6.0.1
- markdownlint@0.45.0
- oxipng@9.1.5
- svgo@4.0.0
- actionlint@1.7.7
- flake8@7.3.0
- hadolint@2.14.0
- hadolint@2.13.1
- shfmt@3.6.0
- shellcheck@0.11.0
- black@25.9.0
- black@25.1.0
- git-diff-check
- gitleaks@8.28.0
- clang-format@16.0.3

6
8MB_no_ota.csv Normal file
View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x660000,
spiffs, data, spiffs, 0x670000,0x180000,
coredump, data, coredump,0x7F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x660000
5 spiffs data spiffs 0x670000 0x180000
6 coredump data coredump 0x7F0000 0x10000

View File

@@ -36,7 +36,6 @@ build_flags =
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
-DSERIAL_BUFFER_SIZE=4096
-DSERIAL_HAS_ON_RECEIVE
-DLIBPAX_ARDUINO
-DLIBPAX_WIFI
-DLIBPAX_BLE
@@ -53,8 +52,8 @@ lib_deps =
https://github.com/meshtastic/esp32_https_server/archive/3223704846752e6d545139204837bdb2a55459ca.zip
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
h2zero/NimBLE-Arduino@^1.4.3
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/mverch67/libpax gitBranch=master
https://github.com/mverch67/libpax/archive/6f52ee989301cdabaeef00bcbf93bff55708ce2f.zip
# renovate: datasource=github-tags depName=XPowersLib packageName=lewisxhe/XPowersLib
https://github.com/lewisxhe/XPowersLib/archive/v0.3.0.zip
# renovate: datasource=git-refs depName=meshtastic-ESP32_Codec2 packageName=https://github.com/meshtastic/ESP32_Codec2 gitBranch=master

View File

@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/d3f6e339534233c7217818867368767590ce549e.zip
https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip
framework = arduino
build_src_filter =

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
PYTHON=${PYTHON:-$(which python3 python | head -n 1)}
BPS_RESET=false

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
CHANGE_MODE=false

View File

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

View File

@@ -87,15 +87,6 @@
</screenshots>
<releases>
<release version="2.7.12" date="2025-10-01">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.12</url>
</release>
<release version="2.7.11" date="2025-09-24">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.11</url>
</release>
<release version="2.7.10" date="2025-09-18">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.10</url>
</release>
<release version="2.7.9" date="2025-09-03">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
</release>

View File

@@ -86,7 +86,7 @@ if platform.name == "espressif32":
if platform.name == "nordicnrf52":
env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex",
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py \"$BUILD_DIR/firmware.hex\" -c -f 0xADA52840 -o \"$BUILD_DIR/firmware.uf2\"",
env.VerboseAction(f"\"{sys.executable}\" ./bin/uf2conv.py $BUILD_DIR/firmware.hex -c -f 0xADA52840 -o $BUILD_DIR/firmware.uf2",
"Generating UF2 file"))
Import("projenv")

View File

@@ -1 +1 @@
2.6.6
2.6.4

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x8029"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x802A"]
],
"usb_product": "Muzi R1 Neo",
"mcu": "nrf52840",
"variant": "r1-neo",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino", "freertos"],
"name": "WisCore RAK4631 Board",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://muzi.works/",
"vendor": "Muzi Works"
}

View File

@@ -16,6 +16,7 @@
"f_cpu": "240000000L",
"f_flash": "80000000L",
"f_boot": "120000000L",
"boot_freq": "120000000L",
"boot": "qio",
"flash_mode": "qio",
"psram_type": "opi",

47
debian/changelog vendored
View File

@@ -1,13 +1,50 @@
meshtasticd (2.7.12.0) unstable; urgency=medium
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
[ Austin Lane ]
* Initial packaging
* Version 2.5.19
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ GitHub Actions ]
* Version 2.7.12
[ ]
* GitHub Actions Automatic version bump
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 01 Oct 2025 19:51:41 +0000
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ Ubuntu ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
* GitHub Actions 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,8 +1,7 @@
#!/usr/bin/bash
export DEBFULLNAME="GitHub Actions"
export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
PKG_VERSION=$(python3 bin/buildinfo.py short)
dch --newversion "$PKG_VERSION.0" \
--distribution unstable \
"Version $PKG_VERSION"
--distribution UNRELEASED \
"GitHub Actions Automatic version bump"

View File

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

View File

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

View File

@@ -55,8 +55,6 @@ build_flags = -Wno-missing-field-initializers
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
#-D OLED_PL=1
#-D DEBUG_HEAP=1 ; uncomment to add free heap space / memory leak debugging logs
#-D DEBUG_LOOP_TIMING=1 ; uncomment to add main loop timing logs
monitor_speed = 115200
monitor_filters = direct
@@ -70,7 +68,7 @@ lib_deps =
# renovate: datasource=git-refs depName=meshtastic-TinyGPSPlus packageName=https://github.com/meshtastic/TinyGPSPlus gitBranch=master
https://github.com/meshtastic/TinyGPSPlus/archive/71a82db35f3b973440044c476d4bcdc673b104f4.zip
# renovate: datasource=git-refs depName=meshtastic-ArduinoThread packageName=https://github.com/meshtastic/ArduinoThread gitBranch=master
https://github.com/meshtastic/ArduinoThread/archive/b841b0415721f1341ea41cccfb4adccfaf951567.zip
https://github.com/meshtastic/ArduinoThread/archive/7c3ee9e1951551b949763b1f5280f8db1fa4068d.zip
# renovate: datasource=custom.pio depName=Nanopb packageName=nanopb/library/Nanopb
nanopb/Nanopb@0.4.91
# renovate: datasource=custom.pio depName=ErriezCRC32 packageName=erriez/library/ErriezCRC32
@@ -89,8 +87,9 @@ check_flags =
framework = arduino
lib_deps =
${env.lib_deps}
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=end2endzone/library/NonBlockingRTTTL
end2endzone/NonBlockingRTTTL@1.3.0
# renovate: datasource=custom.pio depName=NonBlockingRTTTL packageName=mverch67/library/NonBlockingRTTTL
https://github.com/mverch67/NonBlockingRTTTL/archive/ad1c2fb12bc81db546c6a94e963acb3382d3689e.zip ; TODO
build_flags = ${env.build_flags} -Os
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
@@ -101,8 +100,6 @@ lib_deps =
thingsboard/TBPubSubClient@2.12.1
# renovate: datasource=custom.pio depName=NTPClient packageName=arduino-libraries/library/NTPClient
arduino-libraries/NTPClient@3.2.1
# renovate: datasource=custom.pio depName=Syslog packageName=arcao/library/Syslog
arcao/Syslog@2.0.0
; Minimal networking libs for nrf52 (excludes Syslog to save flash)
[nrf52_networking_base]
@@ -115,18 +112,18 @@ lib_deps =
[radiolib_base]
lib_deps =
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
jgromes/RadioLib@7.3.0
jgromes/RadioLib@7.2.1
[device-ui_base]
lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/e564d78ae1a7e9a225aaf4a73b1cb84c549f510f.zip
https://github.com/meshtastic/device-ui/archive/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
lib_deps =
# renovate: datasource=custom.pio depName=Adafruit BusIO packageName=adafruit/library/Adafruit BusIO
adafruit/Adafruit BusIO@1.17.4
adafruit/Adafruit BusIO@1.17.2
# renovate: datasource=custom.pio depName=Adafruit Unified Sensor packageName=adafruit/library/Adafruit Unified Sensor
adafruit/Adafruit Unified Sensor@1.1.15
# renovate: datasource=custom.pio depName=Adafruit BMP280 packageName=adafruit/library/Adafruit BMP280 Library

View File

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

View File

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

View File

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

View File

@@ -39,45 +39,3 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
break;
}
}
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
{
switch (role) {
case meshtastic_Config_DeviceConfig_Role_CLIENT:
return "Client";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
return "Client Mute";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
return "Client Hidden";
break;
case meshtastic_Config_DeviceConfig_Role_CLIENT_BASE:
return "Client Base";
break;
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
return "Lost and Found";
break;
case meshtastic_Config_DeviceConfig_Role_TRACKER:
return "Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_SENSOR:
return "Sensor";
break;
case meshtastic_Config_DeviceConfig_Role_TAK:
return "TAK";
break;
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
return "TAK Tracker";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER:
return "Router";
break;
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
return "Router Late";
break;
default:
return "Unknown";
break;
}
}

View File

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

View File

@@ -22,9 +22,6 @@ class GPSStatus : public Status
meshtastic_Position p = meshtastic_Position_init_default;
/// Time of last valid GPS fix (millis since boot)
uint32_t lastFixMillis = 0;
public:
GPSStatus() { statusType = STATUS_TYPE_GPS; }
@@ -86,9 +83,6 @@ class GPSStatus : public Status
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
{
#ifdef GPS_DEBUG
@@ -120,9 +114,6 @@ class GPSStatus : public Status
if (isDirty) {
if (hasLock) {
// Record time of last valid GPS fix
lastFixMillis = millis();
// 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,
p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -90,9 +90,7 @@ void OSThread::run()
if (heap < newHeap)
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
#endif
#ifdef DEBUG_LOOP_TIMING
LOG_DEBUG("====== Thread next run in: %d", newDelay);
#endif
runned();
if (newDelay >= 0)

View File

@@ -117,12 +117,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define SX126X_MAX_POWER 22
#endif
#ifdef USE_GC1109_PA
// Power Amps are often non-linear, so we can use an array of values for the power curve
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7
#endif
// Default system gain to 0 if not defined
#ifndef TX_GAIN_LORA
#define TX_GAIN_LORA 0
@@ -268,13 +262,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define VEXT_ON_VALUE LOW
#endif
// -----------------------------------------------------------------------------
// Rotary encoder
// -----------------------------------------------------------------------------
#ifndef ROTARY_DELAY
#define ROTARY_DELAY 5
#endif
// -----------------------------------------------------------------------------
// GPS
// -----------------------------------------------------------------------------

View File

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

View File

@@ -14,7 +14,6 @@ class ScanI2C
SCREEN_ST7567,
RTC_RV3028,
RTC_PCF8563,
RTC_RX8130CE,
CARDKB,
TDECKKB,
BBQ10KB,

View File

@@ -197,9 +197,6 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
#ifdef PCF8563_RTC
SCAN_SIMPLE_CASE(PCF8563_RTC, RTC_PCF8563, "PCF8563", (uint8_t)addr.address)
#endif
#ifdef RX8130CE_RTC
SCAN_SIMPLE_CASE(RX8130CE_RTC, RTC_RX8130CE, "RX8130CE", (uint8_t)addr.address)
#endif
case CARDKB_ADDR:
// Do we have the RAK14006 instead?
@@ -378,7 +375,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT
case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2);
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0x11f3 || registerValue == 0xe9c || registerValue == 0xc8d) {
if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) {
type = SHT4X;
logFoundDevice("SHT4X", (uint8_t)addr.address);
} else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) {

View File

@@ -506,26 +506,26 @@ bool GPS::setup()
delay(1000);
#endif
if (probeTries < GPS_PROBETRIES) {
LOG_DEBUG("Probe for GPS at %d", serialSpeeds[speedSelect]);
gnssModel = probe(serialSpeeds[speedSelect]);
if (gnssModel == GNSS_MODEL_UNKNOWN) {
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
if (++speedSelect == array_count(serialSpeeds)) {
speedSelect = 0;
++probeTries;
}
}
}
// Rare Serial Speeds
#ifndef CONFIG_IDF_TARGET_ESP32C6
if (probeTries == GPS_PROBETRIES) {
LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]);
gnssModel = probe(rareSerialSpeeds[speedSelect]);
if (gnssModel == GNSS_MODEL_UNKNOWN) {
if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
if (++speedSelect == array_count(rareSerialSpeeds)) {
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
return true;
}
}
}
#endif
}
if (gnssModel != GNSS_MODEL_UNKNOWN) {
@@ -807,14 +807,6 @@ bool GPS::setup()
} else {
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;
}
@@ -1031,7 +1023,7 @@ void GPS::down()
LOG_DEBUG("%us until next search", sleepTime / 1000);
// If update interval less than 10 seconds, no attempt to sleep
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS || sleepTime == 0)
if (updateInterval <= 10 * 1000UL || sleepTime == 0)
setPowerState(GPS_IDLE);
else {
@@ -1092,7 +1084,7 @@ int32_t GPS::runOnce()
return disable();
}
if (!setup())
return currentDelay; // Setup failed, re-run in two seconds
return 2000; // Setup failed, re-run in two seconds
// We have now loaded our saved preferences from flash
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
@@ -1102,29 +1094,11 @@ int32_t GPS::runOnce()
publishUpdate();
}
// ======================== GPS_ACTIVE state ========================
// In GPS_ACTIVE state, GPS is powered on and we're receiving NMEA messages.
// We use the following logic to determine when to update the local position
// or time by running GPS::publishUpdate.
// Note: Local position update is asynchronous to position broadcast. We
// generally run this state every gps_update_interval seconds, and in most cases
// gps_update_interval is faster than the position broadcast interval so there's a
// fresh position ready when the device wants to broadcast one on the mesh.
//
// 1. Got a time for the first time --> set the time, don't publish.
// 2. Got a lock for the first time
// --> If gps_update_interval is <= 10s --> publishUpdate
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
// 3. Got a lock after turning back on
// --> If gps_update_interval is <= 10s --> publishUpdate
// --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s)
// 4. Hold has expired
// --> If we have a time and a location --> publishUpdate
// --> down()
// 5. Search time has expired
// --> If we have a time and a location --> publishUpdate
// --> If we had a location before but don't now --> publishUpdate
// --> down()
// Repeaters have no need for GPS
if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
return disable();
}
if (whileActive()) {
// if we have received valid NMEA claim we are connected
setConnected();
@@ -1134,81 +1108,55 @@ int32_t GPS::runOnce()
if (!config.position.fixed_position && powerState != GPS_ACTIVE && scheduling.isUpdateDue())
up();
// quality of the previous fix. We set it to 0 when we go down, so it's a way
// to check if we're getting a lock after being GPS_OFF.
uint8_t prev_fixQual = fixQual;
if (powerState == GPS_ACTIVE) {
// if gps_update_interval is <=10s, GPS never goes off, so we treat that differently
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
// 1. Got a time for the first time
// If we've already set time from the GPS, no need to ask the GPS
bool gotTime = (getRTCQuality() >= RTCQualityGPS);
if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time
gotTime = true;
shouldPublish = true;
}
// 2. Got a lock for the first time, or 3. Got a lock after turning back on
uint8_t prev_fixQual = fixQual;
bool gotLoc = lookForLocation();
if (gotLoc) {
#ifdef GPS_DEBUG
if (!hasValidLocation) { // declare that we have location ASAP
if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
LOG_DEBUG("hasValidLocation RISING EDGE");
}
#endif
if (updateInterval <= GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS) {
hasValidLocation = true;
shouldPublish = true;
} else if (!hasValidLocation || prev_fixQual == 0 || (fixHoldEnds + GPS_THREAD_INTERVAL) < millis()) {
hasValidLocation = true;
// Hold for up to 20secs after getting a lock to download ephemeris etc
uint32_t holdTime = updateInterval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS;
if (holdTime > GPS_FIX_HOLD_MAX_MS)
holdTime = GPS_FIX_HOLD_MAX_MS;
fixHoldEnds = millis() + holdTime;
#ifdef GPS_DEBUG
LOG_DEBUG("Holding for %ums after lock", holdTime);
#endif
// 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();
if (tooLong && !gotLoc) {
if (tooLong)
LOG_WARN("Couldn't publish a valid location: didn't get a GPS lock in time");
// Once we get a location we no longer desperately want an update
if ((gotLoc && gotTime) || tooLong) {
if (tooLong && !gotLoc) {
// we didn't get a location during this ack window, therefore declare loss of lock
if (hasValidLocation) {
LOG_DEBUG("hasValidLocation FALLING EDGE");
}
p = meshtastic_Position_init_default;
hasValidLocation = false;
shouldPublish = true;
#ifdef GPS_DEBUG
LOG_DEBUG("hasValidLocation FALLING EDGE");
#endif
}
}
// Hold has expired , Search time has expired, we got a time only, or we never needed to hold.
bool holdExpired = (fixHoldEnds != 0 && millis() > fixHoldEnds);
if (shouldPublish || tooLong || holdExpired) {
if (gotTime && hasValidLocation) {
shouldPublish = true;
}
if (shouldPublish) {
fixHoldEnds = 0;
if (millis() > fixHoldEnds) {
shouldPublish = true; // publish our update at the end of the lock hold
publishUpdate();
}
// There's a chance we just got a time, so keep going to see if we can get a location too
if (tooLong || holdExpired) {
down();
}
#ifdef GPS_DEBUG
} else if (fixHoldEnds != 0) {
} else {
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
#endif
}
}
// ===================== end GPS_ACTIVE state ========================
// If state has changed do a publish
publishUpdate();
if (config.position.fixed_position == true && hasValidLocation)
return disable(); // This should trigger when we have a fixed position, and get that first position
@@ -1265,10 +1213,6 @@ static const char *DETECTED_MESSAGE = "%s detected";
GnssModel_t GPS::probe(int serialSpeed)
{
uint8_t buffer[768] = {0};
switch (currentStep) {
case 0: {
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
_serial_gps->end();
_serial_gps->begin(serialSpeed);
@@ -1278,12 +1222,13 @@ GnssModel_t GPS::probe(int serialSpeed)
_serial_gps->begin(serialSpeed);
#else
if (_serial_gps->baudRate() != serialSpeed) {
LOG_DEBUG("Set GPS Baud to %i", serialSpeed);
LOG_DEBUG("Set Baud to %i", serialSpeed);
_serial_gps->updateBaudRate(serialSpeed);
}
#endif
memset(&ublox_info, 0, sizeof(ublox_info));
uint8_t buffer[768] = {0};
delay(100);
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
@@ -1294,35 +1239,17 @@ 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,VTG,0,0,0,0,0,0*5E\r\n");
delay(20);
// Close NMEA sequences on CM121
_serial_gps->write("$CFGMSG,0,1,0,1*1B\r\n");
_serial_gps->write("$CFGMSG,0,2,0,1*18\r\n");
_serial_gps->write("$CFGMSG,0,3,0,1*19\r\n");
currentDelay = 20;
currentStep = 1;
return GNSS_MODEL_UNKNOWN;
}
case 1: {
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121
std::vector<ChipInfo> unicore = {
{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}};
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
std::vector<ChipInfo> unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}};
PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500);
currentDelay = 20;
currentStep = 2;
return GNSS_MODEL_UNKNOWN;
}
case 2: {
std::vector<ChipInfo> atgm = {
{"ATGM336H", "$GPTXT,01,01,02,HW=ATGM336H", GNSS_MODEL_ATGM336H},
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS)) based on AT6558 */
{"ATGM332D", "$GPTXT,01,01,02,HW=ATGM332D", GNSS_MODEL_ATGM336H}};
PROBE_FAMILY("ATGM33xx Family", "$PCAS06,1*1A", atgm, 500);
currentDelay = 20;
currentStep = 3;
return GNSS_MODEL_UNKNOWN;
}
case 3: {
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF to reduce volume
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF to reduce volume
@@ -1331,18 +1258,9 @@ GnssModel_t GPS::probe(int serialSpeed)
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}};
PROBE_FAMILY("Airoha Family", "$PAIR021*39", airoha, 1000);
currentDelay = 20;
currentStep = 4;
return GNSS_MODEL_UNKNOWN;
}
case 4: {
PROBE_SIMPLE("LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GNSS_MODEL_AG3352, 500);
PROBE_SIMPLE("L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GNSS_MODEL_MTK, 500);
currentDelay = 20;
currentStep = 5;
return GNSS_MODEL_UNKNOWN;
}
case 5: {
// Close all NMEA sentences, valid for MTK3333 and MTK3339 platforms
_serial_gps->write("$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n");
@@ -1353,11 +1271,7 @@ GnssModel_t GPS::probe(int serialSpeed)
{"L80", "_3339_", GNSS_MODEL_MTK_L76B}};
PROBE_FAMILY("MTK Family", "$PMTK605*31", mtk, 500);
currentDelay = 20;
currentStep = 6;
return GNSS_MODEL_UNKNOWN;
}
case 6: {
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
UBXChecksum(cfg_rate, sizeof(cfg_rate));
clearBuffer();
@@ -1366,8 +1280,6 @@ GnssModel_t GPS::probe(int serialSpeed)
GPS_RESPONSE response = getACK(0x06, 0x08, 750);
if (response == GNSS_RESPONSE_NONE) {
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
currentDelay = 2000;
currentStep = 0;
return GNSS_MODEL_UNKNOWN;
} else if (response == GNSS_RESPONSE_FRAME_ERRORS) {
LOG_INFO("UBlox Frame Errors (baudrate %d)", serialSpeed);
@@ -1451,11 +1363,7 @@ GnssModel_t GPS::probe(int serialSpeed)
return GNSS_MODEL_UBLOX10;
}
}
}
}
LOG_WARN("No GNSS Module (baudrate %d)", serialSpeed);
currentDelay = 2000;
currentStep = 0;
return GNSS_MODEL_UNKNOWN;
}
@@ -1532,7 +1440,7 @@ GPS *GPS::createGps()
_en_gpio = PIN_GPS_EN;
#endif
#ifdef ARCH_PORTDUINO
if (!portduino_config.has_gps)
if (!settingsMap[has_gps])
return nullptr;
#endif
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all

View File

@@ -16,9 +16,6 @@
#define GPS_EN_ACTIVE 1
#endif
static constexpr uint32_t GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS = 10 * 1000UL;
static constexpr uint32_t GPS_FIX_HOLD_MAX_MS = 20000;
typedef enum {
GNSS_MODEL_ATGM336H,
GNSS_MODEL_MTK,
@@ -34,8 +31,7 @@ typedef enum {
GNSS_MODEL_MTK_PA1616S,
GNSS_MODEL_AG3335,
GNSS_MODEL_AG3352,
GNSS_MODEL_LS20031,
GNSS_MODEL_CM121
GNSS_MODEL_LS20031
} GnssModel_t;
typedef enum {
@@ -154,8 +150,6 @@ class GPS : private concurrency::OSThread
TinyGPSPlus reader;
uint8_t fixQual = 0; // fix quality from GPGGA
uint32_t lastChecksumFailCount = 0;
uint8_t currentStep = 0;
int32_t currentDelay = 2000;
#ifndef TINYGPS_OPTION_NO_CUSTOM_FIELDS
// (20210908) TinyGps++ can only read the GPGSA "FIX TYPE" field
@@ -178,6 +172,8 @@ class GPS : private concurrency::OSThread
*/
bool hasValidLocation = false; // default to false, until we complete our first read
bool isInPowersave = false;
bool shouldPublish = false; // If we've changed GPS state, this will force a publish the next loop()
bool hasGPS = false; // Do we have a GPS we are talking to

View File

@@ -109,35 +109,6 @@ RTCSetResult readFromRTC()
}
return RTCSetResultSuccess;
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
uint32_t now = millis();
ArtronShop_RX8130CE rtc(&Wire);
tm t;
if (rtc.getTime(&t)) {
tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
LOG_DEBUG("Read RTC time from RX8130CE getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900,
t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
}
#endif
if (currentQuality == RTCQualityNone) {
timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec;
currentQuality = RTCQualityDevice;
}
return RTCSetResultSuccess;
}
}
#else
if (!gettimeofday(&tv, NULL)) {
uint32_t now = millis();
@@ -243,17 +214,6 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
LOG_DEBUG("PCF8563_RTC setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
}
#elif defined(RX8130CE_RTC)
if (rtc_found.address == RX8130CE_RTC) {
ArtronShop_RX8130CE rtc(&Wire);
tm *t = gmtime(&tv->tv_sec);
if (rtc.setTime(*t)) {
LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1,
t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch);
} else {
LOG_WARN("Failed to set time for RX8130CE");
}
}
#elif defined(ARCH_ESP32)
settimeofday(tv, NULL);
#endif

View File

@@ -4,10 +4,6 @@
#include "sys/time.h"
#include <Arduino.h>
#ifdef RX8130CE_RTC
#include <ArtronShop_RX8130CE.h>
#endif
enum RTCQuality {
/// We haven't had our RTC set yet

View File

@@ -243,7 +243,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK)
{
spi1 = &SPI1;
spi1->begin();

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "PowerMon.h"
#include "Throttle.h"
#include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include <OLEDDisplay.h>
@@ -59,6 +58,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "mesh-pb-constants.h"
#include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "meshUtils.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h"
#include "modules/WaypointModule.h"
@@ -83,11 +83,6 @@ extern uint16_t TFT_MESH;
#include "platform/portduino/PortduinoGlue.h"
#endif
#if defined(T_LORA_PAGER)
// KB backlight control
#include "input/cardKbI2cImpl.h"
#endif
using namespace meshtastic; /** @todo remove */
namespace graphics
@@ -100,7 +95,7 @@ namespace graphics
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
// if defined a pixel will blink to show redraws
// #define SHOW_REDRAWS
#define ASCII_BELL '\x07'
// A text message frame + debug frame + all the node infos
FrameCallback *normalFrames;
static uint32_t targetFramerate = IDLE_FRAMERATE;
@@ -221,44 +216,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
ui->update();
}
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback)
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
if (NotificationRenderer::virtualKeyboard) {
delete NotificationRenderer::virtualKeyboard;
NotificationRenderer::virtualKeyboard = nullptr;
}
NotificationRenderer::textInputCallback = nullptr;
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
if (header) {
NotificationRenderer::virtualKeyboard->setHeader(header);
}
if (initialText) {
NotificationRenderer::virtualKeyboard->setInputText(initialText);
}
// Set up callback with safer cleanup mechanism
NotificationRenderer::textInputCallback = textCallback;
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
NotificationRenderer::alertBannerMessage[255] = '\0';
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
// Set the overlay using the same pattern as other notification types
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
uint8_t module_frame;
@@ -383,7 +340,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (portduino_config.displayPanel != no_screen) {
if (settingsMap[displayPanel] != no_screen) {
LOG_DEBUG("Make TFTDisplay!");
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -453,7 +410,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
#endif
dispdev->displayOn();
#if defined(HELTEC_TRACKER_V1_X) || defined(HELTEC_WIRELESS_TRACKER_V2)
#ifdef HELTEC_TRACKER_V1_X
ui->init();
#endif
#ifdef USE_ST7789
@@ -631,7 +588,7 @@ void Screen::setup()
#if ARCH_PORTDUINO
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
if (portduino_config.touchscreenModule) {
if (settingsMap[touchscreenModule]) {
touchScreenImpl1 =
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
touchScreenImpl1->init();
@@ -660,19 +617,6 @@ void Screen::setup()
MeshModule::observeUIEvents(&uiFrameEventObserver);
}
void Screen::setOn(bool on, FrameCallback einkScreensaver)
{
#if defined(T_LORA_PAGER)
if (cardKbI2cImpl)
cardKbI2cImpl->toggleBacklight(on);
#endif
if (!on)
// We handle off commands immediately, because they might be called because the CPU is shutting down
handleSetOn(false, einkScreensaver);
else
enqueueCmd(ScreenCmd{.cmd = Cmd::SET_ON});
}
void Screen::forceDisplay(bool forceUiUpdate)
{
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
@@ -781,19 +725,13 @@ int32_t Screen::runOnce()
handleSetOn(false);
break;
case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleOnPress();
}
break;
case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowPrevFrame();
}
break;
case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowNextFrame();
}
break;
case Cmd::START_ALERT_FRAME: {
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
@@ -815,9 +753,7 @@ int32_t Screen::runOnce()
NotificationRenderer::pauseBanner = false;
case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames();
}
break;
case Cmd::NOOP:
break;
@@ -853,7 +789,6 @@ int32_t Screen::runOnce()
if (showingNormalScreen) {
// standard screen loop handling here
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)) {
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
@@ -944,11 +879,6 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
// Called when a frame should be added / removed, or custom frames should be cleared
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 previousFrameCount = framesetInfo.frameCount;
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
@@ -971,7 +901,6 @@ void Screen::setFrames(FrameFocus focus)
}
#if defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
#if defined(M5STACK_UNITC6L)
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
@@ -980,86 +909,67 @@ void Screen::setFrames(FrameFocus focus)
: graphics::ClockRenderer::drawDigitalClockFrame;
#endif
indicatorIcons.push_back(digital_icon_clock);
}
#endif
// Declare this early so its available in FOCUS_PRESERVE block
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
if (!hiddenFrames.home) {
fsi.positions.home = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
indicatorIcons.push_back(icon_home);
}
fsi.positions.textMessage = numframes;
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
indicatorIcons.push_back(icon_mail);
#ifndef USE_EINK
if (!hiddenFrames.nodelist) {
fsi.positions.nodelist = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
indicatorIcons.push_back(icon_nodes);
}
#endif
// Show detailed node views only on E-Ink builds
#ifdef USE_EINK
if (!hiddenFrames.nodelist_lastheard) {
fsi.positions.nodelist_lastheard = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
indicatorIcons.push_back(icon_nodes);
}
if (!hiddenFrames.nodelist_hopsignal) {
fsi.positions.nodelist_hopsignal = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
indicatorIcons.push_back(icon_signal);
}
if (!hiddenFrames.nodelist_distance) {
fsi.positions.nodelist_distance = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
indicatorIcons.push_back(icon_distance);
}
#endif
#if HAS_GPS
if (!hiddenFrames.nodelist_bearings) {
fsi.positions.nodelist_bearings = numframes;
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
indicatorIcons.push_back(icon_list);
}
if (!hiddenFrames.gps) {
fsi.positions.gps = numframes;
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
indicatorIcons.push_back(icon_compass);
}
#endif
if (RadioLibInterface::instance && !hiddenFrames.lora) {
if (RadioLibInterface::instance) {
fsi.positions.lora = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
indicatorIcons.push_back(icon_radio);
}
if (!hiddenFrames.system) {
fsi.positions.system = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen;
indicatorIcons.push_back(icon_system);
if (!dismissedFrames.memory) {
fsi.positions.memory = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
indicatorIcons.push_back(icon_memory);
}
#if !defined(DISPLAY_CLOCK_FRAME)
if (!hiddenFrames.clock) {
fsi.positions.clock = numframes;
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
: graphics::ClockRenderer::drawDigitalClockFrame;
indicatorIcons.push_back(digital_icon_clock);
}
#endif
if (!hiddenFrames.chirpy) {
fsi.positions.chirpy = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy;
indicatorIcons.push_back(chirpy_small);
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
if (!hiddenFrames.wifi && isWifiAvailable()) {
if (!dismissedFrames.wifi && isWifiAvailable()) {
fsi.positions.wifi = numframes;
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
indicatorIcons.push_back(icon_wifi);
@@ -1101,7 +1011,6 @@ void Screen::setFrames(FrameFocus focus)
if (numMeshNodes > 0)
numMeshNodes--;
if (!hiddenFrames.show_favorites) {
// Temporary array to hold favorite node frames
std::vector<FrameCallback> favoriteFrames;
@@ -1124,7 +1033,6 @@ void Screen::setFrames(FrameFocus focus)
fsi.positions.firstFavorite = 255;
fsi.positions.lastFavorite = 255;
}
}
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
@@ -1162,7 +1070,7 @@ void Screen::setFrames(FrameFocus focus)
ui->switchToFrame(fsi.positions.clock);
break;
case FOCUS_SYSTEM:
ui->switchToFrame(fsi.positions.system);
ui->switchToFrame(fsi.positions.memory);
break;
case FOCUS_PRESERVE:
@@ -1190,101 +1098,30 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames)
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
// Relevant for text message, waypoint, others in future?
// Triggered with a CardKB keycombo
void Screen::hideCurrentFrame()
void Screen::dismissCurrentFrame()
{
uint8_t currentFrame = ui->getUiState()->currentFrame;
bool dismissed = false;
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;
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
LOG_DEBUG("Hide Waypoint");
LOG_DEBUG("Dismiss Waypoint");
devicestate.has_rx_waypoint = false;
hiddenFrames.waypoint = true;
dismissedFrames.waypoint = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.wifi) {
LOG_DEBUG("Hide WiFi Screen");
hiddenFrames.wifi = true;
LOG_DEBUG("Dismiss WiFi Screen");
dismissedFrames.wifi = true;
dismissed = true;
} else if (currentFrame == framesetInfo.positions.lora) {
LOG_INFO("Hide LoRa");
hiddenFrames.lora = true;
} else if (currentFrame == framesetInfo.positions.memory) {
LOG_INFO("Dismiss Memory");
dismissedFrames.memory = true;
dismissed = true;
}
@@ -1317,8 +1154,7 @@ void Screen::blink()
delay(50);
count = count - 1;
}
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in
// OLEDDisplay.
// The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in OLEDDisplay.
dispdev->setBrightness(brightness);
}
@@ -1441,7 +1277,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Outgoing message (likely sent from phone)
devicestate.has_rx_text_message = false;
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
setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list
@@ -1458,36 +1294,28 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
}
// === Prepare banner content ===
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
const meshtastic_Channel channel =
channels.getByIndex(packet->channel ? packet->channel : channels.getPrimaryIndex());
const char *longName = (node && node->has_user) ? node->user.long_name : nullptr;
const char *msgRaw = reinterpret_cast<const char *>(packet->decoded.payload.bytes);
char banner[256];
// Check for bell character in message to determine alert type
bool isAlert = false;
if (moduleConfig.external_notification.alert_bell || moduleConfig.external_notification.alert_bell_vibra ||
moduleConfig.external_notification.alert_bell_buzzer)
// Check for bell character to determine if this message is an alert
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
if (msgRaw[i] == ASCII_BELL) {
if (msgRaw[i] == '\x07') {
isAlert = true;
break;
}
}
// Unlike generic messages, alerts (when enabled via the ext notif module) ignore any
// 'mute' preferences set to any specific node or channel.
if (isAlert) {
if (longName && longName[0]) {
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
} else {
strcpy(banner, "Alert Received");
}
screen->showSimpleBanner(banner, 3000);
} else if (!channel.settings.mute) {
} else {
if (longName && longName[0]) {
#if defined(M5STACK_UNITC6L)
strcpy(banner, "New Message");
@@ -1498,23 +1326,16 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
} else {
strcpy(banner, "New Message");
}
}
#if defined(M5STACK_UNITC6L)
screen->setOn(true);
screen->showSimpleBanner(banner, 1500);
if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DIRECT_MSG_ONLY ||
(isAlert && moduleConfig.external_notification.alert_bell_buzzer) ||
(!isBroadcast(packet->to) && isToUs(packet))) {
// Beep if not in DIRECT_MSG_ONLY mode or if in DIRECT_MSG_ONLY mode and either
// - packet contains an alert and alert bell buzzer is enabled
// - packet is a non-broadcast that is addressed to this node
playLongBeep();
}
#else
screen->showSimpleBanner(banner, 3000);
#endif
}
}
}
return 0;
}
@@ -1522,11 +1343,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Triggered by MeshModules
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) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
@@ -1549,16 +1365,6 @@ int Screen::handleInputEvent(const InputEvent *event)
if (!screenOn)
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.
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
@@ -1596,7 +1402,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
menuHandler::homeBaseMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) {
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
menuHandler::systemBaseMenu();
#if HAS_GPS
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
@@ -1605,7 +1411,7 @@ int Screen::handleInputEvent(const InputEvent *event)
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
menuHandler::clockMenu();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
menuHandler::loraMenu();
menuHandler::LoraRegionPicker();
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) {
if (devicestate.rx_text_message.from) {
menuHandler::messageResponseMenu();
@@ -1673,15 +1479,13 @@ 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 / CLIENT_HIDDEN / CLIENT_BASE
- If role is not client / client_mute
- If the battery level is very low
*/
if (moduleConfig.external_notification.enabled) {
return false;
}
if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT,
meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN,
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) {
if (!meshtastic_Config_DeviceConfig_Role_CLIENT && !meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE) {
return false;
}
if (powerStatus && powerStatus->getBatteryChargePercent() < 10) {

View File

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

View File

@@ -1,7 +1,6 @@
#include "graphics/SharedUIDisplay.h"
#include "RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/draw/UIRenderer.h"
#include "main.h"
#include "meshtastic/config.pb.h"
#include "power.h"
@@ -17,10 +16,6 @@ void determineResolution(int16_t screenheight, int16_t screenwidth)
isHighResolution = true;
}
if (screenwidth > 128 && screenheight <= 64) {
isHighResolution = false;
}
// Special case for Heltec Wireless Tracker v1.1
if (screenwidth == 160 && screenheight == 80) {
isHighResolution = false;
@@ -58,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
// *************************
// * 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;
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 screenH = display->getHeight();
if (!force_no_invert) {
if (!battery_only) {
// === Inverted Header Background ===
if (isInverted) {
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 timeX = screenW - xOffset - timeStrWidth + 4;
if (rtc_sec > 0) {
if (rtc_sec > 0 && !battery_only) {
// === Build Time String ===
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
int hour = hms / SEC_PER_HOUR;
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
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) {
bool isPM = hour >= 12;
hour %= 12;
@@ -222,11 +202,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
}
if (show_date) {
timeStrWidth = display->getStringWidth(dateLine);
} else {
timeStrWidth = display->getStringWidth(timeStr);
}
timeX = screenW - xOffset - timeStrWidth + 3;
// === Show Mail or Mute Icon to the Left of Time ===
@@ -253,7 +229,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconW = 16, iconH = 12;
int iconX = iconRightEdge - iconW;
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
if (isInverted && !force_no_invert) {
if (isInverted) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
display->setColor(BLACK);
@@ -268,7 +244,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
} else {
int iconX = iconRightEdge - (mail_width - 2);
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
if (isInverted && !force_no_invert) {
if (isInverted) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
display->setColor(BLACK);
@@ -284,7 +260,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
int iconX = iconRightEdge - mute_symbol_big_width;
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
if (isInverted && !force_no_invert) {
if (isInverted) {
display->setColor(WHITE);
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
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 ===
display->drawString(timeX, textY, timeStr);
if (isBold)
display->drawString(timeX - 1, textY, timeStr);
}
} else {
// === 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);
// Shared battery/time/mail header
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
bool show_date = false);
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
const int *getTextPositions(OLEDDisplay *display);

View File

@@ -751,8 +751,10 @@ static LGFX *tft = nullptr;
static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h
#elif ARCH_PORTDUINO
#include "Panel_sdl.hpp"
#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
{
@@ -765,26 +767,26 @@ class LGFX : public lgfx::LGFX_Device
LGFX(void)
{
if (portduino_config.displayPanel == st7789)
if (settingsMap[displayPanel] == st7789)
_panel_instance = new lgfx::Panel_ST7789;
else if (portduino_config.displayPanel == st7735)
else if (settingsMap[displayPanel] == st7735)
_panel_instance = new lgfx::Panel_ST7735;
else if (portduino_config.displayPanel == st7735s)
else if (settingsMap[displayPanel] == st7735s)
_panel_instance = new lgfx::Panel_ST7735S;
else if (portduino_config.displayPanel == st7796)
else if (settingsMap[displayPanel] == st7796)
_panel_instance = new lgfx::Panel_ST7796;
else if (portduino_config.displayPanel == ili9341)
else if (settingsMap[displayPanel] == ili9341)
_panel_instance = new lgfx::Panel_ILI9341;
else if (portduino_config.displayPanel == ili9342)
else if (settingsMap[displayPanel] == ili9342)
_panel_instance = new lgfx::Panel_ILI9342;
else if (portduino_config.displayPanel == ili9488)
else if (settingsMap[displayPanel] == ili9488)
_panel_instance = new lgfx::Panel_ILI9488;
else if (portduino_config.displayPanel == hx8357d)
else if (settingsMap[displayPanel] == hx8357d)
_panel_instance = new lgfx::Panel_HX8357D;
#if defined(SDL_h_)
else if (portduino_config.displayPanel == x11)
#if defined(LGFX_SDL)
else if (settingsMap[displayPanel] == x11) {
_panel_instance = new lgfx::Panel_sdl;
}
#endif
else {
_panel_instance = new lgfx::Panel_NULL;
@@ -793,62 +795,61 @@ class LGFX : public lgfx::LGFX_Device
auto buscfg = _bus_instance.config();
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.
if (portduino_config.displayPanel != x11)
_panel_instance->setBus(&_bus_instance); // set the bus on the panel.
auto cfg = _panel_instance->config(); // Gets a structure for display panel settings.
LOG_DEBUG("Width: %d, Height: %d", portduino_config.displayWidth, portduino_config.displayHeight);
cfg.pin_cs = portduino_config.displayCS.pin; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = portduino_config.displayReset.pin;
if (portduino_config.displayRotate) {
cfg.panel_width = portduino_config.displayHeight; // actual displayable width
cfg.panel_height = portduino_config.displayWidth; // actual displayable height
LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]);
cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = settingsMap[displayReset];
if (settingsMap[displayRotate]) {
cfg.panel_width = settingsMap[displayHeight]; // actual displayable width
cfg.panel_height = settingsMap[displayWidth]; // actual displayable height
} else {
cfg.panel_width = portduino_config.displayWidth; // actual displayable width
cfg.panel_height = portduino_config.displayHeight; // actual displayable height
cfg.panel_width = settingsMap[displayWidth]; // actual displayable width
cfg.panel_height = settingsMap[displayHeight]; // actual displayable height
}
cfg.offset_x = portduino_config.displayOffsetX; // Panel offset amount in X direction
cfg.offset_y = portduino_config.displayOffsetY; // Panel offset amount in Y direction
cfg.offset_rotation = portduino_config.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.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction
cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction
cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored)
cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed
_panel_instance->config(cfg);
// Configure settings for touch control.
if (portduino_config.touchscreenModule) {
if (portduino_config.touchscreenModule == xpt2046) {
if (settingsMap[touchscreenModule]) {
if (settingsMap[touchscreenModule] == xpt2046) {
_touch_instance = new lgfx::Touch_XPT2046;
} else if (portduino_config.touchscreenModule == stmpe610) {
} else if (settingsMap[touchscreenModule] == stmpe610) {
_touch_instance = new lgfx::Touch_STMPE610;
} else if (portduino_config.touchscreenModule == ft5x06) {
} else if (settingsMap[touchscreenModule] == ft5x06) {
_touch_instance = new lgfx::Touch_FT5x06;
}
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_max = portduino_config.displayHeight - 1;
touch_cfg.x_max = settingsMap[displayHeight] - 1;
touch_cfg.y_min = 0;
touch_cfg.y_max = portduino_config.displayWidth - 1;
touch_cfg.pin_int = portduino_config.touchscreenIRQ.pin;
touch_cfg.y_max = settingsMap[displayWidth] - 1;
touch_cfg.pin_int = settingsMap[touchscreenIRQ];
touch_cfg.bus_shared = true;
touch_cfg.offset_rotation = portduino_config.touchscreenRotate;
if (portduino_config.touchscreenI2CAddr != -1) {
touch_cfg.i2c_addr = portduino_config.touchscreenI2CAddr;
touch_cfg.offset_rotation = settingsMap[touchscreenRotate];
if (settingsMap[touchscreenI2CAddr] != -1) {
touch_cfg.i2c_addr = settingsMap[touchscreenI2CAddr];
} else {
touch_cfg.spi_host = portduino_config.touchscreen_spi_dev_int;
touch_cfg.spi_host = settingsMap[touchscreenspidev];
}
_touch_instance->config(touch_cfg);
_panel_instance->setTouch(_touch_instance);
}
#if defined(SDL_h_)
if (portduino_config.displayPanel == x11) {
#if defined(LGFX_SDL)
if (settingsMap[displayPanel] == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance;
sdl_panel_->setup();
sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER);
@@ -1114,10 +1115,10 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
backlightEnable = p;
#if ARCH_PORTDUINO
if (portduino_config.displayRotate) {
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayWidth, portduino_config.displayWidth);
if (settingsMap[displayRotate]) {
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]);
} else {
setGeometry(GEOMETRY_RAWMODE, portduino_config.displayHeight, portduino_config.displayHeight);
setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]);
}
#elif defined(SCREEN_ROTATE)
@@ -1236,36 +1237,37 @@ void TFTDisplay::display(bool fromBlank)
void TFTDisplay::sdlLoop()
{
#if defined(SDL_h_)
#if defined(LGFX_SDL)
static int lastPressed = 0;
static int shuttingDown = false;
if (portduino_config.displayPanel == x11) {
if (settingsMap[displayPanel] == x11) {
lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance;
if (sdl_panel_->loop() && !shuttingDown) {
LOG_WARN("Window Closed!");
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0};
inputBroker->injectInputEvent(&event);
}
// debounce
if (lastPressed != 0 && !sdl_panel_->gpio_in(lastPressed))
if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed))
return;
if (!sdl_panel_->gpio_in(37)) {
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 (!sdl_panel_->gpio_in(36)) {
} 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 (!sdl_panel_->gpio_in(38)) {
} 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 (!sdl_panel_->gpio_in(39)) {
} 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 (!sdl_panel_->gpio_in(SDL_SCANCODE_KP_ENTER)) {
} 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);
@@ -1286,8 +1288,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(true);
#if ARCH_PORTDUINO
display(true);
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
if (settingsMap[displayBacklight] > 0)
digitalWrite(settingsMap[displayBacklight], TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->wakeup();
tft->powerSaveOff();
@@ -1310,8 +1312,8 @@ void TFTDisplay::sendCommand(uint8_t com)
backlightEnable->set(false);
#if ARCH_PORTDUINO
tft->clear();
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
if (settingsMap[displayBacklight] > 0)
digitalWrite(settingsMap[displayBacklight], !TFT_BACKLIGHT_ON);
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE)
tft->sleep();
tft->powerSaveOn();

View File

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

View File

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

View File

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

View File

@@ -330,7 +330,8 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
#if HAS_GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// Line 3
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
if (config.display.gps_format !=
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
// Line 4
@@ -394,18 +395,8 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int textWidth = display->getStringWidth(shortnameble);
int nameX = (SCREEN_WIDTH - textWidth);
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
// === Second Row: Role ===
auto role = DisplayFormatters::getDeviceRole(config.device.role);
char device_role[25];
snprintf(device_role, sizeof(device_role), "Role: %s", role);
textWidth = display->getStringWidth(device_role);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], device_role);
// === Third Row: Radio Preset ===
// === Second Row: Radio Preset ===
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) {
@@ -419,7 +410,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
// === Fourth Row: Frequency / ChanNum ===
// === Third Row: Frequency / ChanNum ===
char frequencyslot[35];
char freqStr[16];
float freq = RadioLibInterface::instance->getFreq();
@@ -446,7 +437,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
#if !defined(M5STACK_UNITC6L)
// === Fifth Row: Channel Utilization ===
// === Fourth Row: Channel Utilization ===
const char *chUtil = "ChUtil:";
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
@@ -463,7 +454,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
int starting_position = centerofscreen - total_line_content_width;
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
// Force 56% or higher to show a full 100% bar, text would still show related percent.
if (chutil_percent >= 61) {
@@ -500,7 +491,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
}
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
chUtilPercentage);
#endif
}
@@ -508,7 +499,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
// ****************************
// * System Screen *
// ****************************
void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setFont(FONT_SMALL);
@@ -664,34 +655,6 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
}
#endif
}
// ****************************
// * Chirpy Screen *
// ****************************
void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3);
int iconY = (SCREEN_HEIGHT - chirpy_height) / 2;
int textX_offset = 10;
if (isHighResolution) {
iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3);
iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2;
textX_offset = textX_offset * 4;
display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez);
} else {
display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy);
}
int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2);
display->drawString(textX, getTextPositions(display)[line++], "Hello");
textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2);
display->drawString(textX, getTextPositions(display)[line++], "World!");
}
} // namespace DebugRenderer
} // namespace graphics
#endif

View File

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

View File

@@ -10,10 +10,7 @@
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h"
@@ -29,28 +26,6 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none;
bool test_enabled = false;
uint8_t test_count = 0;
void menuHandler::loraMenu()
{
static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "LoRa Region"};
enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, lora_picker = 3 };
BannerOverlayOptions bannerOptions;
bannerOptions.message = "LoRa Actions";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
// No action
} else if (selected == device_role_picker) {
menuHandler::menuQueue = menuHandler::device_role_picker;
} else if (selected == radio_preset_picker) {
menuHandler::menuQueue = menuHandler::radio_preset_picker;
} else if (selected == lora_picker) {
menuHandler::menuQueue = menuHandler::lora_picker;
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::OnboardMessage()
{
static const char *optionsArray[] = {"OK", "Got it!"};
@@ -116,8 +91,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
auto changes = SEGMENT_CONFIG;
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
if (!owner.is_licensed) {
bool keygenSuccess = false;
@@ -126,7 +99,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
keygenSuccess = true;
}
} else {
LOG_INFO("Generate new PKI keys");
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
@@ -144,97 +116,9 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
if (myRegion->dutyCycle < 100) {
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
}
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
// Default broker is in use, so subscribe to the appropriate MQTT root topic for this region
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
changes |= SEGMENT_MODULECONFIG;
}
service->reloadConfig(changes);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::DeviceRolePicker()
{
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
enum optionsNumbers {
Back = 0,
devicerole_client = 1,
devicerole_clientmute = 2,
devicerole_lostandfound = 3,
devicerole_tracker = 4
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Device Role";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == devicerole_client) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
} else if (selected == devicerole_clientmute) {
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE;
} else if (selected == devicerole_lostandfound) {
config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND;
} else if (selected == devicerole_tracker) {
config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::RadioPresetPicker()
{
static const char *optionsArray[] = {"Back", "LongSlow", "LongModerate", "LongFast", "MediumSlow",
"MediumFast", "ShortSlow", "ShortFast", "ShortTurbo"};
enum optionsNumbers {
Back = 0,
radiopreset_LongSlow = 1,
radiopreset_LongModerate = 2,
radiopreset_LongFast = 3,
radiopreset_MediumSlow = 4,
radiopreset_MediumFast = 5,
radiopreset_ShortSlow = 6,
radiopreset_ShortFast = 7,
radiopreset_ShortTurbo = 8
};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Radio Preset";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 9;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Back) {
menuHandler::menuQueue = menuHandler::lora_Menu;
screen->runNow();
return;
} else if (selected == radiopreset_LongSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW;
} else if (selected == radiopreset_LongModerate) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_MODERATE;
} else if (selected == radiopreset_LongFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
} else if (selected == radiopreset_MediumSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW;
} else if (selected == radiopreset_MediumFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
} else if (selected == radiopreset_ShortSlow) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW;
} else if (selected == radiopreset_ShortFast) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST;
} else if (selected == radiopreset_ShortTurbo) {
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO;
}
service->reloadConfig(SEGMENT_CONFIG);
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
};
screen->showOverlayBanner(bannerOptions);
}
@@ -436,7 +320,7 @@ void menuHandler::messageResponseMenu()
bannerOptions.optionsCount = options;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == Dismiss) {
screen->hideCurrentFrame();
screen->dismissCurrentFrame();
} else if (selected == Preset) {
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
@@ -477,11 +361,8 @@ void menuHandler::homeBaseMenu()
optionsArray[options] = "Sleep Screen";
optionsEnumArray[options++] = Sleep;
#endif
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
optionsArray[options] = "Send Position";
} else {
optionsArray[options] = "Send Node Info";
}
optionsEnumArray[options++] = Position;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "New Preset";
@@ -574,7 +455,7 @@ void menuHandler::textMessageBaseMenu()
void menuHandler::systemBaseMenu()
{
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd };
enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd };
static const char *optionsArray[enumEnd] = {"Back"};
static int optionsEnumArray[enumEnd] = {Back};
int options = 1;
@@ -586,9 +467,6 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Screen Options";
optionsEnumArray[options++] = ScreenOptions;
#endif
optionsArray[options] = "Frame Visiblity Toggle";
optionsEnumArray[options++] = FrameToggles;
#if defined(M5STACK_UNITC6L)
optionsArray[options] = "Bluetooth";
#else
@@ -626,9 +504,6 @@ void menuHandler::systemBaseMenu()
} else if (selected == PowerMenu) {
menuHandler::menuQueue = menuHandler::power_menu;
screen->runNow();
} else if (selected == FrameToggles) {
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == Test) {
menuHandler::menuQueue = menuHandler::test_menu;
screen->runNow();
@@ -695,17 +570,18 @@ void menuHandler::favoriteBaseMenu()
void menuHandler::positionBaseMenu()
{
enum optionsNumbers { Back, GPSToggle, GPSFormat, CompassMenu, CompassCalibrate, enumEnd };
enum optionsNumbers { Back, GPSToggle, CompassMenu, CompassCalibrate, enumEnd };
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "GPS Format", "Compass"};
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, GPSFormat, CompassMenu};
int options = 4;
static const char *optionsArray[enumEnd] = {"Back", "GPS Toggle", "Compass"};
static int optionsEnumArray[enumEnd] = {Back, GPSToggle, CompassMenu};
int options = 3;
#if !MESHTASTIC_EXCLUDE_I2C
if (accelerometerThread) {
optionsArray[options] = "Compass Calibrate";
optionsEnumArray[options++] = CompassCalibrate;
}
#endif
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Position Action";
bannerOptions.optionsArrayPtr = optionsArray;
@@ -715,14 +591,13 @@ void menuHandler::positionBaseMenu()
if (selected == GPSToggle) {
menuQueue = gps_toggle_menu;
screen->runNow();
} else if (selected == GPSFormat) {
menuQueue = gps_format_menu;
screen->runNow();
} else if (selected == CompassMenu) {
menuQueue = compass_point_north_menu;
screen->runNow();
#if !MESHTASTIC_EXCLUDE_I2C
} else if (selected == CompassCalibrate) {
accelerometerThread->calibrate(30);
#endif
}
};
screen->showOverlayBanner(bannerOptions);
@@ -844,58 +719,6 @@ void menuHandler::GPSToggleMenu()
bannerOptions.InitialSelected = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2;
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::GPSFormatMenu()
{
static const char *optionsArray[] = {"Back",
isHighResolution ? "Decimal Degrees" : "DEC",
isHighResolution ? "Degrees Minutes Seconds" : "DMS",
isHighResolution ? "Universal Transverse Mercator" : "UTM",
isHighResolution ? "Military Grid Reference System" : "MGRS",
isHighResolution ? "Open Location Code" : "OLC",
isHighResolution ? "Ordnance Survey Grid Ref" : "OSGR",
isHighResolution ? "Maidenhead Locator" : "MLS"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "GPS Format";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 8;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == 1) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 2) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 3) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 4) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 5) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 6) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else if (selected == 7) {
uiconfig.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS;
saveUIConfig();
service->reloadConfig(SEGMENT_CONFIG);
} else {
menuQueue = position_base_menu;
screen->runNow();
}
};
bannerOptions.InitialSelected = uiconfig.gps_format + 1;
screen->showOverlayBanner(bannerOptions);
}
#endif
void menuHandler::BluetoothToggleMenu()
@@ -921,11 +744,11 @@ void menuHandler::BluetoothToggleMenu()
void menuHandler::BuzzerModeMenu()
{
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only", "DMs Only"};
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Buzzer Mode";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 5;
bannerOptions.optionsCount = 4;
bannerOptions.bannerCallback = [](int selected) -> void {
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
service->reloadConfig(SEGMENT_CONFIG);
@@ -1191,33 +1014,16 @@ void menuHandler::traceRouteMenu()
void menuHandler::testMenu()
{
enum optionsNumbers { Back, NumberPicker, ShowChirpy };
static const char *optionsArray[4] = {"Back"};
static int optionsEnumArray[4] = {Back};
int options = 1;
optionsArray[options] = "Number Picker";
optionsEnumArray[options++] = NumberPicker;
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
optionsEnumArray[options++] = ShowChirpy;
static const char *optionsArray[] = {"Back", "Number Picker"};
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Hidden Test Menu";
std::string message = "Test to Run?\n";
bannerOptions.message = message.c_str();
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.optionsCount = 2;
bannerOptions.bannerCallback = [](int selected) -> void {
if (selected == NumberPicker) {
if (selected == 1) {
menuQueue = number_test;
screen->runNow();
} else if (selected == ShowChirpy) {
screen->toggleFrameVisibility("chirpy");
screen->setFrames(Screen::FOCUS_SYSTEM);
} else {
menuQueue = system_base_menu;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
@@ -1418,116 +1224,6 @@ void menuHandler::keyVerificationFinalPrompt()
}
}
void menuHandler::FrameToggles_menu()
{
enum optionsNumbers {
Finish,
nodelist,
nodelist_lastheard,
nodelist_hopsignal,
nodelist_distance,
nodelist_bearings,
gps,
lora,
clock,
show_favorites,
enumEnd
};
static const char *optionsArray[enumEnd] = {"Finish"};
static int optionsEnumArray[enumEnd] = {Finish};
int options = 1;
// Track last selected index (not enum value!)
static int lastSelectedIndex = 0;
#ifndef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List";
optionsEnumArray[options++] = nodelist;
#endif
#ifdef USE_EINK
optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard";
optionsEnumArray[options++] = nodelist_lastheard;
optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal";
optionsEnumArray[options++] = nodelist_hopsignal;
optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance";
optionsEnumArray[options++] = nodelist_distance;
#endif
#if HAS_GPS
optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings";
optionsEnumArray[options++] = nodelist_bearings;
optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position";
optionsEnumArray[options++] = gps;
#endif
optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa";
optionsEnumArray[options++] = lora;
optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock";
optionsEnumArray[options++] = clock;
optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites";
optionsEnumArray[options++] = show_favorites;
BannerOverlayOptions bannerOptions;
bannerOptions.message = "Show/Hide Frames";
bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = options;
bannerOptions.optionsEnumPtr = optionsEnumArray;
bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value
bannerOptions.bannerCallback = [options](int selected) mutable -> void {
// Find the index of selected in optionsEnumArray
int idx = 0;
for (; idx < options; ++idx) {
if (optionsEnumArray[idx] == selected)
break;
}
lastSelectedIndex = idx;
if (selected == Finish) {
screen->setFrames(Screen::FOCUS_DEFAULT);
} else if (selected == nodelist) {
screen->toggleFrameVisibility("nodelist");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_lastheard) {
screen->toggleFrameVisibility("nodelist_lastheard");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_hopsignal) {
screen->toggleFrameVisibility("nodelist_hopsignal");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_distance) {
screen->toggleFrameVisibility("nodelist_distance");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == nodelist_bearings) {
screen->toggleFrameVisibility("nodelist_bearings");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == gps) {
screen->toggleFrameVisibility("gps");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == lora) {
screen->toggleFrameVisibility("lora");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == clock) {
screen->toggleFrameVisibility("clock");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
} else if (selected == show_favorites) {
screen->toggleFrameVisibility("show_favorites");
menuHandler::menuQueue = menuHandler::FrameToggles;
screen->runNow();
}
};
screen->showOverlayBanner(bannerOptions);
}
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
{
if (menuQueue != menu_none)
@@ -1535,18 +1231,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
switch (menuQueue) {
case menu_none:
break;
case lora_Menu:
loraMenu();
break;
case lora_picker:
LoraRegionPicker();
break;
case device_role_picker:
DeviceRolePicker();
break;
case radio_preset_picker:
RadioPresetPicker();
break;
case no_timeout_lora_picker:
LoraRegionPicker(0);
break;
@@ -1572,9 +1259,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case gps_toggle_menu:
GPSToggleMenu();
break;
case gps_format_menu:
GPSFormatMenu();
break;
#endif
case compass_point_north_menu:
compassNorthMenu();
@@ -1636,9 +1320,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
case power_menu:
powerMenu();
break;
case FrameToggles:
FrameToggles_menu();
break;
case throttle_message:
screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000);
break;

View File

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

View File

@@ -7,18 +7,10 @@
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#if HAS_BUTTON
#include "input/ButtonThread.h"
#endif
#include "main.h"
#include <algorithm>
#include <string>
#include <vector>
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
@@ -26,11 +18,6 @@
using namespace meshtastic;
#if HAS_BUTTON
// Global button thread pointer defined in main.cpp
extern ::ButtonThread *UserButtonThread;
#endif
// External references to global variables from Screen.cpp
extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString;
@@ -51,8 +38,6 @@ bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
uint32_t pow_of_10(uint32_t n)
{
@@ -104,33 +89,14 @@ void NotificationRenderer::resetBanner()
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// Handle text_input notifications first - they have their own timeout/banner logic
if (current_notification_type == notificationTypeEnum::text_input) {
// Check for timeout and reset if needed for text input
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
resetBanner();
if (!isOverlayBannerShowing() || pauseBanner)
return;
}
drawTextInput(display, state);
return;
}
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner();
}
// Exit if no banner is showing or banner is paused
if (!isOverlayBannerShowing() || pauseBanner) {
return;
}
switch (current_notification_type) {
case notificationTypeEnum::none:
// Do nothing - no notification to display
break;
case notificationTypeEnum::text_input:
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
break;
case notificationTypeEnum::text_banner:
case notificationTypeEnum::selection_picker:
drawAlertBannerOverlay(display, state);
@@ -250,11 +216,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
// Handle input
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
alertBannerCallback(selectedNodenum);
@@ -303,9 +267,12 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
}
// make temp buffer for name
// fi
if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) {
@@ -319,8 +286,7 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
}
scratchLineBuffer[scratchLineNum][39] = '\0';
} else {
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
scratchLineBuffer[scratchLineNum][39] = '\0';
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36);
}
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
}
@@ -367,11 +333,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
// Handle input
if (alertBannerOptions > 0) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_LEFT ||
inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
curSelected--;
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_RIGHT ||
inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
curSelected++;
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
if (optionsEnumPtr != nullptr) {
@@ -741,99 +705,6 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
"Please be patient and do not power off.");
}
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (virtualKeyboard) {
// Check for timeout and auto-exit if needed
if (virtualKeyboard->isTimedOut()) {
LOG_INFO("Virtual keyboard timeout - auto-exiting");
// Cancel virtual keyboard - call callback with empty string to indicate timeout
auto callback = textInputCallback; // Store callback before clearing
// Clean up first to prevent re-entry
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
// Call callback after cleanup
if (callback) {
callback("");
}
// Restore normal overlays
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
if (inEvent.inputEvent == INPUT_BROKER_UP) {
// high frequency for move cursor left/right than up/down with encoders
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorLeft();
} else {
virtualKeyboard->moveCursorUp();
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorRight();
} else {
virtualKeyboard->moveCursorDown();
}
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
virtualKeyboard->moveCursorUp();
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
virtualKeyboard->moveCursorDown();
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
virtualKeyboard->handlePress();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
virtualKeyboard->handleLongPress();
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback;
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
if (callback) {
callback("");
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
// Consume the event after processing for virtual keyboard
inEvent.inputEvent = INPUT_BROKER_NONE;
}
// Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
// Draw the virtual keyboard
virtualKeyboard->draw(display, 0, 0);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
resetBanner();
}
}
bool NotificationRenderer::isOverlayBannerShowing()
{
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);

View File

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

View File

@@ -20,9 +20,7 @@
// External variables
extern graphics::Screen *screen;
#if defined(M5STACK_UNITC6L)
static uint32_t lastSwitchTime = 0;
#endif
namespace graphics
{
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
@@ -118,124 +116,64 @@ void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, con
}
// Draw GPS status coordinates
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps,
const char *mode)
void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{
auto gpsFormat = uiconfig.gps_format;
auto gpsFormat = config.display.gps_format;
char displayLine[32];
if (!gps->getIsConnected() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, "No GPS present");
display->drawString(x, y, displayLine);
}
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
} else if (!gps->getHasLock() && !config.position.fixed_position) {
if (strcmp(mode, "line1") == 0) {
strcpy(displayLine, "No GPS Lock");
display->drawString(x, y, displayLine);
}
display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
} else {
geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude()));
if (gpsFormat != meshtastic_DeviceUIConfig_GpsCoordinateFormat_DMS) {
char coordinateLine_1[22];
char coordinateLine_2[22];
if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %f", geoCoord.getLatitude() * 1e-7);
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %f", geoCoord.getLongitude() * 1e-7);
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %06u E", geoCoord.getUTMZone(),
geoCoord.getUTMBand(), geoCoord.getUTMEasting());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%07u N", geoCoord.getUTMNorthing());
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%2i%1c %1c%1c", geoCoord.getMGRSZone(),
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getMGRSEasting(),
geoCoord.getMGRSNorthing());
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC) { // Open Location Code
geoCoord.getOLCCode(coordinateLine_1);
coordinateLine_2[0] = '\0';
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') { // OSGR is only valid around the UK region
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%s", "Out of Boundary");
coordinateLine_2[0] = '\0';
if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) {
char coordinateLine[22];
if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees
snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7,
geoCoord.getLongitude() * 1e-7);
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(),
geoCoord.getUTMEasting(), geoCoord.getUTMNorthing());
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System
snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(),
geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(),
geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing());
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code
geoCoord.getOLCCode(coordinateLine);
} else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference
if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region
snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary");
else
snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(),
geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing());
}
// If fixed position, display text "Fixed GPS" alternating with the coordinates.
if (config.position.fixed_position) {
if ((millis() / 10000) % 2) {
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y,
coordinateLine);
} else {
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "%1c%1c", geoCoord.getOSGRE100k(),
geoCoord.getOSGRN100k());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "%05u E %05u N", geoCoord.getOSGREasting(),
geoCoord.getOSGRNorthing());
display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS");
}
} else if (gpsFormat == meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) { // Maidenhead Locator System
double lat = geoCoord.getLatitude() * 1e-7;
double lon = geoCoord.getLongitude() * 1e-7;
// Normalize
if (lat > 90.0)
lat = 90.0;
if (lat < -90.0)
lat = -90.0;
while (lon < -180.0)
lon += 360.0;
while (lon >= 180.0)
lon -= 360.0;
double adjLon = lon + 180.0;
double adjLat = lat + 90.0;
char maiden[10]; // enough for 8-char + null
// Field (2 letters)
int lonField = int(adjLon / 20.0);
int latField = int(adjLat / 10.0);
adjLon -= lonField * 20.0;
adjLat -= latField * 10.0;
// Square (2 digits)
int lonSquare = int(adjLon / 2.0);
int latSquare = int(adjLat / 1.0);
adjLon -= lonSquare * 2.0;
adjLat -= latSquare * 1.0;
// Subsquare (2 letters)
double lonUnit = 2.0 / 24.0;
double latUnit = 1.0 / 24.0;
int lonSub = int(adjLon / lonUnit);
int latSub = int(adjLat / latUnit);
snprintf(maiden, sizeof(maiden), "%c%c%c%c%c%c", 'A' + lonField, 'A' + latField, '0' + lonSquare, '0' + latSquare,
'A' + lonSub, 'A' + latSub);
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "MH: %s", maiden);
coordinateLine_2[0] = '\0'; // only need one line
}
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
} else if (strcmp(mode, "combined") == 0) {
display->drawString(x, y, coordinateLine_1);
if (coordinateLine_2[0] != '\0') {
display->drawString(x + display->getStringWidth(coordinateLine_1), y, coordinateLine_2);
}
}
} else {
char coordinateLine_1[22];
char coordinateLine_2[22];
snprintf(coordinateLine_1, sizeof(coordinateLine_1), "Lat: %2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(),
geoCoord.getDMSLatMin(), geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
snprintf(coordinateLine_2, sizeof(coordinateLine_2), "Lon: %3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(),
geoCoord.getDMSLonMin(), geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
if (strcmp(mode, "line1") == 0) {
display->drawString(x, y, coordinateLine_1);
} else if (strcmp(mode, "line2") == 0) {
display->drawString(x, y, coordinateLine_2);
} else { // both
display->drawString(x, y, coordinateLine_1);
display->drawString(x, y + 10, coordinateLine_2);
display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine);
}
} else {
char latLine[22];
char lonLine[22];
snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(),
geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP());
snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(),
geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP());
display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1,
latLine);
display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine);
}
}
}
@@ -291,9 +229,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
return;
uint32_t now = millis();
display->clear();
#if defined(M5STACK_UNITC6L)
uint32_t now = millis();
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
{
display->display();
@@ -738,6 +676,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
int textWidth = 0;
int nameX = 0;
int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
@@ -998,26 +937,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
config.display.heading_bold = false;
const char *displayLine = ""; // Initialize to empty string by default
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
bool usePhoneGPS = (ourNode && nodeDB->hasValidPosition(ourNode) &&
config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED);
if (usePhoneGPS) {
// Phone-provided GPS is active
displayLine = "Phone GPS";
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// GPS disabled / not present
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
if (config.position.fixed_position) {
displayLine = "Fixed GPS";
} else {
@@ -1034,7 +954,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
// Onboard GPS
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
}
@@ -1061,62 +980,46 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
// If GPS is off, no need to display these parts
if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) {
// === Second Row: Last GPS Fix ===
if (gpsStatus->getLastFixMillis() > 0) {
uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix
uint32_t days = delta / 86400;
uint32_t hours = (delta % 86400) / 3600;
uint32_t mins = (delta % 3600) / 60;
uint32_t secs = delta % 60;
char buf[32];
#if defined(USE_EINK)
// E-Ink: skip seconds, show only days/hours/mins
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else {
snprintf(buf, sizeof(buf), "Last: %um", mins);
}
#else
// Non E-Ink: include seconds where useful
if (days > 0) {
snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours);
} else if (hours > 0) {
snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins);
} else if (mins > 0) {
snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs);
} else {
snprintf(buf, sizeof(buf), "Last: %us", secs);
}
// === Second Row: Date ===
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
char datetimeStr[25];
bool showTime = false; // set to true for full datetime
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
char fullLine[40];
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
#if !defined(M5STACK_UNITC6L)
display->drawString(0, getTextPositions(display)[line++], fullLine);
#endif
display->drawString(0, getTextPositions(display)[line++], buf);
} else {
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
}
// === Third Row: Latitude ===
char latStr[32];
#if defined(M5STACK_UNITC6L)
snprintf(latStr, sizeof(latStr), "Lat:%.5f", geoCoord.getLatitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++] + 2, latStr);
#else
snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++], latStr);
#endif
// === Third Row: Line 1 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
// === Fourth Row: Longitude ===
char lonStr[32];
#if defined(M5STACK_UNITC6L)
snprintf(lonStr, sizeof(lonStr), "Lon:%.3f", geoCoord.getLongitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++] + 4, lonStr);
#else
snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7);
display->drawString(x, getTextPositions(display)[line++], lonStr);
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
// === Fourth Row: Line 2 GPS Info ===
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
}
// === Final Row: Altitude ===
char altitudeLine[32] = {0};
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
? ourNode->position.altitude
: geoCoord.getAltitude();
// === Fifth Row: Altitude ===
char DisplayLineTwo[32] = {0};
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0fft", alt * METERS_TO_FEET);
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
} else {
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
}
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
#endif
}
#if !defined(M5STACK_UNITC6L)
// === Draw Compass if heading is valid ===
@@ -1201,7 +1104,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
}
}
#endif
#endif // HAS_GPS
#endif
}
#ifdef USERPREFS_OEM_TEXT
@@ -1294,13 +1197,14 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
#if defined(USE_EINK)
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
static bool navBarPrevVisible = false;
if (navBarVisible && !navBarPrevVisible) {

View File

@@ -1,6 +1,5 @@
#pragma once
#include "NodeDB.h"
#include "graphics/Screen.h"
#include "graphics/emotes.h"
#include <OLEDDisplay.h>
@@ -38,8 +37,7 @@ class UIRenderer
// GPS status functions
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus,
const char *mode = "line1");
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);

View File

@@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = {
0xA9 // Row 7: #..#.#.#
};
// 🪙 System Icon
const uint8_t icon_system[] PROGMEM = {
// 🪙 Memory Icon
const uint8_t icon_memory[] PROGMEM = {
0x24, // Row 0: ..#..#..
0x3C, // Row 1: ..####..
0xC3, // Row 2: ##....##
@@ -287,79 +287,6 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101
#define analog_icon_clock_height 8
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
0b00100100, 0b01000010, 0b01000010, 0b11111111};
#define chirpy_width 38
#define chirpy_height 50
const uint8_t chirpy[] = {
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1,
0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff,
0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3,
0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03,
0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0,
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
#define chirpy_width_hirez 76
#define chirpy_height_hirez 100
const uint8_t chirpy_hirez[] = {
0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00,
0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff,
0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f,
0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff,
0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00,
0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc,
0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03,
0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0,
0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f,
0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0,
0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff,
0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03,
0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00,
0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00,
0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0,
0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00,
0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f,
0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c,
0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00,
0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00,
0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc,
0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03,
0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00,
0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0,
0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3};
#define chirpy_small_image_width 8
#define chirpy_small_image_height 8
const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
#ifdef M5STACK_UNITC6L
#include "img/icon_small.xbm"
#else

View File

@@ -41,78 +41,78 @@ void tftSetup(void)
PacketAPI::create(PacketServer::init());
deviceScreen->init(new PacketClient);
#else
if (portduino_config.displayPanel != no_screen) {
if (settingsMap[displayPanel] != no_screen) {
DisplayDriverConfig displayConfig;
static char *panels[] = {"NOSCREEN", "X11", "FB", "ST7789", "ST7735", "ST7735S",
"ST7796", "ILI9341", "ILI9342", "ILI9486", "ILI9488", "HX8357D"};
static char *touch[] = {"NOTOUCH", "XPT2046", "STMPE610", "GT911", "FT5x06"};
#if defined(USE_X11)
if (portduino_config.displayPanel == x11) {
if (portduino_config.displayWidth && portduino_config.displayHeight)
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)portduino_config.displayWidth,
(uint16_t)portduino_config.displayHeight);
if (settingsMap[displayPanel] == x11) {
if (settingsMap[displayWidth] && settingsMap[displayHeight])
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::X11, (uint16_t)settingsMap[displayWidth],
(uint16_t)settingsMap[displayHeight]);
else
displayConfig.device(DisplayDriverConfig::device_t::X11);
} else
#elif defined(USE_FRAMEBUFFER)
if (portduino_config.displayPanel == fb) {
if (portduino_config.displayWidth && portduino_config.displayHeight)
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)portduino_config.displayWidth,
(uint16_t)portduino_config.displayHeight);
if (settingsMap[displayPanel] == fb) {
if (settingsMap[displayWidth] && settingsMap[displayHeight])
displayConfig = DisplayDriverConfig(DisplayDriverConfig::device_t::FB, (uint16_t)settingsMap[displayWidth],
(uint16_t)settingsMap[displayHeight]);
else
displayConfig.device(DisplayDriverConfig::device_t::FB);
} else
#endif
{
displayConfig.device(DisplayDriverConfig::device_t::CUSTOM_TFT)
.panel(DisplayDriverConfig::panel_config_t{.type = panels[portduino_config.displayPanel],
.panel_width = (uint16_t)portduino_config.displayWidth,
.panel_height = (uint16_t)portduino_config.displayHeight,
.rotation = (bool)portduino_config.displayRotate,
.pin_cs = (int16_t)portduino_config.displayCS.pin,
.pin_rst = (int16_t)portduino_config.displayReset.pin,
.offset_x = (uint16_t)portduino_config.displayOffsetX,
.offset_y = (uint16_t)portduino_config.displayOffsetY,
.offset_rotation = (uint8_t)portduino_config.displayOffsetRotate,
.invert = portduino_config.displayInvert ? true : false,
.rgb_order = (bool)portduino_config.displayRGBOrder,
.dlen_16bit = portduino_config.displayPanel == ili9486 ||
portduino_config.displayPanel == ili9488})
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)portduino_config.displayBusFrequency,
.panel(DisplayDriverConfig::panel_config_t{.type = panels[settingsMap[displayPanel]],
.panel_width = (uint16_t)settingsMap[displayWidth],
.panel_height = (uint16_t)settingsMap[displayHeight],
.rotation = (bool)settingsMap[displayRotate],
.pin_cs = (int16_t)settingsMap[displayCS],
.pin_rst = (int16_t)settingsMap[displayReset],
.offset_x = (uint16_t)settingsMap[displayOffsetX],
.offset_y = (uint16_t)settingsMap[displayOffsetY],
.offset_rotation = (uint8_t)settingsMap[displayOffsetRotate],
.invert = settingsMap[displayInvert] ? true : false,
.rgb_order = (bool)settingsMap[displayRGBOrder],
.dlen_16bit = settingsMap[displayPanel] == ili9486 ||
settingsMap[displayPanel] == ili9488})
.bus(DisplayDriverConfig::bus_config_t{.freq_write = (uint32_t)settingsMap[displayBusFrequency],
.freq_read = 16000000,
.spi{.pin_dc = (int8_t)portduino_config.displayDC.pin,
.spi{.pin_dc = (int8_t)settingsMap[displayDC],
.use_lock = true,
.spi_host = (uint16_t)portduino_config.display_spi_dev_int}})
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = portduino_config.keyboardDevice,
.pointerDevice = portduino_config.pointerDevice})
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)portduino_config.displayBacklight.pin,
.pwm_channel = (int8_t)portduino_config.displayBacklightPWMChannel.pin,
.invert = (bool)portduino_config.displayBacklightInvert});
if (portduino_config.touchscreenI2CAddr == -1) {
.spi_host = (uint16_t)settingsMap[displayspidev]}})
.input(DisplayDriverConfig::input_config_t{.keyboardDevice = settingsStrings[keyboardDevice],
.pointerDevice = settingsStrings[pointerDevice]})
.light(DisplayDriverConfig::light_config_t{.pin_bl = (int16_t)settingsMap[displayBacklight],
.pwm_channel = (int8_t)settingsMap[displayBacklightPWMChannel],
.invert = (bool)settingsMap[displayBacklightInvert]});
if (settingsMap[touchscreenI2CAddr] == -1) {
displayConfig.touch(
DisplayDriverConfig::touch_config_t{.type = touch[portduino_config.touchscreenModule],
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
DisplayDriverConfig::touch_config_t{.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
.spi{
.spi_host = (int8_t)portduino_config.touchscreen_spi_dev_int,
.spi_host = (int8_t)settingsMap[touchscreenspidev],
},
.pin_cs = (int16_t)portduino_config.touchscreenCS.pin});
.pin_cs = (int16_t)settingsMap[touchscreenCS]});
} else {
displayConfig.touch(DisplayDriverConfig::touch_config_t{
.type = touch[portduino_config.touchscreenModule],
.freq = (uint32_t)portduino_config.touchscreenBusFrequency,
.type = touch[settingsMap[touchscreenModule]],
.freq = (uint32_t)settingsMap[touchscreenBusFrequency],
.x_min = 0,
.x_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayWidth
: portduino_config.displayHeight) -
.x_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayWidth] : settingsMap[displayHeight]) -
1),
.y_min = 0,
.y_max = (int16_t)((portduino_config.touchscreenRotate & 1 ? portduino_config.displayHeight
: portduino_config.displayWidth) -
.y_max =
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
1),
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
.i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}});
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
}
}
deviceScreen = &DeviceScreen::create(&displayConfig);

View File

@@ -274,13 +274,8 @@ int32_t ButtonThread::runOnce()
}
}
btnEvent = BUTTON_EVENT_NONE;
// only pull when the button is pressed, we get notified via IRQ on a new press
if (!userButton.isIdle() || waitingForLongPress) {
return 50;
}
return 100; // FIXME: Why can't we rely on interrupts and use INT32_MAX here?
}
/*
* Attach (or re-attach) hardware interrupts for buttons

View File

@@ -76,9 +76,6 @@ class ButtonThread : public Observable<const InputEvent *>, public concurrency::
return digitalRead(buttonPin); // Most buttons are active low by default
}
// Returns true while this thread's button is physically held down
bool isHeld() { return isButtonPressed(_pinNum); }
// Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32
int beforeLightSleep(void *unused);

View File

@@ -188,7 +188,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length)
// Feed input to the canned messages module
void ExpressLRSFiveWay::sendKey(input_broker_event key)
{
InputEvent e = {};
InputEvent e;
e.source = inputSourceName;
e.inputEvent = key;
notifyObservers(&e);

View File

@@ -3,66 +3,16 @@
InputBroker *inputBroker = nullptr;
InputBroker::InputBroker()
{
#ifdef HAS_FREE_RTOS
inputEventQueue = xQueueCreate(5, sizeof(InputEvent));
pollSoonQueue = xQueueCreate(5, sizeof(InputPollable *));
xTaskCreate(pollSoonWorker, "input-pollSoon", 2 * 1024, this, 10, &pollSoonTask);
#endif
}
InputBroker::InputBroker(){};
void InputBroker::registerSource(Observable<const InputEvent *> *source)
{
this->inputEventObserver.observe(source);
}
#ifdef HAS_FREE_RTOS
void InputBroker::requestPollSoon(InputPollable *pollable)
{
if (xPortInIsrContext() == pdTRUE) {
xQueueSendFromISR(pollSoonQueue, &pollable, NULL);
} else {
xQueueSend(pollSoonQueue, &pollable, 0);
}
}
void InputBroker::queueInputEvent(const InputEvent *event)
{
if (xPortInIsrContext() == pdTRUE) {
xQueueSendFromISR(inputEventQueue, event, NULL);
} else {
xQueueSend(inputEventQueue, event, portMAX_DELAY);
}
}
void InputBroker::processInputEventQueue()
{
InputEvent event;
while (xQueueReceive(inputEventQueue, &event, 0)) {
handleInputEvent(&event);
}
}
#endif
int InputBroker::handleInputEvent(const InputEvent *event)
{
powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release
this->notifyObservers(event);
return 0;
}
#ifdef HAS_FREE_RTOS
void InputBroker::pollSoonWorker(void *p)
{
InputBroker *instance = (InputBroker *)p;
while (true) {
InputPollable *pollable = NULL;
xQueueReceive(instance->pollSoonQueue, &pollable, portMAX_DELAY);
if (pollable) {
pollable->pollOnce();
}
}
vTaskDelete(NULL);
}
#endif

View File

@@ -1,14 +1,9 @@
#pragma once
#include "Observer.h"
#include "freertosinc.h"
enum input_broker_event {
INPUT_BROKER_NONE = 0,
INPUT_BROKER_SELECT = 10,
INPUT_BROKER_SELECT_LONG = 11,
INPUT_BROKER_UP_LONG = 12,
INPUT_BROKER_DOWN_LONG = 13,
INPUT_BROKER_UP = 17,
INPUT_BROKER_DOWN = 18,
INPUT_BROKER_LEFT = 19,
@@ -43,13 +38,6 @@ typedef struct _InputEvent {
uint16_t touchX;
uint16_t touchY;
} InputEvent;
class InputPollable
{
public:
virtual void pollOnce() = 0;
};
class InputBroker : public Observable<const InputEvent *>
{
CallbackObserver<InputBroker, const InputEvent *> inputEventObserver =
@@ -59,22 +47,9 @@ class InputBroker : public Observable<const InputEvent *>
InputBroker();
void registerSource(Observable<const InputEvent *> *source);
void injectInputEvent(const InputEvent *event) { handleInputEvent(event); }
#ifdef HAS_FREE_RTOS
void requestPollSoon(InputPollable *pollable);
void queueInputEvent(const InputEvent *event);
void processInputEventQueue();
#endif
protected:
int handleInputEvent(const InputEvent *event);
private:
#ifdef HAS_FREE_RTOS
QueueHandle_t inputEventQueue;
QueueHandle_t pollSoonQueue;
TaskHandle_t pollSoonTask;
static void pollSoonWorker(void *p);
#endif
};
extern InputBroker *inputBroker;

View File

@@ -33,9 +33,9 @@ int32_t LinuxInput::runOnce()
{
if (firstTime) {
if (portduino_config.keyboardDevice == "")
if (settingsStrings[keyboardDevice] == "")
return disable();
fd = open(portduino_config.keyboardDevice.c_str(), O_RDWR);
fd = open(settingsStrings[keyboardDevice].c_str(), O_RDWR);
if (fd < 0)
return disable();
ret = ioctl(fd, EVIOCGRAB, (void *)1);
@@ -73,7 +73,7 @@ int32_t LinuxInput::runOnce()
int rd = read(events[i].data.fd, ev, sizeof(ev));
assert(rd > ((signed int)sizeof(struct input_event)));
for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) {
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
e.kbchar = 0;

View File

@@ -8,7 +8,7 @@
RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl()
RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME)
{
rotary = nullptr;
}
@@ -18,6 +18,7 @@ bool RotaryEncoderImpl::init()
if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 ||
moduleConfig.canned_message.inputbroker_pin_b == 0) {
// Input device is disabled.
disable();
return false;
}
@@ -29,11 +30,7 @@ bool RotaryEncoderImpl::init()
moduleConfig.canned_message.inputbroker_pin_press);
rotary->resetButton();
interruptInstance = this;
auto interruptHandler = []() { inputBroker->requestPollSoon(interruptInstance); };
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_a, interruptHandler, CHANGE);
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_b, interruptHandler, CHANGE);
attachInterrupt(moduleConfig.canned_message.inputbroker_pin_press, interruptHandler, CHANGE);
inputBroker->registerSource(this);
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
@@ -41,36 +38,36 @@ bool RotaryEncoderImpl::init()
return true;
}
void RotaryEncoderImpl::pollOnce()
int32_t RotaryEncoderImpl::runOnce()
{
InputEvent e{ORIGIN_NAME, INPUT_BROKER_NONE, 0, 0, 0};
InputEvent e{originName, INPUT_BROKER_NONE, 0, 0, 0};
static uint32_t lastPressed = millis();
if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
if (lastPressed + 200 < millis()) {
LOG_DEBUG("Rotary event Press");
lastPressed = millis();
e.inputEvent = this->eventPressed;
inputBroker->queueInputEvent(&e);
}
}
} else {
switch (rotary->process()) {
case RotaryEncoder::DIRECTION_CW:
LOG_DEBUG("Rotary event CW");
e.inputEvent = this->eventCw;
inputBroker->queueInputEvent(&e);
break;
case RotaryEncoder::DIRECTION_CCW:
LOG_DEBUG("Rotary event CCW");
e.inputEvent = this->eventCcw;
inputBroker->queueInputEvent(&e);
break;
default:
break;
}
}
RotaryEncoderImpl *RotaryEncoderImpl::interruptInstance;
if (e.inputEvent != INPUT_BROKER_NONE) {
this->notifyObservers(&e);
}
return 10;
}
#endif

View File

@@ -1,6 +1,6 @@
#pragma once
// This is a version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
#include "InputBroker.h"
#include "concurrency/OSThread.h"
@@ -8,21 +8,21 @@
class RotaryEncoder;
class RotaryEncoderImpl : public InputPollable
class RotaryEncoderImpl : public Observable<const InputEvent *>, public concurrency::OSThread
{
public:
RotaryEncoderImpl();
bool init(void);
virtual void pollOnce() override;
protected:
static RotaryEncoderImpl *interruptInstance;
virtual int32_t runOnce() override;
input_broker_event eventCw = INPUT_BROKER_NONE;
input_broker_event eventCcw = INPUT_BROKER_NONE;
input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary;
const char *originName;
};
extern RotaryEncoderImpl *rotaryEncoderImpl;

View File

@@ -8,17 +8,15 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu
void RotaryEncoderInterruptBase::init(
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed, input_broker_event eventPressedLong,
input_broker_event eventPressed,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
{
this->_pinA = pinA;
this->_pinB = pinB;
this->_pinPress = pinPress;
this->_eventCw = eventCw;
this->_eventCcw = eventCcw;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
bool isRAK = false;
#ifdef RAK_4631
@@ -27,7 +25,7 @@ void RotaryEncoderInterruptBase::init(
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, CHANGE);
attachInterrupt(pinPress, onIntPress, RISING);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinA, INPUT_PULLUP);
@@ -45,40 +43,13 @@ void RotaryEncoderInterruptBase::init(
int32_t RotaryEncoderInterruptBase::runOnce()
{
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
unsigned long now = millis();
// Handle press long/short detection
if (this->action == ROTARY_ACTION_PRESSED) {
bool buttonPressed = !digitalRead(_pinPress);
if (!pressDetected && buttonPressed) {
pressDetected = true;
pressStartTime = now;
}
if (pressDetected) {
uint32_t duration = now - pressStartTime;
if (!buttonPressed) {
// released -> if short press, send short, else already sent long
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short");
LOG_DEBUG("Rotary event Press");
e.inputEvent = this->_eventPressed;
}
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
this->action = ROTARY_ACTION_NONE;
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
lastPressLongEventTime == 0) {
// fire single-shot long press
lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
}
} else if (this->action == ROTARY_ACTION_CW) {
LOG_DEBUG("Rotary event CW");
e.inputEvent = this->_eventCw;
@@ -91,9 +62,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
this->notifyObservers(&e);
}
if (!pressDetected) {
this->action = ROTARY_ACTION_NONE;
}
return INT32_MAX;
}
@@ -101,7 +70,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
void RotaryEncoderInterruptBase::intPressHandler()
{
this->action = ROTARY_ACTION_PRESSED;
setIntervalFromNow(20); // start checking for long/short
setIntervalFromNow(20); // TODO: this modifies a non-volatile variable!
}
void RotaryEncoderInterruptBase::intAHandler()
@@ -151,7 +120,7 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
// Logic to prevent bouncing.
newState = ROTARY_EVENT_CLEARED;
}
setIntervalFromNow(ROTARY_DELAY); // TODO: this modifies a non-volatile variable!
setIntervalFromNow(50); // TODO: this modifies a non-volatile variable!
return newState;
}

View File

@@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
public:
explicit RotaryEncoderInterruptBase(const char *name);
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed, input_broker_event eventPressedLong,
input_broker_event eventPressed,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
void intPressHandler();
@@ -33,22 +33,10 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
private:
// pins and events
uint8_t _pinA = 0;
uint8_t _pinB = 0;
uint8_t _pinPress = 0;
input_broker_event _eventCw = INPUT_BROKER_NONE;
input_broker_event _eventCcw = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName;
// Long press detection variables
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastPressLongEventTime = 0;
unsigned long lastPressKeyTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select
const unsigned long pressDebounceMs = 200; // ms
};

View File

@@ -1,6 +1,5 @@
#include "RotaryEncoderInterruptImpl1.h"
#include "InputBroker.h"
extern bool osk_found;
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
@@ -20,14 +19,12 @@ bool RotaryEncoderInterruptImpl1::init()
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
// moduleConfig.canned_message.ext_notification_module_output
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed,
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
osk_found = true;
return true;
}

View File

@@ -49,7 +49,7 @@ bool SeesawRotary::init()
int32_t SeesawRotary::runOnce()
{
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
bool currentlyPressed = !ss.digitalRead(SS_SWITCH);

View File

@@ -29,7 +29,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
void SerialKeyboard::erase()
{
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_BACK;
e.kbchar = 0x08;
e.source = this->_originName;
@@ -80,7 +80,7 @@ int32_t SerialKeyboard::runOnce()
if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but
// shouldn't be a limitation
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
// SELECT OR SEND OR CANCEL EVENT

View File

@@ -200,11 +200,6 @@ uint8_t TCA8418KeyboardBase::flush()
return count;
}
void TCA8418KeyboardBase::clearInt()
{
writeRegister(TCA8418_REG_INT_STAT, 3);
}
uint8_t TCA8418KeyboardBase::digitalRead(uint8_t pinnum) const
{
if (pinnum > TCA8418_COL9)

View File

@@ -37,8 +37,6 @@ class TCA8418KeyboardBase
virtual void begin(i2c_com_fptr_t r, i2c_com_fptr_t w, uint8_t addr = TCA8418_KB_ADDR);
virtual void reset(void);
void clearInt(void);
virtual void trigger(void);
virtual void setBacklight(bool on);

View File

@@ -105,14 +105,7 @@ void TLoraPagerKeyboard::trigger()
void TLoraPagerKeyboard::setBacklight(bool on)
{
uint32_t _brightness = 0;
if (on)
_brightness = brightness;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcWrite(KB_BL_PIN, _brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness);
#endif
toggleBacklight(!on);
}
void TLoraPagerKeyboard::pressed(uint8_t key)
@@ -199,6 +192,7 @@ void TLoraPagerKeyboard::hapticFeedback()
// toggle brightness of the backlight in three steps
void TLoraPagerKeyboard::toggleBacklight(bool off)
{
static uint32_t brightness = 0;
if (off) {
brightness = 0;
} else {
@@ -212,7 +206,11 @@ void TLoraPagerKeyboard::toggleBacklight(bool off)
}
LOG_DEBUG("Toggle backlight: %d", brightness);
setBacklight(true);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcWrite(KB_BL_PIN, brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness);
#endif
}
void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)

View File

@@ -26,5 +26,4 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
uint32_t brightness = 0;
};

View File

@@ -18,7 +18,7 @@ TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTo
void TouchScreenImpl1::init()
{
#if ARCH_PORTDUINO
if (portduino_config.touchscreenModule) {
if (settingsMap[touchscreenModule]) {
TouchScreenBase::init(true);
inputBroker->registerSource(this);
} else {
@@ -47,7 +47,7 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y)
*/
void TouchScreenImpl1::onEvent(const TouchEvent &event)
{
InputEvent e = {};
InputEvent e;
e.source = event.source;
e.kbchar = 0;
e.touchX = event.x;

View File

@@ -1,14 +1,12 @@
#include "TrackballInterruptBase.h"
#include "configuration.h"
extern bool osk_found;
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
input_broker_event eventRight, input_broker_event eventPressed,
input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(),
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
{
this->_pinDown = pinDown;
this->_pinUp = pinUp;
@@ -20,7 +18,6 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
this->_eventLeft = eventLeft;
this->_eventRight = eventRight;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
if (pinPress != 255) {
pinMode(pinPress, INPUT_PULLUP);
@@ -43,57 +40,20 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
}
LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
this->_pinLeft, this->_pinRight, pinPress);
osk_found = true;
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
pinPress);
this->setInterval(100);
}
int32_t TrackballInterruptBase::runOnce()
{
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
// Handle long press detection for press button
if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = millis() - pressStartTime;
bool buttonStillPressed = false;
#if defined(T_DECK)
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
#else
buttonStillPressed = !digitalRead(_pinPress);
#endif
if (!buttonStillPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION) {
// Short press
e.inputEvent = this->_eventPressed;
}
// Reset state
pressDetected = false;
pressStartTime = 0;
lastLongPressEventTime = 0;
this->action = TB_ACTION_NONE;
} else if (pressDuration >= LONG_PRESS_DURATION) {
// Long press detected
uint32_t currentTime = millis();
// Only trigger long press event if enough time has passed since the last one
if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventPressedLong;
lastLongPressEventTime = currentTime;
}
this->action = TB_ACTION_PRESSED_LONG;
}
}
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
if (this->action == TB_ACTION_PRESSED) {
// LOG_DEBUG("Trackball event Press");
e.inputEvent = this->_eventPressed;
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
// LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp;
@@ -108,11 +68,9 @@ int32_t TrackballInterruptBase::runOnce()
e.inputEvent = this->_eventRight;
}
#else
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
// Start long press detection
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
// LOG_DEBUG("Trackball event Press");
e.inputEvent = this->_eventPressed;
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
// LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp;
@@ -133,16 +91,10 @@ int32_t TrackballInterruptBase::runOnce()
e.kbchar = 0x00;
this->notifyObservers(&e);
}
// Only update lastEvent for non-press actions or completed press actions
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
lastEvent = action;
if (!pressDetected) {
this->action = TB_ACTION_NONE;
}
}
return 50; // Check more frequently for better long press detection
return 100;
}
void TrackballInterruptBase::intPressHandler()

View File

@@ -6,7 +6,7 @@
#ifndef TB_DIRECTION
#if ARCH_PORTDUINO
#include "PortduinoGlue.h"
#define TB_DIRECTION (PinStatus) portduino_config.lora_usb_vid
#define TB_DIRECTION (PinStatus) settingsMap[tbDirection]
#else
#define TB_DIRECTION RISING
#endif
@@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
explicit TrackballInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight,
input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)());
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(),
void (*onIntPress)());
void intPressHandler();
void intDownHandler();
void intUpHandler();
@@ -33,7 +33,6 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
enum TrackballInterruptBaseActionType {
TB_ACTION_NONE,
TB_ACTION_PRESSED,
TB_ACTION_PRESSED_LONG,
TB_ACTION_UP,
TB_ACTION_DOWN,
TB_ACTION_LEFT,
@@ -47,20 +46,12 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
// Long press detection for press button
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastLongPressEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 500; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events
private:
input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventLeft = INPUT_BROKER_NONE;
input_broker_event _eventRight = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName;
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
};

View File

@@ -13,12 +13,11 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
input_broker_event eventLeft = INPUT_BROKER_LEFT;
input_broker_event eventRight = INPUT_BROKER_RIGHT;
input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight,
eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown,
TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft,
TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed);
eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp,
TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight,
TrackballInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
}

View File

@@ -7,22 +7,14 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre
}
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong,
input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(),
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(),
void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs)
{
this->_pinDown = pinDown;
this->_pinUp = pinUp;
this->_pinPress = pinPress;
this->_eventDown = eventDown;
this->_eventUp = eventUp;
this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
this->_eventUpLong = eventUpLong;
this->_eventDownLong = eventDownLong;
// Store debounce configuration passed by caller
this->updownDebounceMs = updownDebounceMs;
bool isRAK = false;
#ifdef RAK_4631
isRAK = true;
@@ -30,110 +22,45 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress,
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, FALLING);
attachInterrupt(pinPress, onIntPress, RISING);
}
if (!isRAK || this->_pinDown != 0) {
pinMode(this->_pinDown, INPUT_PULLUP);
attachInterrupt(this->_pinDown, onIntDown, FALLING);
attachInterrupt(this->_pinDown, onIntDown, RISING);
}
if (!isRAK || this->_pinUp != 0) {
pinMode(this->_pinUp, INPUT_PULLUP);
attachInterrupt(this->_pinUp, onIntUp, FALLING);
attachInterrupt(this->_pinUp, onIntUp, RISING);
}
LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress);
this->setInterval(20);
this->setInterval(100);
}
int32_t UpDownInterruptBase::runOnce()
{
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
unsigned long now = millis();
// Read all button states once at the beginning
bool pressButtonPressed = !digitalRead(_pinPress);
bool upButtonPressed = !digitalRead(_pinUp);
bool downButtonPressed = !digitalRead(_pinDown);
// Handle initial button press detection - only if not already detected
if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) {
pressDetected = true;
pressStartTime = now;
} else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) {
upDetected = true;
upStartTime = now;
} else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) {
downDetected = true;
downStartTime = now;
}
// Handle long press detection for press button
if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = now - pressStartTime;
if (!pressButtonPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
if (this->action == UPDOWN_ACTION_PRESSED) {
if (now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("GPIO event Press");
e.inputEvent = this->_eventPressed;
}
// Reset state
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
} else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) {
// First long press event only - avoid repeated events causing lag
e.inputEvent = this->_eventPressedLong;
lastPressLongEventTime = now;
}
}
// Handle long press detection for up button
if (upDetected && upStartTime > 0) {
uint32_t upDuration = now - upStartTime;
if (!upButtonPressed) {
// Button released
if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) {
} else if (this->action == UPDOWN_ACTION_UP) {
if (now - lastUpKeyTime >= updownDebounceMs) {
lastUpKeyTime = now;
LOG_DEBUG("GPIO event Up");
e.inputEvent = this->_eventUp;
}
// Reset state
upDetected = false;
upStartTime = 0;
lastUpLongEventTime = 0;
} else if (upDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventUpLong;
lastUpLongEventTime = now;
}
}
}
// Handle long press detection for down button
if (downDetected && downStartTime > 0) {
uint32_t downDuration = now - downStartTime;
if (!downButtonPressed) {
// Button released
if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) {
} else if (this->action == UPDOWN_ACTION_DOWN) {
if (now - lastDownKeyTime >= updownDebounceMs) {
lastDownKeyTime = now;
LOG_DEBUG("GPIO event Down");
e.inputEvent = this->_eventDown;
}
// Reset state
downDetected = false;
downStartTime = 0;
lastDownLongEventTime = 0;
} else if (downDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventDownLong;
lastDownLongEventTime = now;
}
}
}
if (e.inputEvent != INPUT_BROKER_NONE) {
@@ -142,11 +69,8 @@ int32_t UpDownInterruptBase::runOnce()
this->notifyObservers(&e);
}
if (!pressDetected && !upDetected && !downDetected) {
this->action = UPDOWN_ACTION_NONE;
}
return 20; // This will control how the input frequency
return 100;
}
void UpDownInterruptBase::intPressHandler()

View File

@@ -8,8 +8,7 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
public:
explicit UpDownInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong,
input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
unsigned long updownDebounceMs = 50);
void intPressHandler();
void intDownHandler();
@@ -18,41 +17,16 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
int32_t runOnce() override;
protected:
enum UpDownInterruptBaseActionType {
UPDOWN_ACTION_NONE,
UPDOWN_ACTION_PRESSED,
UPDOWN_ACTION_PRESSED_LONG,
UPDOWN_ACTION_UP,
UPDOWN_ACTION_UP_LONG,
UPDOWN_ACTION_DOWN,
UPDOWN_ACTION_DOWN_LONG
};
enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN };
volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE;
// Long press detection variables
uint32_t pressStartTime = 0;
uint32_t upStartTime = 0;
uint32_t downStartTime = 0;
bool pressDetected = false;
bool upDetected = false;
bool downDetected = false;
uint32_t lastPressLongEventTime = 0;
uint32_t lastUpLongEventTime = 0;
uint32_t lastDownLongEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300;
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300;
private:
uint8_t _pinDown = 0;
uint8_t _pinUp = 0;
uint8_t _pinPress = 0;
input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
input_broker_event _eventUpLong = INPUT_BROKER_NONE;
input_broker_event _eventDownLong = INPUT_BROKER_NONE;
const char *_originName;
unsigned long lastUpKeyTime = 0;

View File

@@ -1,6 +1,5 @@
#include "UpDownInterruptImpl1.h"
#include "InputBroker.h"
extern bool osk_found;
UpDownInterruptImpl1 *upDownInterruptImpl1;
@@ -18,18 +17,13 @@ bool UpDownInterruptImpl1::init()
uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b;
uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press;
input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN
input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP
input_broker_event eventDown = INPUT_BROKER_DOWN;
input_broker_event eventUp = INPUT_BROKER_UP;
input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
input_broker_event eventUpLong = INPUT_BROKER_UP_LONG;
input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG;
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong,
eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp,
UpDownInterruptImpl1::handleIntPressed);
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown,
UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this);
osk_found = true;
return true;
}

View File

@@ -90,7 +90,7 @@ int32_t KbI2cBase::runOnce()
while (keyCount--) {
const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent();
if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) {
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
switch (key.key) {
@@ -187,7 +187,7 @@ int32_t KbI2cBase::runOnce()
}
case 0x37: { // MPR121
MPRkeyboard.trigger();
InputEvent e = {};
InputEvent e;
while (MPRkeyboard.hasEvent()) {
char nextEvent = MPRkeyboard.dequeueEvent();
@@ -250,7 +250,7 @@ int32_t KbI2cBase::runOnce()
}
case 0x84: { // Adafruit TCA8418
TCAKeyboard.trigger();
InputEvent e = {};
InputEvent e;
while (TCAKeyboard.hasEvent()) {
char nextEvent = TCAKeyboard.dequeueEvent();
e.inputEvent = INPUT_BROKER_ANYKEY;
@@ -333,7 +333,6 @@ int32_t KbI2cBase::runOnce()
}
TCAKeyboard.trigger();
}
TCAKeyboard.clearInt();
break;
}
case 0x02: {
@@ -351,7 +350,7 @@ int32_t KbI2cBase::runOnce()
}
if (PrintDataBuf != 0) {
LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf);
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_MATRIXKEY;
e.source = this->_originName;
e.kbchar = PrintDataBuf;
@@ -366,7 +365,7 @@ int32_t KbI2cBase::runOnce()
if (i2cBus->available()) {
char c = i2cBus->read();
InputEvent e = {};
InputEvent e;
e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName;
switch (c) {
@@ -521,10 +520,3 @@ int32_t KbI2cBase::runOnce()
}
return 300;
}
void KbI2cBase::toggleBacklight(bool on)
{
#if defined(T_LORA_PAGER)
TCAKeyboard.setBacklight(on);
#endif
}

View File

@@ -12,7 +12,6 @@ class KbI2cBase : public Observable<const InputEvent *>, public concurrency::OST
{
public:
explicit KbI2cBase(const char *name);
void toggleBacklight(bool on);
protected:
virtual int32_t runOnce() override;

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