mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-14 14:52:32 +00:00
Compare commits
88 Commits
fix-msg-al
...
arduino-es
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b05f090dc | ||
|
|
e9b87f7a81 | ||
|
|
173b75a1c0 | ||
|
|
b0dae54c97 | ||
|
|
71d84404c6 | ||
|
|
6f5bdd73cb | ||
|
|
cc3c568501 | ||
|
|
13ebceb3bc | ||
|
|
70724bef72 | ||
|
|
bf4e2e8e86 | ||
|
|
2dc7760508 | ||
|
|
d201f6a1ed | ||
|
|
9977035499 | ||
|
|
096afa07f8 | ||
|
|
760471d620 | ||
|
|
6165b4f7a9 | ||
|
|
ae814b5463 | ||
|
|
4ee07226e4 | ||
|
|
78dfb05eeb | ||
|
|
9211b1bb4b | ||
|
|
70ac3601b0 | ||
|
|
51acd92a37 | ||
|
|
6d2093650a | ||
|
|
b6dd99917d | ||
|
|
d00b2afe1d | ||
|
|
e49b07ac8c | ||
|
|
8989de118c | ||
|
|
1914fa0321 | ||
|
|
d47d249550 | ||
|
|
962e5d513c | ||
|
|
ac4bcd2f56 | ||
|
|
e17c50bb86 | ||
|
|
abc0eb196a | ||
|
|
701028b749 | ||
|
|
108bdf7b0d | ||
|
|
f267b5f5f7 | ||
|
|
0cd860e300 | ||
|
|
31fdb36987 | ||
|
|
e7741c20e4 | ||
|
|
d1d16fc25f | ||
|
|
c8afbe68b5 | ||
|
|
803e96800e | ||
|
|
6c69780615 | ||
|
|
ff28355e16 | ||
|
|
d5bb566276 | ||
|
|
351cbd723b | ||
|
|
18d005d7e6 | ||
|
|
8791cd7851 | ||
|
|
590db89643 | ||
|
|
ea1d968777 | ||
|
|
40d728a14b | ||
|
|
e39b56547e | ||
|
|
a7f63d5783 | ||
|
|
be1a724ab4 | ||
|
|
04f98cf20e | ||
|
|
75a1a41d8c | ||
|
|
344efa6675 | ||
|
|
e919b3da9c | ||
|
|
155783c782 | ||
|
|
76bcd05259 | ||
|
|
2d29cbc34c | ||
|
|
2b77864192 | ||
|
|
10aa4cbfee | ||
|
|
c24a174490 | ||
|
|
3e5e19efec | ||
|
|
ea2c247024 | ||
|
|
0b463b6443 | ||
|
|
7d3c804279 | ||
|
|
1fa7d9f40e | ||
|
|
aa7abb1d3e | ||
|
|
effb454af0 | ||
|
|
27bb42ed3b | ||
|
|
05dc8caed8 | ||
|
|
f3734d407d | ||
|
|
bc8e509a8c | ||
|
|
74d0472834 | ||
|
|
5afc43ebc1 | ||
|
|
6cdb92b7a8 | ||
|
|
0e1872398a | ||
|
|
f25544c7e0 | ||
|
|
f40b2ba153 | ||
|
|
5033fd1f9f | ||
|
|
cd170fb011 | ||
|
|
fa169a5e43 | ||
|
|
c6e2a53a02 | ||
|
|
a286c06271 | ||
|
|
dda4b90e34 | ||
|
|
cc5dc5046c |
@@ -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];
|
||||
|
||||
5
.github/actions/setup-base/action.yml
vendored
5
.github/actions/setup-base/action.yml
vendored
@@ -11,11 +11,6 @@ runs:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
|
||||
- name: Uncomment build epoch
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
7
.github/workflows/main_matrix.yml
vendored
7
.github/workflows/main_matrix.yml
vendored
@@ -3,7 +3,7 @@ concurrency:
|
||||
group: ci-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
on:
|
||||
# # Triggers the workflow on push but only for the master branch
|
||||
# # Triggers the workflow on push but only for the main branches
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -48,10 +48,6 @@ 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: |
|
||||
@@ -262,6 +258,7 @@ jobs:
|
||||
push: false
|
||||
|
||||
gather-artifacts:
|
||||
# trunk-ignore(checkov/CKV2_GHA_1)
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
508
.github/workflows/merge_queue.yml
vendored
Normal file
508
.github/workflows/merge_queue.yml
vendored
Normal file
@@ -0,0 +1,508 @@
|
||||
name: Merge Queue
|
||||
# Not sure how concurrency works in merge_queue, removing for now.
|
||||
# concurrency:
|
||||
# group: merge-queue-${{ github.head_ref || github.run_id }}
|
||||
# cancel-in-progress: true
|
||||
on:
|
||||
# Merge group is a special trigger that is used to trigger the workflow when a merge group is created.
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
FAIL_FAST_PER_ARCH: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
- check
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
cache: pip
|
||||
- run: pip install -U platformio
|
||||
- name: Generate matrix
|
||||
id: jsonStep
|
||||
run: |
|
||||
if [[ "$GITHUB_HEAD_REF" == "" ]]; then
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}})
|
||||
else
|
||||
TARGETS=$(./bin/generate_ci_matrix.py ${{matrix.arch}} pr)
|
||||
fi
|
||||
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:
|
||||
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@v4
|
||||
- name: Get release version string
|
||||
run: |
|
||||
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||
echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
|
||||
id: version
|
||||
env:
|
||||
BUILD_LOCATION: local
|
||||
outputs:
|
||||
long: ${{ steps.version.outputs.long }}
|
||||
deb: ${{ steps.version.outputs.deb }}
|
||||
|
||||
check:
|
||||
needs: setup
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix: ${{ fromJson(needs.setup.outputs.check) }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build base
|
||||
id: base
|
||||
uses: ./.github/actions/setup-base
|
||||
- name: Check ${{ matrix.board }}
|
||||
run: bin/check-all.sh ${{ matrix.board }}
|
||||
|
||||
build-esp32:
|
||||
needs: [setup, version]
|
||||
strategy:
|
||||
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.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'
|
||||
uses: ./.github/workflows/build_debian_src.yml
|
||||
with:
|
||||
series: UNRELEASED
|
||||
build_location: local
|
||||
secrets: inherit
|
||||
|
||||
package-pio-deps-native-tft:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
with:
|
||||
pio_env: native-tft
|
||||
secrets: inherit
|
||||
|
||||
test-native:
|
||||
if: ${{ !contains(github.ref_name, 'event/') }}
|
||||
uses: ./.github/workflows/test_native.yml
|
||||
|
||||
docker-deb-amd64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: debian
|
||||
platform: linux/amd64
|
||||
runs-on: ubuntu-24.04
|
||||
push: false
|
||||
|
||||
docker-deb-amd64-tft:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: debian
|
||||
platform: linux/amd64
|
||||
runs-on: ubuntu-24.04
|
||||
push: false
|
||||
pio_env: native-tft
|
||||
|
||||
docker-alp-amd64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: alpine
|
||||
platform: linux/amd64
|
||||
runs-on: ubuntu-24.04
|
||||
push: false
|
||||
|
||||
docker-alp-amd64-tft:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: alpine
|
||||
platform: linux/amd64
|
||||
runs-on: ubuntu-24.04
|
||||
push: false
|
||||
pio_env: native-tft
|
||||
|
||||
docker-deb-arm64:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: debian
|
||||
platform: linux/arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
push: false
|
||||
|
||||
docker-deb-armv7:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
distro: debian
|
||||
platform: linux/arm/v7
|
||||
runs-on: ubuntu-24.04-arm
|
||||
push: false
|
||||
|
||||
gather-artifacts:
|
||||
# trunk-ignore(checkov/CKV2_GHA_1)
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
version,
|
||||
build-esp32,
|
||||
build-esp32s3,
|
||||
build-esp32c3,
|
||||
build-esp32c6,
|
||||
build-nrf52840,
|
||||
build-rp2040,
|
||||
build-rp2350,
|
||||
build-stm32,
|
||||
]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
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@v4
|
||||
with:
|
||||
path: ./
|
||||
pattern: firmware-${{matrix.arch}}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Move files up
|
||||
run: mv -b -t ./ ./bin/device-*.sh ./bin/device-*.bat
|
||||
|
||||
- name: Repackage in single firmware zip
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
overwrite: true
|
||||
path: |
|
||||
./firmware-*.bin
|
||||
./firmware-*.uf2
|
||||
./firmware-*.hex
|
||||
./firmware-*-ota.zip
|
||||
./device-*.sh
|
||||
./device-*.bat
|
||||
./littlefs-*.bin
|
||||
./bleota*bin
|
||||
./Meshtastic_nRF52_factory_erase*.uf2
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output
|
||||
|
||||
# For diagnostics
|
||||
- name: Show artifacts
|
||||
run: ls -lR
|
||||
|
||||
- name: Device scripts permissions
|
||||
run: |
|
||||
chmod +x ./output/device-install.sh
|
||||
chmod +x ./output/device-update.sh
|
||||
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- name: Repackage in single elfs zip
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
overwrite: true
|
||||
path: ./*.elf
|
||||
retention-days: 30
|
||||
|
||||
- uses: scruplelesswizard/comment-artifact@main
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
name: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
description: "Download firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip. This artifact will be available for 90 days from creation"
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
needs:
|
||||
- version
|
||||
- gather-artifacts
|
||||
- build-debian-src
|
||||
- package-pio-deps-native-tft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
id: create_release
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
name: Meshtastic Firmware ${{ needs.version.outputs.long }} Alpha
|
||||
tag_name: v${{ needs.version.outputs.long }}
|
||||
body: |
|
||||
Autogenerated by github action, developer should edit as required before publishing...
|
||||
|
||||
- name: Download source deb
|
||||
uses: actions/download-artifact@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@v4
|
||||
with:
|
||||
pattern: platformio-deps-native-tft-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output/pio-deps-native-tft
|
||||
|
||||
- name: Zip Linux sources
|
||||
working-directory: output
|
||||
run: |
|
||||
zip -j -9 -r ./meshtasticd-${{ needs.version.outputs.deb }}-src.zip ./debian-src
|
||||
zip -9 -r ./platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip ./pio-deps-native-tft
|
||||
|
||||
# For diagnostics
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Add Linux sources to GtiHub Release
|
||||
# Only run when targeting master branch with workflow_dispatch
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
run: |
|
||||
gh release upload v${{ needs.version.outputs.long }} ./output/meshtasticd-${{ needs.version.outputs.deb }}-src.zip
|
||||
gh release upload v${{ needs.version.outputs.long }} ./output/platformio-deps-native-tft-${{ needs.version.outputs.long }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release-firmware:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- esp32
|
||||
- esp32s3
|
||||
- esp32c3
|
||||
- esp32c6
|
||||
- nrf52840
|
||||
- rp2040
|
||||
- rp2350
|
||||
- stm32
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-artifacts, version]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./output
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Device scripts permissions
|
||||
run: |
|
||||
chmod +x ./output/device-install.sh
|
||||
chmod +x ./output/device-update.sh
|
||||
|
||||
- name: Zip firmware
|
||||
run: zip -j -9 -r ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./output
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
merge-multiple: true
|
||||
path: ./elfs
|
||||
|
||||
- name: Zip debug elfs
|
||||
run: zip -j -9 -r ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip ./elfs
|
||||
|
||||
# For diagnostics
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -lR
|
||||
|
||||
- name: Add bins and debug elfs to GitHub Release
|
||||
# Only run when targeting master branch with workflow_dispatch
|
||||
if: ${{ github.ref_name == 'master' }}
|
||||
run: |
|
||||
gh release upload v${{ needs.version.outputs.long }} ./firmware-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
gh release upload v${{ needs.version.outputs.long }} ./debug-elfs-${{matrix.arch}}-${{ needs.version.outputs.long }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-firmware:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
needs: [release-firmware, version]
|
||||
env:
|
||||
targets: |-
|
||||
esp32,esp32s3,esp32c3,esp32c6,nrf52840,rp2040,rp2350,stm32
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: firmware-{${{ env.targets }}}-${{ needs.version.outputs.long }}
|
||||
merge-multiple: true
|
||||
path: ./publish
|
||||
|
||||
- name: Publish firmware to meshtastic.github.io
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
# On event/* branches, use the event name as the destination prefix
|
||||
DEST_PREFIX: ${{ contains(github.ref_name, 'event/') && format('{0}/', github.ref_name) || '' }}
|
||||
with:
|
||||
deploy_key: ${{ secrets.DIST_PAGES_DEPLOY_KEY }}
|
||||
external_repository: meshtastic/meshtastic.github.io
|
||||
publish_branch: master
|
||||
publish_dir: ./publish
|
||||
destination_dir: ${{ env.DEST_PREFIX }}firmware-${{ needs.version.outputs.long }}
|
||||
keep_files: true
|
||||
user_name: github-actions[bot]
|
||||
user_email: github-actions[bot]@users.noreply.github.com
|
||||
commit_message: ${{ needs.version.outputs.long }}
|
||||
enable_jekyll: true
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -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.*
|
||||
|
||||
@@ -8,15 +8,15 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.469
|
||||
- renovate@41.94.0
|
||||
- checkov@3.2.471
|
||||
- renovate@41.115.2
|
||||
- prettier@3.6.2
|
||||
- trufflehog@3.90.5
|
||||
- trufflehog@3.90.6
|
||||
- yamllint@1.37.1
|
||||
- bandit@1.8.6
|
||||
- trivy@0.66.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.12.11
|
||||
- ruff@0.13.0
|
||||
- isort@6.0.1
|
||||
- markdownlint@0.45.0
|
||||
- oxipng@9.1.5
|
||||
|
||||
6
8MB_no_ota.csv
Normal file
6
8MB_no_ota.csv
Normal 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,
|
||||
|
@@ -52,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
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||
|
||||
export PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
if (echo $2 | grep -q "esp32"); then
|
||||
|
||||
@@ -6,6 +6,8 @@ from os.path import join
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from readprops import readProps
|
||||
|
||||
@@ -125,11 +127,16 @@ for pref in userPrefs:
|
||||
pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
|
||||
|
||||
# General options that are passed to the C and C++ compilers
|
||||
# Calculate unix epoch for current day (midnight)
|
||||
current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
build_epoch = int(current_date.timestamp())
|
||||
|
||||
flags = [
|
||||
"-DAPP_VERSION=" + verObj["long"],
|
||||
"-DAPP_VERSION_SHORT=" + verObj["short"],
|
||||
"-DAPP_ENV=" + env.get("PIOENV"),
|
||||
"-DAPP_REPO=" + repo_owner,
|
||||
"-DBUILD_EPOCH=" + str(build_epoch),
|
||||
] + pref_flags
|
||||
|
||||
print ("Using flags:")
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"f_boot": "120000000L",
|
||||
"boot_freq": "120000000L",
|
||||
"boot": "qio",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
|
||||
@@ -53,14 +53,14 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||
#-DBUILD_EPOCH=$UNIX_TIME
|
||||
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now
|
||||
#-D OLED_PL=1
|
||||
|
||||
monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
@@ -87,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/>
|
||||
|
||||
@@ -99,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]
|
||||
@@ -118,7 +117,7 @@ lib_deps =
|
||||
[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/233d18ef42e9d189f90fdfe621f0cd7edff2d221.zip
|
||||
https://github.com/meshtastic/device-ui/archive/9ed5355a24059750e9b2eb5d669574d9ea42a37b.zip
|
||||
|
||||
; Common libs for environmental measurements in telemetry module
|
||||
[environmental_base]
|
||||
|
||||
Submodule protobufs updated: a84657c220...945b796a98
@@ -8,7 +8,6 @@
|
||||
"replacements:all",
|
||||
"workarounds:all"
|
||||
],
|
||||
"baseBranchPatterns": ["master"],
|
||||
"forkProcessing": "enabled",
|
||||
"ignoreDeps": [
|
||||
"protobufs"
|
||||
|
||||
@@ -183,9 +183,9 @@ class AmbientLightingThread : public concurrency::OSThread
|
||||
#endif
|
||||
#endif
|
||||
pixels.show();
|
||||
LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||
moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
// LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d",
|
||||
// moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red,
|
||||
// moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue);
|
||||
#endif
|
||||
#ifdef RGBLED_CA
|
||||
analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
// Forward declarations
|
||||
#if defined(DEBUG_HEAP)
|
||||
class MemGet;
|
||||
extern MemGet memGet;
|
||||
#endif
|
||||
|
||||
// DEBUG LED
|
||||
#ifndef LED_STATE_ON
|
||||
#define LED_STATE_ON 1
|
||||
@@ -23,6 +29,7 @@
|
||||
#define MESHTASTIC_LOG_LEVEL_ERROR "ERROR"
|
||||
#define MESHTASTIC_LOG_LEVEL_CRIT "CRIT "
|
||||
#define MESHTASTIC_LOG_LEVEL_TRACE "TRACE"
|
||||
#define MESHTASTIC_LOG_LEVEL_HEAP "HEAP"
|
||||
|
||||
#include "SerialConsole.h"
|
||||
|
||||
@@ -62,6 +69,25 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(DEBUG_HEAP)
|
||||
#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__)
|
||||
|
||||
// Macro-based heap debugging
|
||||
#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap();
|
||||
#define DEBUG_HEAP_AFTER(context, ptr) \
|
||||
do { \
|
||||
auto heapAfter = memGet.getFreeHeap(); \
|
||||
if (heapBefore != heapAfter) { \
|
||||
LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#else
|
||||
#define LOG_HEAP(...)
|
||||
#define DEBUG_HEAP_BEFORE
|
||||
#define DEBUG_HEAP_AFTER(context, ptr)
|
||||
#endif
|
||||
|
||||
/// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic
|
||||
extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
|
||||
|
||||
@@ -38,46 +38,4 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
return useShortName ? "Custom" : "Invalid";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *DisplayFormatters::getDeviceRole(meshtastic_Config_DeviceConfig_Role role)
|
||||
{
|
||||
switch (role) {
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT:
|
||||
return "Client";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE:
|
||||
return "Client Mute";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN:
|
||||
return "Client Hidden";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND:
|
||||
return "Lost and Found";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TRACKER:
|
||||
return "Tracker";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_SENSOR:
|
||||
return "Sensor";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TAK:
|
||||
return "TAK";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_TAK_TRACKER:
|
||||
return "TAK Tracker";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_ROUTER:
|
||||
return "Router";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_ROUTER_LATE:
|
||||
return "Router Late";
|
||||
break;
|
||||
case meshtastic_Config_DeviceConfig_Role_REPEATER:
|
||||
return "Repeater";
|
||||
break;
|
||||
default:
|
||||
return "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,4 @@ class DisplayFormatters
|
||||
public:
|
||||
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName,
|
||||
bool usePreset);
|
||||
|
||||
public:
|
||||
static const char *getDeviceRole(meshtastic_Config_DeviceConfig_Role role);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -851,9 +851,9 @@ void Power::readPowerStatus()
|
||||
running++;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG(threadlist);
|
||||
LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
|
||||
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
|
||||
LOG_HEAP(threadlist);
|
||||
LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
|
||||
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
|
||||
lastheap = memGet.getFreeHeap();
|
||||
}
|
||||
#ifdef DEBUG_HEAP_MQTT
|
||||
|
||||
@@ -57,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
|
||||
@@ -99,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
|
||||
@@ -288,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 {
|
||||
@@ -297,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;
|
||||
}
|
||||
|
||||
@@ -28,14 +28,11 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
|
||||
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
|
||||
|
||||
@@ -86,9 +86,9 @@ void OSThread::run()
|
||||
#ifdef DEBUG_HEAP
|
||||
auto newHeap = memGet.getFreeHeap();
|
||||
if (newHeap < heap)
|
||||
LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
||||
LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
||||
if (heap < newHeap)
|
||||
LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
||||
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
|
||||
#endif
|
||||
|
||||
runned();
|
||||
|
||||
@@ -431,7 +431,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_SERIAL 1
|
||||
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1
|
||||
#define MESHTASTIC_EXCLUDE_ADMIN 1
|
||||
#define MESHTASTIC_EXCLUDE_AMBIENTLIGHTING 1
|
||||
#endif
|
||||
|
||||
// // Turn off wifi even if HW supports wifi (webserver relies on wifi and is also disabled)
|
||||
|
||||
@@ -294,6 +294,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
type = AHT10;
|
||||
break;
|
||||
#endif
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
case INA_ADDR:
|
||||
case INA_ADDR_ALTERNATE:
|
||||
case INA_ADDR_WAVESHARE_UPS:
|
||||
@@ -340,6 +341,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
// else: probably a RAK12500/UBLOX GPS on I2C
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case MCP9808_ADDR:
|
||||
// We need to check for STK8BAXX first, since register 0x07 is new data flag for the z-axis and can produce some
|
||||
// weird result. and register 0x00 doesn't seems to be colliding with MCP9808 and LIS3DH chips.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <cstring> // Include for strstr
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "configuration.h"
|
||||
@@ -808,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;
|
||||
}
|
||||
@@ -1214,7 +1205,7 @@ static const char *DETECTED_MESSAGE = "%s detected";
|
||||
LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \
|
||||
clearBuffer(); \
|
||||
_serial_gps->write(COMMAND "\r\n"); \
|
||||
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \
|
||||
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \
|
||||
if (detectedDriver != GNSS_MODEL_UNKNOWN) { \
|
||||
return detectedDriver; \
|
||||
} \
|
||||
@@ -1248,15 +1239,9 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
_serial_gps->write("$PUBX,40,GSV,0,0,0,0,0,0*59\r\n");
|
||||
_serial_gps->write("$PUBX,40,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");
|
||||
delay(20);
|
||||
|
||||
// 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);
|
||||
|
||||
std::vector<ChipInfo> atgm = {
|
||||
@@ -1382,36 +1367,55 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
return GNSS_MODEL_UNKNOWN;
|
||||
}
|
||||
|
||||
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap)
|
||||
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed)
|
||||
{
|
||||
String response = "";
|
||||
// Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline
|
||||
// Higher baud rates get proportionally larger buffers to handle more data
|
||||
int bufferSize = (serialSpeed * 256) / 9600;
|
||||
// Clamp buffer size between reasonable limits
|
||||
if (bufferSize < 128)
|
||||
bufferSize = 128;
|
||||
if (bufferSize > 2048)
|
||||
bufferSize = 2048;
|
||||
|
||||
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
|
||||
uint16_t responseLen = 0;
|
||||
unsigned long start = millis();
|
||||
while (millis() - start < timeout) {
|
||||
if (_serial_gps->available()) {
|
||||
response += (char)_serial_gps->read();
|
||||
char c = _serial_gps->read();
|
||||
|
||||
if (response.endsWith(",") || response.endsWith("\r\n")) {
|
||||
// Add char to buffer if there's space
|
||||
if (responseLen < bufferSize - 1) {
|
||||
response[responseLen++] = c;
|
||||
response[responseLen] = '\0';
|
||||
}
|
||||
|
||||
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.c_str());
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
|
||||
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
delete[] response; // Cleanup before return
|
||||
return chipInfo.driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.endsWith("\r\n")) {
|
||||
response.trim();
|
||||
response = ""; // Reset the response string for the next potential message
|
||||
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
|
||||
// Reset the response buffer for the next potential message
|
||||
responseLen = 0;
|
||||
response[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response.c_str());
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
|
||||
delete[] response; // Cleanup before return
|
||||
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
|
||||
}
|
||||
|
||||
GPS *GPS::createGps()
|
||||
@@ -1436,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
|
||||
|
||||
@@ -31,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 {
|
||||
@@ -237,7 +236,7 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap);
|
||||
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed);
|
||||
|
||||
// Get GNSS model
|
||||
GnssModel_t probe(int serialSpeed);
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
static RTCQuality currentQuality = RTCQualityNone;
|
||||
uint32_t lastSetFromPhoneNtpOrGps = 0;
|
||||
|
||||
static uint32_t lastTimeValidationWarning = 0;
|
||||
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
|
||||
|
||||
RTCQuality getRTCQuality()
|
||||
{
|
||||
return currentQuality;
|
||||
@@ -48,7 +51,9 @@ RTCSetResult readFromRTC()
|
||||
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
@@ -87,7 +92,10 @@ RTCSetResult readFromRTC()
|
||||
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, 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
|
||||
@@ -130,15 +138,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
|
||||
uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv->tv_sec < BUILD_EPOCH) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
#endif
|
||||
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;
|
||||
} else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
|
||||
BUILD_EPOCH + FORTY_YEARS);
|
||||
#endif
|
||||
} else if ((uint64_t)tv->tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
// Calculate max allowed time safely to avoid overflow in logging
|
||||
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
|
||||
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
|
||||
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
@@ -256,15 +269,20 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
#endif
|
||||
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;
|
||||
} else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH,
|
||||
BUILD_EPOCH + FORTY_YEARS);
|
||||
#endif
|
||||
} else if ((uint64_t)tv.tv_sec > ((uint64_t)BUILD_EPOCH + FORTY_YEARS)) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
// Calculate max allowed time safely to avoid overflow in logging
|
||||
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
|
||||
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
|
||||
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
}
|
||||
#endif
|
||||
@@ -324,14 +342,40 @@ uint32_t getValidTime(RTCQuality minQuality, bool local)
|
||||
time_t gm_mktime(struct tm *tm)
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_TZ
|
||||
setenv("TZ", "GMT0", 1);
|
||||
time_t res = mktime(tm);
|
||||
if (*config.device.tzdef) {
|
||||
setenv("TZ", config.device.tzdef, 1);
|
||||
} else {
|
||||
setenv("TZ", "UTC0", 1);
|
||||
time_t result = 0;
|
||||
|
||||
// First, get us to the start of tm->year, by calcuating the number of days since the Unix epoch.
|
||||
int year = 1900 + tm->tm_year; // tm_year is years since 1900
|
||||
int year_minus_one = year - 1;
|
||||
int days_before_this_year = 0;
|
||||
days_before_this_year += year_minus_one * 365;
|
||||
// leap days: every 4 years, except 100s, but including 400s.
|
||||
days_before_this_year += year_minus_one / 4 - year_minus_one / 100 + year_minus_one / 400;
|
||||
// subtract from 1970-01-01 to get days since epoch
|
||||
days_before_this_year -= 719162; // (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400);
|
||||
|
||||
// Now, within this tm->year, compute the days *before* this tm->month starts.
|
||||
int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; // non-leap year
|
||||
int days_this_year_before_this_month = days_before_month[tm->tm_mon]; // tm->tm_mon is 0..11
|
||||
|
||||
// If this is a leap year, and we're past February, add a day:
|
||||
if (tm->tm_mon >= 2 && (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0)) {
|
||||
days_this_year_before_this_month += 1;
|
||||
}
|
||||
return res;
|
||||
|
||||
// And within this month:
|
||||
int days_this_month_before_today = tm->tm_mday - 1; // tm->tm_mday is 1..31
|
||||
|
||||
// Now combine them all together, and convert days to seconds:
|
||||
result += (days_before_this_year + days_this_year_before_this_month + days_this_month_before_today);
|
||||
result *= 86400L;
|
||||
|
||||
// Finally, add in the hours, minutes, and seconds of today:
|
||||
result += tm->tm_hour * 3600;
|
||||
result += tm->tm_min * 60;
|
||||
result += tm->tm_sec;
|
||||
|
||||
return result;
|
||||
#else
|
||||
return mktime(tm);
|
||||
#endif
|
||||
|
||||
@@ -56,5 +56,5 @@ time_t gm_mktime(struct tm *tm);
|
||||
#define SEC_PER_HOUR 3600
|
||||
#define SEC_PER_MIN 60
|
||||
#ifdef BUILD_EPOCH
|
||||
#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware
|
||||
static constexpr uint64_t FORTY_YEARS = (40ULL * 365 * SEC_PER_DAY); // Use 64-bit arithmetic to prevent overflow
|
||||
#endif
|
||||
|
||||
@@ -149,11 +149,6 @@ void Screen::showSimpleBanner(const char *message, uint32_t durationMs)
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
{
|
||||
// Don't show overlay banner if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -178,11 +173,6 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
// Don't show node picker if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -206,11 +196,6 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
// Don't show number picker if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||
#endif
|
||||
@@ -231,30 +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);
|
||||
|
||||
// Start OnScreenKeyboardModule session (non-touch variant)
|
||||
OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback);
|
||||
NotificationRenderer::virtualKeyboard = OnScreenKeyboardModule::instance().getKeyboard();
|
||||
NotificationRenderer::textInputCallback = textCallback;
|
||||
|
||||
// Store the message and set the expiration timestamp (use same pattern as other notifications)
|
||||
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
|
||||
NotificationRenderer::alertBannerMessage[255] = '\0';
|
||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
|
||||
|
||||
// Set the overlay using the same pattern as other notification types
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
uint8_t module_frame;
|
||||
@@ -356,6 +317,14 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#elif defined(USE_SSD1306)
|
||||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
#elif defined(USE_SPISSD1306)
|
||||
dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48);
|
||||
if (!dispdev->init()) {
|
||||
LOG_DEBUG("Error: SSD1306 not detected!");
|
||||
} else {
|
||||
static_cast<SSD1306Spi *>(dispdev)->setHorizontalOffset(32);
|
||||
LOG_INFO("SSD1306 init success");
|
||||
}
|
||||
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
|
||||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||||
@@ -371,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);
|
||||
@@ -546,7 +515,7 @@ void Screen::setup()
|
||||
// === Apply loaded brightness ===
|
||||
#if defined(ST7789_CS)
|
||||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
|
||||
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SPISSD1306)
|
||||
dispdev->setBrightness(brightness);
|
||||
#endif
|
||||
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||
@@ -593,7 +562,7 @@ void Screen::setup()
|
||||
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
|
||||
#elif defined(USE_ST7789)
|
||||
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
|
||||
#else
|
||||
#elif !defined(M5STACK_UNITC6L)
|
||||
dispdev->flipScreenVertically();
|
||||
#endif
|
||||
}
|
||||
@@ -619,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();
|
||||
@@ -731,7 +700,11 @@ int32_t Screen::runOnce()
|
||||
|
||||
#ifndef DISABLE_WELCOME_UNSET
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
menuHandler::LoraRegionPicker();
|
||||
#else
|
||||
menuHandler::OnboardMessage();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||
@@ -752,19 +725,13 @@ int32_t Screen::runOnce()
|
||||
handleSetOn(false);
|
||||
break;
|
||||
case Cmd::ON_PRESS:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleOnPress();
|
||||
}
|
||||
handleOnPress();
|
||||
break;
|
||||
case Cmd::SHOW_PREV_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowPrevFrame();
|
||||
}
|
||||
handleShowPrevFrame();
|
||||
break;
|
||||
case Cmd::SHOW_NEXT_FRAME:
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
handleShowNextFrame();
|
||||
}
|
||||
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
|
||||
@@ -786,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();
|
||||
}
|
||||
setFrames();
|
||||
break;
|
||||
case Cmd::NOOP:
|
||||
break;
|
||||
@@ -824,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
|
||||
@@ -915,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
|
||||
@@ -942,91 +901,75 @@ void Screen::setFrames(FrameFocus focus)
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
fsi.positions.clock = numframes;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame;
|
||||
#else
|
||||
normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame
|
||||
: graphics::ClockRenderer::drawDigitalClockFrame;
|
||||
#endif
|
||||
indicatorIcons.push_back(digital_icon_clock);
|
||||
#endif
|
||||
|
||||
// Declare this early so it’s 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.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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
fsi.positions.nodelist_lastheard = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||
indicatorIcons.push_back(icon_nodes);
|
||||
|
||||
fsi.positions.nodelist_hopsignal = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||
indicatorIcons.push_back(icon_signal);
|
||||
|
||||
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);
|
||||
}
|
||||
fsi.positions.nodelist_bearings = numframes;
|
||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||
indicatorIcons.push_back(icon_list);
|
||||
|
||||
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);
|
||||
}
|
||||
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(small_chirpy);
|
||||
}
|
||||
|
||||
#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);
|
||||
@@ -1068,29 +1011,27 @@ void Screen::setFrames(FrameFocus focus)
|
||||
if (numMeshNodes > 0)
|
||||
numMeshNodes--;
|
||||
|
||||
if (!hiddenFrames.show_favorites) {
|
||||
// Temporary array to hold favorite node frames
|
||||
std::vector<FrameCallback> favoriteFrames;
|
||||
// Temporary array to hold favorite node frames
|
||||
std::vector<FrameCallback> favoriteFrames;
|
||||
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
}
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert favorite frames *after* collecting them all
|
||||
if (!favoriteFrames.empty()) {
|
||||
fsi.positions.firstFavorite = numframes;
|
||||
for (const auto &f : favoriteFrames) {
|
||||
normalFrames[numframes++] = f;
|
||||
indicatorIcons.push_back(icon_node);
|
||||
}
|
||||
fsi.positions.lastFavorite = numframes - 1;
|
||||
} else {
|
||||
fsi.positions.firstFavorite = 255;
|
||||
fsi.positions.lastFavorite = 255;
|
||||
// Insert favorite frames *after* collecting them all
|
||||
if (!favoriteFrames.empty()) {
|
||||
fsi.positions.firstFavorite = numframes;
|
||||
for (const auto &f : favoriteFrames) {
|
||||
normalFrames[numframes++] = f;
|
||||
indicatorIcons.push_back(icon_node);
|
||||
}
|
||||
fsi.positions.lastFavorite = numframes - 1;
|
||||
} else {
|
||||
fsi.positions.firstFavorite = 255;
|
||||
fsi.positions.lastFavorite = 255;
|
||||
}
|
||||
|
||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||
@@ -1129,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:
|
||||
@@ -1157,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;
|
||||
}
|
||||
|
||||
@@ -1372,6 +1242,10 @@ void Screen::handleShowNextFrame()
|
||||
|
||||
void Screen::setFastFramerate()
|
||||
{
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
dispdev->clear();
|
||||
dispdev->display();
|
||||
#endif
|
||||
// We are about to start a transition so speed up fps
|
||||
targetFramerate = SCREEN_TRANSITION_FRAMERATE;
|
||||
|
||||
@@ -1403,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
|
||||
@@ -1411,28 +1285,21 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
// Incoming message
|
||||
devicestate.has_rx_text_message = true; // Needed to include the message frame
|
||||
hasUnreadMessage = true; // Enables mail icon in the header
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||
|
||||
// Always update frame list to include new message, but defer UI updates if virtual keyboard is active
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view
|
||||
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
} else {
|
||||
// Virtual keyboard is active - just mark that frames need regeneration when keyboard closes
|
||||
// The devicestate and hasUnreadMessage are already set above, so message will appear later
|
||||
LOG_DEBUG("Virtual keyboard active - deferring frame list update for new message");
|
||||
// Only wake/force display if the configuration allows it
|
||||
if (shouldWakeOnReceivedMessage()) {
|
||||
setOn(true); // Wake up the screen first
|
||||
forceDisplay(); // Forces screen redraw
|
||||
}
|
||||
|
||||
// Show message alert - either as normal banner or as keyboard popup
|
||||
// === Common variables ===
|
||||
// === Prepare banner content ===
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from);
|
||||
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;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) {
|
||||
@@ -1442,57 +1309,31 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
|
||||
}
|
||||
}
|
||||
|
||||
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
|
||||
// === Normal banner mode ===
|
||||
char banner[256];
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName);
|
||||
} else {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
} else {
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
strcpy(banner, "Alert Received");
|
||||
}
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
} else {
|
||||
// === Virtual keyboard popup mode ===
|
||||
char title[64];
|
||||
if (isAlert) {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(title, sizeof(title), "Alert from %s", longName);
|
||||
} else {
|
||||
strcpy(title, "Alert Received");
|
||||
}
|
||||
if (longName && longName[0]) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
strcpy(banner, "New Message");
|
||||
#else
|
||||
snprintf(banner, sizeof(banner), "New Message from\n%s", longName);
|
||||
#endif
|
||||
|
||||
} else {
|
||||
if (longName && longName[0]) {
|
||||
snprintf(title, sizeof(title), "%s", longName);
|
||||
} else {
|
||||
strcpy(title, "New Message");
|
||||
}
|
||||
strcpy(banner, "New Message");
|
||||
}
|
||||
|
||||
// Prepare content - clean the message content
|
||||
char content[200];
|
||||
size_t contentLen = 0;
|
||||
for (size_t i = 0; i < packet->decoded.payload.size && i < sizeof(content) - 1; i++) {
|
||||
if (msgRaw[i] != '\x07' && msgRaw[i] != '\0') { // Skip bell character and null
|
||||
content[contentLen++] = msgRaw[i];
|
||||
}
|
||||
}
|
||||
content[contentLen] = '\0';
|
||||
|
||||
// Show popup with title and content on virtual keyboard
|
||||
NotificationRenderer::showKeyboardMessagePopupWithTitle(title, content, 5000);
|
||||
|
||||
// Force display update to show the popup immediately
|
||||
setFastFramerate();
|
||||
forceDisplay();
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->setOn(true);
|
||||
screen->showSimpleBanner(banner, 1500);
|
||||
playLongBeep();
|
||||
#else
|
||||
screen->showSimpleBanner(banner, 3000);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1502,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)
|
||||
@@ -1529,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
|
||||
@@ -1576,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) {
|
||||
@@ -1585,12 +1411,16 @@ 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();
|
||||
} else {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
menuHandler::textMessageMenu();
|
||||
#else
|
||||
menuHandler::textMessageBaseMenu();
|
||||
#endif
|
||||
}
|
||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
|
||||
@@ -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;
|
||||
@@ -81,6 +81,8 @@ class Screen
|
||||
#include <SSD1306Wire.h>
|
||||
#elif defined(USE_ST7789)
|
||||
#include <ST7789Spi.h>
|
||||
#elif defined(USE_SPISSD1306)
|
||||
#include <SSD1306Spi.h>
|
||||
#else
|
||||
// the SH1106/SSD1306 variant is auto-detected
|
||||
#include <AutoOLEDWire.h>
|
||||
@@ -313,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)
|
||||
{
|
||||
@@ -593,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
|
||||
@@ -659,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;
|
||||
@@ -669,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;
|
||||
@@ -678,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();
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
|
||||
#define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28
|
||||
#define FONT_LARGE FONT_LARGE_LOCAL // Height: 28
|
||||
#elif defined(M5STACK_UNITC6L)
|
||||
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
|
||||
#define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13
|
||||
#define FONT_LARGE FONT_SMALL_LOCAL // Height: 13
|
||||
#else
|
||||
#define FONT_SMALL FONT_SMALL_LOCAL // Height: 13
|
||||
#define FONT_MEDIUM FONT_MEDIUM_LOCAL // Height: 19
|
||||
|
||||
@@ -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);
|
||||
@@ -129,7 +124,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Battery Icons ===
|
||||
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||||
batteryX += 1;
|
||||
@@ -192,28 +187,13 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||
int 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);
|
||||
}
|
||||
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);
|
||||
@@ -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);
|
||||
}
|
||||
// === 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 ===
|
||||
@@ -368,7 +337,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
display->setColor(WHITE); // Reset for other UI
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -767,24 +767,24 @@ 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(LGFX_SDL)
|
||||
else if (portduino_config.displayPanel == x11) {
|
||||
else if (settingsMap[displayPanel] == x11) {
|
||||
_panel_instance = new lgfx::Panel_sdl;
|
||||
}
|
||||
#endif
|
||||
@@ -795,61 +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.
|
||||
_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(LGFX_SDL)
|
||||
if (portduino_config.displayPanel == x11) {
|
||||
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);
|
||||
@@ -1115,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)
|
||||
@@ -1240,7 +1240,7 @@ void TFTDisplay::sdlLoop()
|
||||
#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!");
|
||||
@@ -1288,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();
|
||||
@@ -1312,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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h"
|
||||
@@ -189,7 +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);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
@@ -293,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,
|
||||
@@ -313,7 +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);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
|
||||
@@ -277,12 +277,13 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
|
||||
|
||||
// Line 1 (Still)
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||
if (config.display.heading_bold)
|
||||
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||
|
||||
display->setColor(WHITE);
|
||||
|
||||
#endif
|
||||
// Setup string to assemble analogClock string
|
||||
std::string analogClock = "";
|
||||
|
||||
@@ -386,40 +387,46 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
char shortnameble[35];
|
||||
getMacAddr(dmac);
|
||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(shortnameble, sizeof(shortnameble), "%s", screen->ourId);
|
||||
#else
|
||||
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
|
||||
#endif
|
||||
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) {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "Reg: %s/%s", region, mode);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s", region);
|
||||
#else
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||
#endif
|
||||
}
|
||||
textWidth = display->getStringWidth(regionradiopreset);
|
||||
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();
|
||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||
if (config.lora.channel_num == 0) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz", freqStr);
|
||||
#else
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %sMHz", freqStr);
|
||||
#endif
|
||||
} else {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "%sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
#else
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %sMHz (%d)", freqStr, config.lora.channel_num);
|
||||
#endif
|
||||
}
|
||||
size_t len = strlen(frequencyslot);
|
||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||
@@ -429,7 +436,8 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
||||
|
||||
// === Fifth Row: Channel Utilization ===
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Fourth Row: Channel Utilization ===
|
||||
const char *chUtil = "ChUtil:";
|
||||
char chUtilPercentage[10];
|
||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||
@@ -446,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) {
|
||||
@@ -483,14 +491,15 @@ 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
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * 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);
|
||||
@@ -510,8 +519,11 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
#ifdef USE_EINK
|
||||
barsOffset -= 12;
|
||||
#endif
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
const int barX = x + 45 + barsOffset;
|
||||
#else
|
||||
const int barX = x + 40 + barsOffset;
|
||||
|
||||
#endif
|
||||
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||
if (total == 0)
|
||||
return;
|
||||
@@ -536,7 +548,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
// Label
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->drawString(labelX, getTextPositions(display)[line], label);
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// Bar
|
||||
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
|
||||
display->setColor(WHITE);
|
||||
@@ -544,7 +556,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
|
||||
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||
display->setColor(WHITE);
|
||||
|
||||
#endif
|
||||
// Value string
|
||||
display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
|
||||
@@ -597,10 +609,16 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
line += 1;
|
||||
}
|
||||
line += 1;
|
||||
|
||||
char appversionstr[35];
|
||||
snprintf(appversionstr, sizeof(appversionstr), "Ver: %s", optstr(APP_VERSION));
|
||||
char appversionstr_formatted[40];
|
||||
char *lastDot = strrchr(appversionstr, '.');
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (lastDot != nullptr) {
|
||||
*lastDot = '\0'; // truncate string
|
||||
}
|
||||
#else
|
||||
if (lastDot) {
|
||||
size_t prefixLen = lastDot - appversionstr;
|
||||
strncpy(appversionstr_formatted, appversionstr, prefixLen);
|
||||
@@ -611,10 +629,12 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
strncpy(appversionstr, appversionstr_formatted, sizeof(appversionstr) - 1);
|
||||
appversionstr[sizeof(appversionstr) - 1] = '\0';
|
||||
}
|
||||
#endif
|
||||
int textWidth = display->getStringWidth(appversionstr);
|
||||
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
||||
|
||||
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it
|
||||
line += 1;
|
||||
char uptimeStr[32] = "";
|
||||
@@ -633,33 +653,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * 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!");
|
||||
#endif
|
||||
}
|
||||
} // namespace DebugRenderer
|
||||
} // namespace graphics
|
||||
|
||||
@@ -31,11 +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
|
||||
|
||||
@@ -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,26 +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", "Region Picker", "Device Role"};
|
||||
enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "LoRa Actions";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Back) {
|
||||
// No action
|
||||
} else if (selected == lora_picker) {
|
||||
menuHandler::menuQueue = menuHandler::lora_picker;
|
||||
} else if (selected == device_role_picker) {
|
||||
menuHandler::menuQueue = menuHandler::device_role_picker;
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::OnboardMessage()
|
||||
{
|
||||
static const char *optionsArray[] = {"OK", "Got it!"};
|
||||
@@ -102,7 +79,11 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
"NP_865",
|
||||
"BR_902"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "LoRa Region";
|
||||
#else
|
||||
bannerOptions.message = "Set the LoRa region";
|
||||
#endif
|
||||
bannerOptions.durationMs = duration;
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 27;
|
||||
@@ -142,40 +123,6 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
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::TwelveHourPicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||
@@ -317,7 +264,11 @@ void menuHandler::TZPicker()
|
||||
|
||||
void menuHandler::clockMenu()
|
||||
{
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[] = {"Back", "Time Format", "Timezone"};
|
||||
#else
|
||||
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
|
||||
#endif
|
||||
enum optionsNumbers { Back = 0, Clock = 1, Time = 2, Timezone = 3 };
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Clock Action";
|
||||
@@ -341,8 +292,11 @@ void menuHandler::clockMenu()
|
||||
void menuHandler::messageResponseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back = 0, Dismiss = 1, Preset = 2, Freetext = 3, Aloud = 4, enumEnd = 5 };
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply Preset"};
|
||||
#else
|
||||
static const char *optionsArray[enumEnd] = {"Back", "Dismiss", "Reply via Preset"};
|
||||
#endif
|
||||
static int optionsEnumArray[enumEnd] = {Back, Dismiss, Preset};
|
||||
int options = 3;
|
||||
|
||||
@@ -356,13 +310,17 @@ void menuHandler::messageResponseMenu()
|
||||
optionsEnumArray[options++] = Aloud;
|
||||
#endif
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Message";
|
||||
#else
|
||||
bannerOptions.message = "Message Action";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
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);
|
||||
@@ -403,13 +361,14 @@ 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";
|
||||
}
|
||||
|
||||
optionsArray[options] = "Send Position";
|
||||
optionsEnumArray[options++] = Position;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "New Preset";
|
||||
#else
|
||||
optionsArray[options] = "New Preset Msg";
|
||||
#endif
|
||||
optionsEnumArray[options++] = Preset;
|
||||
if (kb_found) {
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
@@ -417,7 +376,11 @@ void menuHandler::homeBaseMenu()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Home";
|
||||
#else
|
||||
bannerOptions.message = "Home Action";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
@@ -456,6 +419,11 @@ void menuHandler::homeBaseMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::textMessageMenu()
|
||||
{
|
||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||
}
|
||||
|
||||
void menuHandler::textMessageBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
||||
@@ -487,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;
|
||||
@@ -499,14 +467,17 @@ 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
|
||||
optionsArray[options] = "Bluetooth Toggle";
|
||||
#endif
|
||||
optionsEnumArray[options++] = Bluetooth;
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Power";
|
||||
#else
|
||||
optionsArray[options] = "Reboot/Shutdown";
|
||||
#endif
|
||||
optionsEnumArray[options++] = PowerMenu;
|
||||
|
||||
if (test_enabled) {
|
||||
@@ -515,7 +486,11 @@ void menuHandler::systemBaseMenu()
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "System";
|
||||
#else
|
||||
bannerOptions.message = "System Action";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
@@ -529,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();
|
||||
@@ -551,7 +523,11 @@ void menuHandler::systemBaseMenu()
|
||||
void menuHandler::favoriteBaseMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Preset, Freetext, Remove, TraceRoute, enumEnd };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset"};
|
||||
#else
|
||||
static const char *optionsArray[enumEnd] = {"Back", "New Preset Msg"};
|
||||
#endif
|
||||
static int optionsEnumArray[enumEnd] = {Back, Preset};
|
||||
int options = 2;
|
||||
|
||||
@@ -559,13 +535,19 @@ void menuHandler::favoriteBaseMenu()
|
||||
optionsArray[options] = "New Freetext Msg";
|
||||
optionsEnumArray[options++] = Freetext;
|
||||
}
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Trace Route";
|
||||
optionsEnumArray[options++] = TraceRoute;
|
||||
#endif
|
||||
optionsArray[options] = "Remove Favorite";
|
||||
optionsEnumArray[options++] = Remove;
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Favorites";
|
||||
#else
|
||||
bannerOptions.message = "Favorites Action";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
@@ -594,11 +576,12 @@ void menuHandler::positionBaseMenu()
|
||||
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;
|
||||
@@ -611,8 +594,10 @@ void menuHandler::positionBaseMenu()
|
||||
} 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);
|
||||
@@ -621,11 +606,19 @@ void menuHandler::positionBaseMenu()
|
||||
void menuHandler::nodeListMenu()
|
||||
{
|
||||
enum optionsNumbers { Back, Favorite, TraceRoute, Verify, Reset, enumEnd };
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Reset Node"};
|
||||
#else
|
||||
static const char *optionsArray[] = {"Back", "Add Favorite", "Trace Route", "Key Verification", "Reset NodeDB"};
|
||||
#endif
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Node Action";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.optionsCount = 3;
|
||||
#else
|
||||
bannerOptions.optionsCount = 5;
|
||||
#endif
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == Favorite) {
|
||||
menuQueue = add_favorite;
|
||||
@@ -732,7 +725,11 @@ void menuHandler::BluetoothToggleMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Bluetooth";
|
||||
#else
|
||||
bannerOptions.message = "Toggle Bluetooth";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -924,7 +921,11 @@ void menuHandler::rebootMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Reboot";
|
||||
#else
|
||||
bannerOptions.message = "Reboot Device?";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -944,7 +945,11 @@ void menuHandler::shutdownMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Shutdown";
|
||||
#else
|
||||
bannerOptions.message = "Shutdown Device?";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
@@ -961,7 +966,12 @@ void menuHandler::shutdownMenu()
|
||||
|
||||
void menuHandler::addFavoriteMenu()
|
||||
{
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
screen->showNodePicker("Node Favorite", 30000, [](uint32_t nodenum) -> void {
|
||||
#else
|
||||
screen->showNodePicker("Node To Favorite", 30000, [](uint32_t nodenum) -> void {
|
||||
|
||||
#endif
|
||||
LOG_WARN("Nodenum: %u", nodenum);
|
||||
nodeDB->set_favorite(true, nodenum);
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
@@ -1004,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);
|
||||
@@ -1174,7 +1167,11 @@ void menuHandler::powerMenu()
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
bannerOptions.message = "Power";
|
||||
#else
|
||||
bannerOptions.message = "Reboot / Shutdown";
|
||||
#endif
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||
@@ -1227,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)
|
||||
@@ -1344,15 +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 no_timeout_lora_picker:
|
||||
LoraRegionPicker(0);
|
||||
break;
|
||||
@@ -1439,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;
|
||||
|
||||
@@ -9,9 +9,7 @@ class menuHandler
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
@@ -41,14 +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 handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
@@ -81,7 +76,7 @@ class menuHandler
|
||||
static void notificationsMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
|
||||
@@ -181,12 +181,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
const int fixedTopHeight = 24;
|
||||
const int windowX = 0;
|
||||
const int windowY = fixedTopHeight;
|
||||
const int windowWidth = 64;
|
||||
const int windowHeight = SCREEN_HEIGHT - fixedTopHeight;
|
||||
#else
|
||||
const int navHeight = FONT_HEIGHT_SMALL;
|
||||
const int scrollBottom = SCREEN_HEIGHT - navHeight;
|
||||
const int usableHeight = scrollBottom;
|
||||
const int textWidth = SCREEN_WIDTH;
|
||||
|
||||
#endif
|
||||
bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
bool isBold = config.display.heading_bold;
|
||||
|
||||
@@ -201,7 +208,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
const char *messageString = "No messages";
|
||||
int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->drawString(center_text, windowY + (windowHeight / 2) - (FONT_HEIGHT_SMALL / 2) - 5, messageString);
|
||||
#else
|
||||
display->drawString(center_text, getTextPositions(display)[2], messageString);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,6 +220,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||
char headerStr[80];
|
||||
const char *sender = "???";
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (node && node->has_user)
|
||||
sender = node->user.short_name;
|
||||
#else
|
||||
if (node && node->has_user) {
|
||||
if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
|
||||
sender = node->user.long_name;
|
||||
@@ -216,6 +231,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
sender = node->user.short_name;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||
uint8_t timestampHours, timestampMinutes;
|
||||
int32_t daysAgo;
|
||||
@@ -235,10 +251,61 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
sender);
|
||||
}
|
||||
} else {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(headerStr, sizeof(headerStr), "%s from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
|
||||
sender);
|
||||
#else
|
||||
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
|
||||
sender);
|
||||
#endif
|
||||
}
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
int headerY = getTextPositions(display)[1];
|
||||
display->drawString(x, headerY, headerStr);
|
||||
for (int separatorX = 0; separatorX < SCREEN_WIDTH; separatorX += 2) {
|
||||
display->setPixel(separatorX, fixedTopHeight - 1);
|
||||
}
|
||||
cachedLines.clear();
|
||||
std::string fullMsg(messageBuf);
|
||||
std::string currentLine;
|
||||
for (size_t i = 0; i < fullMsg.size();) {
|
||||
unsigned char c = fullMsg[i];
|
||||
size_t charLen = 1;
|
||||
if ((c & 0xE0) == 0xC0)
|
||||
charLen = 2;
|
||||
else if ((c & 0xF0) == 0xE0)
|
||||
charLen = 3;
|
||||
else if ((c & 0xF8) == 0xF0)
|
||||
charLen = 4;
|
||||
std::string nextChar = fullMsg.substr(i, charLen);
|
||||
std::string testLine = currentLine + nextChar;
|
||||
if (display->getStringWidth(testLine.c_str()) > windowWidth) {
|
||||
cachedLines.push_back(currentLine);
|
||||
currentLine = nextChar;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
|
||||
i += charLen;
|
||||
}
|
||||
if (!currentLine.empty())
|
||||
cachedLines.push_back(currentLine);
|
||||
cachedHeights = calculateLineHeights(cachedLines, emotes);
|
||||
int yOffset = windowY;
|
||||
int linesDrawn = 0;
|
||||
for (size_t i = 0; i < cachedLines.size(); ++i) {
|
||||
if (linesDrawn >= 2)
|
||||
break;
|
||||
int lineHeight = cachedHeights[i];
|
||||
if (yOffset + lineHeight > windowY + windowHeight)
|
||||
break;
|
||||
drawStringWithEmotes(display, windowX, yOffset, cachedLines[i], emotes, numEmotes);
|
||||
yOffset += lineHeight;
|
||||
linesDrawn++;
|
||||
}
|
||||
screen->forceDisplay();
|
||||
#else
|
||||
uint32_t now = millis();
|
||||
#ifndef EXCLUDE_EMOJI
|
||||
// === Bounce animation setup ===
|
||||
@@ -355,6 +422,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
// Draw header at the end to sort out overlapping elements
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
|
||||
|
||||
@@ -21,6 +21,10 @@ extern bool haveGlyphs(const char *str);
|
||||
// Global screen instance
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static uint32_t lastSwitchTime = 0;
|
||||
#else
|
||||
#endif
|
||||
namespace graphics
|
||||
{
|
||||
namespace NodeListRenderer
|
||||
@@ -393,9 +397,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int columnWidth = display->getWidth();
|
||||
#else
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
|
||||
#endif
|
||||
display->clear();
|
||||
|
||||
// Draw the battery/time header
|
||||
@@ -408,8 +414,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
|
||||
int visibleNodeRows = totalRowsAvailable;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int totalColumns = 1;
|
||||
#else
|
||||
int totalColumns = 2;
|
||||
|
||||
#endif
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
|
||||
startIndex++; // skip own node
|
||||
@@ -445,12 +454,14 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// Draw column separator
|
||||
if (shownCount > 0) {
|
||||
const int firstNodeY = y + 3;
|
||||
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||
}
|
||||
|
||||
#endif
|
||||
const int scrollStartY = y + 3;
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||
}
|
||||
@@ -468,6 +479,13 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
if (now - lastSwitchTime >= 3000) {
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT) {
|
||||
currentMode = MODE_LAST_HEARD;
|
||||
@@ -522,6 +540,14 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
double lat = DegD(ourNode->position.latitude_i);
|
||||
double lon = DegD(ourNode->position.longitude_i);
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
uint32_t now = millis();
|
||||
if (now - lastSwitchTime >= 2000) {
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
#if HAS_GPS
|
||||
if (screen->hasHeading()) {
|
||||
|
||||
@@ -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;
|
||||
@@ -49,11 +36,8 @@ const int *NotificationRenderer::optionsEnumPtr = nullptr;
|
||||
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||
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)
|
||||
{
|
||||
@@ -86,13 +70,9 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
||||
|
||||
void NotificationRenderer::resetBanner()
|
||||
{
|
||||
notificationTypeEnum previousType = current_notification_type;
|
||||
|
||||
alertBannerMessage[0] = '\0';
|
||||
current_notification_type = notificationTypeEnum::none;
|
||||
|
||||
OnScreenKeyboardModule::instance().clearPopup();
|
||||
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||
inEvent.kbchar = 0;
|
||||
curSelected = 0;
|
||||
@@ -105,44 +85,18 @@ void NotificationRenderer::resetBanner()
|
||||
currentNumber = 0;
|
||||
|
||||
nodeDB->pause_sort(false);
|
||||
|
||||
// If we're exiting from text_input (virtual keyboard), stop module and trigger frame update
|
||||
// to ensure any messages received during keyboard use are now displayed
|
||||
if (previousType == notificationTypeEnum::text_input && screen) {
|
||||
OnScreenKeyboardModule::instance().stop(false);
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
drawTextInput(display, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
|
||||
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0')
|
||||
resetBanner();
|
||||
}
|
||||
|
||||
// Exit if no banner is showing or banner is paused
|
||||
if (!isOverlayBannerShowing() || pauseBanner) {
|
||||
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);
|
||||
@@ -313,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) {
|
||||
@@ -329,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++];
|
||||
}
|
||||
@@ -503,6 +459,135 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
// count lines
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
if (!isHighResolution && boxWidth <= 100)
|
||||
boxWidth += 20;
|
||||
}
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1)
|
||||
boxHeight += (isHighResolution ? 4 : 3);
|
||||
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines)
|
||||
boxWidth += (isHighResolution ? 4 : 2);
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
if (visibleTotalLines == 1) {
|
||||
boxTop += 25;
|
||||
}
|
||||
if (alertBannerOptions < 3) {
|
||||
int missingLines = 3 - alertBannerOptions;
|
||||
int moveUp = missingLines * (effectiveLineHeight / 2);
|
||||
boxTop -= moveUp;
|
||||
if (boxTop < 0)
|
||||
boxTop = 0;
|
||||
}
|
||||
|
||||
// === Draw Box ===
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
int swingRange = 8;
|
||||
static int swingOffset = 0;
|
||||
static bool swingRight = true;
|
||||
static unsigned long lastSwingTime = 0;
|
||||
unsigned long now = millis();
|
||||
int swingSpeedMs = 10 / (swingRange * 2);
|
||||
if (now - lastSwingTime >= (unsigned long)swingSpeedMs) {
|
||||
lastSwingTime = now;
|
||||
if (swingRight) {
|
||||
swingOffset++;
|
||||
if (swingOffset >= swingRange)
|
||||
swingRight = false;
|
||||
} else {
|
||||
swingOffset--;
|
||||
if (swingOffset <= 0)
|
||||
swingRight = true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
bool isTitle = (i == 0);
|
||||
int globalOptionIndex = (i - 1) + firstOptionToShow;
|
||||
bool isSelectedOption = (!isTitle && globalOptionIndex >= 0 && globalOptionIndex == curSelected);
|
||||
|
||||
uint16_t visibleWidth = 64 - hPadding * 2;
|
||||
if (totalLines > visibleTotalLines)
|
||||
visibleWidth -= 6;
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
|
||||
if (isTitle) {
|
||||
if (visibleTotalLines == 1) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, boxTop, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, boxTop, lineBuffer);
|
||||
display->setColor(WHITE);
|
||||
if (needs_bell) {
|
||||
int bellY = boxTop + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||
display->drawXbm(boxLeft + (boxWidth - lineWidths[i]) / 2 - 10, bellY, 8, 8, bell_alert);
|
||||
display->drawXbm(boxLeft + (boxWidth + lineWidths[i]) / 2 + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
}
|
||||
lineY = boxTop + effectiveLineHeight + 1;
|
||||
} else if (isSelectedOption) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(BLACK);
|
||||
if (lineLengths[i] > 15 && lineWidths[i] > visibleWidth) {
|
||||
int textX = boxLeft + hPadding + swingOffset;
|
||||
display->drawString(textX, lineY - 1, lineBuffer);
|
||||
} else {
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY - 1, lineBuffer);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
lineY += effectiveLineHeight;
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, lineY, boxWidth, effectiveLineHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(boxLeft + (boxWidth - lineWidths[i]) / 2, lineY, lineBuffer);
|
||||
lineY += effectiveLineHeight;
|
||||
}
|
||||
}
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#else
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
@@ -591,6 +676,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Draw the last text message we received
|
||||
@@ -619,182 +705,10 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
|
||||
"Please be patient and do not power off.");
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
// Delegate session to OnScreenKeyboardModule
|
||||
auto &osk = OnScreenKeyboardModule::instance();
|
||||
|
||||
if (!osk.isActive()) {
|
||||
LOG_INFO("Virtual keyboard is not active - resetting banner");
|
||||
resetBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
|
||||
osk.handleInput(inEvent);
|
||||
inEvent.inputEvent = INPUT_BROKER_NONE; // consume
|
||||
}
|
||||
|
||||
// Draw. If draw returns false, session ended (timeout or cancel)
|
||||
if (!osk.draw(display)) {
|
||||
// Session ended, ensure banner reset and restore frames
|
||||
resetBanner();
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
}
|
||||
|
||||
void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs)
|
||||
{
|
||||
if (!title || !content || current_notification_type != notificationTypeEnum::text_input)
|
||||
return;
|
||||
OnScreenKeyboardModule::instance().showPopup(title, content, durationMs);
|
||||
}
|
||||
|
||||
// drawKeyboardMessagePopup removed; OnScreenKeyboardModule handles popup drawing within draw()
|
||||
|
||||
// Custom inverted color version for keyboard popup - black background with white text
|
||||
void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
|
||||
{
|
||||
bool is_picker = false;
|
||||
uint16_t lineCount = 0;
|
||||
// === Layout Configuration ===
|
||||
constexpr uint16_t hPadding = 5;
|
||||
constexpr uint16_t vPadding = 2;
|
||||
bool needs_bell = false;
|
||||
uint16_t lineWidths[totalLines] = {0};
|
||||
uint16_t lineLengths[totalLines] = {0};
|
||||
|
||||
if (maxWidth != 0)
|
||||
is_picker = true;
|
||||
|
||||
// Setup font and alignment
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
while (lines[lineCount] != nullptr) {
|
||||
auto newlinePointer = strchr(lines[lineCount], '\n');
|
||||
if (newlinePointer)
|
||||
lineLengths[lineCount] = (newlinePointer - lines[lineCount]);
|
||||
else
|
||||
lineLengths[lineCount] = strlen(lines[lineCount]);
|
||||
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], lineLengths[lineCount], true);
|
||||
if (!is_picker) {
|
||||
if (lineWidths[lineCount] > maxWidth)
|
||||
maxWidth = lineWidths[lineCount];
|
||||
}
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||
if (needs_bell) {
|
||||
if (isHighResolution && boxWidth <= 150)
|
||||
boxWidth += 26;
|
||||
if (!isHighResolution && boxWidth <= 100)
|
||||
boxWidth += 20;
|
||||
}
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
if (visibleTotalLines == 1) {
|
||||
boxHeight += (isHighResolution) ? 4 : 3;
|
||||
}
|
||||
|
||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||
if (totalLines > visibleTotalLines) {
|
||||
boxWidth += (isHighResolution) ? 4 : 2;
|
||||
}
|
||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||
|
||||
// === Draw Box with INVERTED COLORS ===
|
||||
// Add outer separation pixels (1-pixel white background around the box)
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
|
||||
// Make outer corners round by filling back with背景色 (BLACK for separation)
|
||||
display->setColor(BLACK);
|
||||
// Top-left outer corner
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, 1, 1);
|
||||
// Top-right outer corner
|
||||
display->fillRect(boxLeft + boxWidth, boxTop - 1, 1, 1);
|
||||
// Bottom-left outer corner
|
||||
display->fillRect(boxLeft - 1, boxTop + boxHeight, 1, 1);
|
||||
// Bottom-right outer corner
|
||||
display->fillRect(boxLeft + boxWidth, boxTop + boxHeight, 1, 1);
|
||||
|
||||
// Draw single pixel black border
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
|
||||
// Make inner corners round by filling white pixels at corners
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
|
||||
// Fill interior with BLACK (inverted)
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, boxHeight - 2);
|
||||
|
||||
// === Draw Content with WHITE text on BLACK background ===
|
||||
display->setColor(WHITE);
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
|
||||
char lineBuffer[lineLengths[i] + 1];
|
||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
|
||||
// For keyboard popup, treat first line as title if it's different from others
|
||||
if (i == 0 && lineCount > 1) {
|
||||
// Title line - use inverted colors (white background, black text)
|
||||
display->setColor(WHITE);
|
||||
int background_yOffset = 1;
|
||||
// Check for low hanging characters
|
||||
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
||||
background_yOffset = -1;
|
||||
}
|
||||
display->fillRect(boxLeft + 1, boxTop + 1, boxWidth - 2, effectiveLineHeight - background_yOffset);
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
display->drawString(textX, lineY - yOffset, lineBuffer);
|
||||
display->setColor(WHITE); // Reset to white for next lines
|
||||
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||
} else {
|
||||
// Content lines - white text on black background
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
lineY += effectiveLineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// === Scroll Bar (if needed) ===
|
||||
if (totalLines > visibleTotalLines) {
|
||||
const uint8_t scrollBarWidth = 5;
|
||||
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight;
|
||||
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||
|
||||
float ratio = (float)visibleTotalLines / totalLines;
|
||||
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
@@ -3,10 +3,6 @@
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
@@ -26,22 +22,16 @@ 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;
|
||||
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
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);
|
||||
static void drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
|
||||
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
static uint32_t lastSwitchTime = 0;
|
||||
namespace graphics
|
||||
{
|
||||
NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
||||
@@ -218,7 +218,6 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// **********************
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
|
||||
@@ -230,8 +229,15 @@ 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)
|
||||
if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒
|
||||
{
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
currentFavoriteNodeNum = node->num;
|
||||
// === Create the shortName and title string ===
|
||||
const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node";
|
||||
@@ -250,9 +256,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
// List of available macro Y positions in order, from top to bottom.
|
||||
int line = 1; // which slot to use next
|
||||
std::string usernameStr;
|
||||
|
||||
// === 1. Long Name (always try to show first) ===
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.short_name : nullptr;
|
||||
#else
|
||||
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
|
||||
#endif
|
||||
|
||||
if (username) {
|
||||
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
|
||||
// Print node's long name (e.g. "Backpack Node")
|
||||
@@ -307,7 +317,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
if (seenStr[0] && line < 5) {
|
||||
display->drawString(x, getTextPositions(display)[line++], seenStr);
|
||||
}
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === 4. Uptime (only show if metric is present) ===
|
||||
char uptimeStr[32] = "";
|
||||
if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) {
|
||||
@@ -479,6 +489,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
||||
}
|
||||
// else show nothing
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ****************************
|
||||
@@ -492,7 +503,11 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
int line = 1;
|
||||
|
||||
// === Header ===
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
graphics::drawCommonHeader(display, x, y, "Home");
|
||||
#else
|
||||
graphics::drawCommonHeader(display, x, y, "");
|
||||
#endif
|
||||
|
||||
// === Content below header ===
|
||||
|
||||
@@ -507,20 +522,25 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
config.display.heading_bold = false;
|
||||
|
||||
// Display Region and Channel Utilization
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
#else
|
||||
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
|
||||
#endif
|
||||
char uptimeStr[32] = "";
|
||||
uint32_t uptime = millis() / 1000;
|
||||
uint32_t days = uptime / 86400;
|
||||
uint32_t hours = (uptime % 86400) / 3600;
|
||||
uint32_t mins = (uptime % 3600) / 60;
|
||||
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
if (days)
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours);
|
||||
else if (hours)
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins);
|
||||
else
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins);
|
||||
#endif
|
||||
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr);
|
||||
|
||||
// === Second Row: Satellites and Voltage ===
|
||||
@@ -549,6 +569,21 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
line += 1;
|
||||
|
||||
// === Node Identity ===
|
||||
int textWidth = 0;
|
||||
int nameX = 0;
|
||||
char shortnameble[35];
|
||||
snprintf(shortnameble, sizeof(shortnameble), "%s",
|
||||
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
|
||||
|
||||
// === ShortName Centered ===
|
||||
textWidth = display->getStringWidth(shortnameble);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
#else
|
||||
if (powerStatus->getHasBattery()) {
|
||||
char batStr[20];
|
||||
int batV = powerStatus->getBatteryVoltageMv() / 1000;
|
||||
@@ -674,6 +709,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Start Functions to write date/time to the screen
|
||||
@@ -832,6 +868,28 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
||||
// needs to be drawn relative to x and y
|
||||
|
||||
// draw centered icon left to right and centered above the one line of app text
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
// Draw region in upper left
|
||||
if (upperMsg) {
|
||||
int msgWidth = display->getStringWidth(upperMsg);
|
||||
int msgX = x + (SCREEN_WIDTH - msgWidth) / 2;
|
||||
int msgY = y;
|
||||
display->drawString(msgX, msgY, upperMsg);
|
||||
}
|
||||
// Draw version and short name in bottom middle
|
||||
char buf[25];
|
||||
snprintf(buf, sizeof(buf), "%s %s", xstr(APP_VERSION_SHORT),
|
||||
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
|
||||
|
||||
display->drawString(x + getStringCenteredX(buf), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, buf);
|
||||
screen->forceDisplay();
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
|
||||
#else
|
||||
display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2,
|
||||
icon_width, icon_height, icon_bits);
|
||||
|
||||
@@ -840,7 +898,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
||||
const char *title = "meshtastic.org";
|
||||
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Draw region in upper left
|
||||
if (upperMsg)
|
||||
display->drawString(x + 0, y + 0, upperMsg);
|
||||
@@ -855,6 +912,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
||||
screen->forceDisplay();
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code
|
||||
#endif
|
||||
}
|
||||
|
||||
// ****************************
|
||||
@@ -879,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 {
|
||||
@@ -915,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);
|
||||
}
|
||||
|
||||
@@ -942,65 +980,48 @@ 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];
|
||||
snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7);
|
||||
#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
|
||||
|
||||
// === Fourth Row: Longitude ===
|
||||
char lonStr[32];
|
||||
snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7);
|
||||
#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);
|
||||
|
||||
// === Fifth Row: Altitude ===
|
||||
char DisplayLineTwo[32] = {0};
|
||||
int32_t alt = (strcmp(displayLine, "Phone GPS") == 0 && ourNode && nodeDB->hasValidPosition(ourNode))
|
||||
? ourNode->position.altitude
|
||||
: geoCoord.getAltitude();
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET);
|
||||
} else {
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude());
|
||||
snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude());
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Draw Compass if heading is valid ===
|
||||
if (validHeading) {
|
||||
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
|
||||
@@ -1083,6 +1104,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
@@ -1239,7 +1261,6 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Knock the corners off the square
|
||||
display->setColor(BLACK);
|
||||
display->drawRect(rectX, y - 2, 1, 1);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "NodeDB.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include <OLEDDisplay.h>
|
||||
|
||||
@@ -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,78 +287,9 @@ 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
|
||||
static unsigned char 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
|
||||
static unsigned char 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
|
||||
static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f};
|
||||
|
||||
#ifdef M5STACK_UNITC6L
|
||||
#include "img/icon_small.xbm"
|
||||
#else
|
||||
#include "img/icon.xbm"
|
||||
#endif
|
||||
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
|
||||
30
src/graphics/img/icon_small.xbm
Normal file
30
src/graphics/img/icon_small.xbm
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef USERPREFS_HAS_SPLASH
|
||||
#define icon_width 50
|
||||
#define icon_height 20
|
||||
static uint8_t icon_bits[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x80, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x80,
|
||||
0x07, 0xc0, 0x07, 0x00, 0x00, 0x00, 0xc0, 0x0f,
|
||||
0xc0, 0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xe0,
|
||||
0x0f, 0x00, 0x00, 0x00, 0xe0, 0x07, 0xf0, 0x1f,
|
||||
0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x3f, 0x00,
|
||||
0x00, 0x00, 0xf8, 0x03, 0xf8, 0x7f, 0x00, 0x00,
|
||||
0x00, 0xf8, 0x01, 0xfc, 0x7e, 0x00, 0x00, 0x00,
|
||||
0xfc, 0x00, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0xfe,
|
||||
0x00, 0x7e, 0xf8, 0x00, 0x00, 0x00, 0x7e, 0x00,
|
||||
0x3f, 0xf8, 0x01, 0x00, 0x00, 0x3f, 0x00, 0x1f,
|
||||
0xf0, 0x01, 0x00, 0x00, 0x1f, 0x80, 0x1f, 0xe0,
|
||||
0x03, 0x00, 0x80, 0x1f, 0xc0, 0x0f, 0xe0, 0x03,
|
||||
0x00, 0x80, 0x0f, 0xc0, 0x07, 0xc0, 0x07, 0x00,
|
||||
0xc0, 0x0f, 0xe0, 0x07, 0x80, 0x0f, 0x00, 0xe0,
|
||||
0x07, 0xf0, 0x03, 0x80, 0x1f, 0x00, 0xe0, 0x03,
|
||||
0xf8, 0x03, 0x00, 0x1f, 0x00, 0xf0, 0x03, 0xf8,
|
||||
0x01, 0x00, 0x3f, 0x00, 0xf8, 0x01, 0xfc, 0x00,
|
||||
0x00, 0x7e, 0x00, 0xfc, 0x00, 0xfe, 0x00, 0x00,
|
||||
0x7e, 0x00, 0xfc, 0x00, 0x7e, 0x00, 0x00, 0xfc,
|
||||
0x00, 0x7e, 0x00, 0x3f, 0x00, 0x00, 0xf8, 0x00,
|
||||
0x7e, 0x00, 0x3e, 0x00, 0x00, 0xf8, 0x00, 0x38,
|
||||
0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00 };
|
||||
#endif
|
||||
@@ -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) -
|
||||
1),
|
||||
.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) -
|
||||
1),
|
||||
.pin_int = (int16_t)portduino_config.touchscreenIRQ.pin,
|
||||
.offset_rotation = (uint8_t)portduino_config.touchscreenRotate,
|
||||
.i2c{.i2c_addr = (uint8_t)portduino_config.touchscreenI2CAddr}});
|
||||
.y_max =
|
||||
(int16_t)((settingsMap[touchscreenRotate] & 1 ? settingsMap[displayHeight] : settingsMap[displayWidth]) -
|
||||
1),
|
||||
.pin_int = (int16_t)settingsMap[touchscreenIRQ],
|
||||
.offset_rotation = (uint8_t)settingsMap[touchscreenRotate],
|
||||
.i2c{.i2c_addr = (uint8_t)settingsMap[touchscreenI2CAddr]}});
|
||||
}
|
||||
}
|
||||
deviceScreen = &DeviceScreen::create(&displayConfig);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -48,37 +46,10 @@ int32_t RotaryEncoderInterruptBase::runOnce()
|
||||
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");
|
||||
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;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("Rotary event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
} 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;
|
||||
}
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,9 +40,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -53,47 +50,10 @@ int32_t TrackballInterruptBase::runOnce()
|
||||
{
|
||||
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);
|
||||
}
|
||||
lastEvent = action;
|
||||
this->action = TB_ACTION_NONE;
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,20 +22,20 @@ 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()
|
||||
@@ -51,88 +43,23 @@ int32_t UpDownInterruptBase::runOnce()
|
||||
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) {
|
||||
lastPressKeyTime = now;
|
||||
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;
|
||||
if (this->action == UPDOWN_ACTION_PRESSED) {
|
||||
if (now - lastPressKeyTime >= pressDebounceMs) {
|
||||
lastPressKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Press");
|
||||
e.inputEvent = this->_eventPressed;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
lastUpKeyTime = now;
|
||||
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;
|
||||
}
|
||||
} else if (this->action == UPDOWN_ACTION_UP) {
|
||||
if (now - lastUpKeyTime >= updownDebounceMs) {
|
||||
lastUpKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Up");
|
||||
e.inputEvent = this->_eventUp;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
lastDownKeyTime = now;
|
||||
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;
|
||||
}
|
||||
} else if (this->action == UPDOWN_ACTION_DOWN) {
|
||||
if (now - lastDownKeyTime >= updownDebounceMs) {
|
||||
lastDownKeyTime = now;
|
||||
LOG_DEBUG("GPIO event Down");
|
||||
e.inputEvent = this->_eventDown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
this->action = UPDOWN_ACTION_NONE;
|
||||
return 100;
|
||||
}
|
||||
|
||||
void UpDownInterruptBase::intPressHandler()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
95
src/input/i2cButton.cpp
Normal file
95
src/input/i2cButton.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "i2cButton.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
|
||||
#include "MeshService.h"
|
||||
#include "RadioLibInterface.h"
|
||||
#include "buzz.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "main.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#endif
|
||||
|
||||
i2cButtonThread *i2cButton;
|
||||
|
||||
using namespace concurrency;
|
||||
|
||||
extern void i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value);
|
||||
|
||||
extern void i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value);
|
||||
|
||||
#define PI4IO_M_ADDR 0x43
|
||||
#define getbit(x, y) ((x) >> (y)&0x01)
|
||||
#define PI4IO_REG_IRQ_STA 0x13
|
||||
#define PI4IO_REG_IN_STA 0x0F
|
||||
#define PI4IO_REG_CHIP_RESET 0x01
|
||||
|
||||
i2cButtonThread::i2cButtonThread(const char *name) : OSThread(name)
|
||||
{
|
||||
_originName = name;
|
||||
if (inputBroker)
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
int32_t i2cButtonThread::runOnce()
|
||||
{
|
||||
static bool btn1_pressed = false;
|
||||
static uint32_t press_start_time = 0;
|
||||
const uint32_t LONG_PRESS_TIME = 1000;
|
||||
static bool long_press_triggered = false;
|
||||
|
||||
uint8_t in_data;
|
||||
i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, &in_data);
|
||||
i2c_write_byte(PI4IO_M_ADDR, PI4IO_REG_IRQ_STA, in_data);
|
||||
if (getbit(in_data, 0)) {
|
||||
uint8_t input_state;
|
||||
i2c_read_byte(PI4IO_M_ADDR, PI4IO_REG_IN_STA, &input_state);
|
||||
|
||||
if (!getbit(input_state, 0)) {
|
||||
if (!btn1_pressed) {
|
||||
btn1_pressed = true;
|
||||
press_start_time = millis();
|
||||
long_press_triggered = false;
|
||||
}
|
||||
} else {
|
||||
if (btn1_pressed) {
|
||||
btn1_pressed = false;
|
||||
uint32_t press_duration = millis() - press_start_time;
|
||||
if (long_press_triggered) {
|
||||
long_press_triggered = false;
|
||||
return 50;
|
||||
}
|
||||
|
||||
if (press_duration < LONG_PRESS_TIME) {
|
||||
InputEvent evt;
|
||||
evt.source = "UserButton";
|
||||
evt.inputEvent = INPUT_BROKER_USER_PRESS;
|
||||
evt.kbchar = 0;
|
||||
evt.touchX = 0;
|
||||
evt.touchY = 0;
|
||||
this->notifyObservers(&evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (btn1_pressed && !long_press_triggered && (millis() - press_start_time >= LONG_PRESS_TIME)) {
|
||||
long_press_triggered = true;
|
||||
InputEvent evt;
|
||||
evt.source = "UserButton";
|
||||
evt.inputEvent = INPUT_BROKER_SELECT;
|
||||
evt.kbchar = 0;
|
||||
evt.touchX = 0;
|
||||
evt.touchY = 0;
|
||||
this->notifyObservers(&evt);
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
#endif
|
||||
18
src/input/i2cButton.h
Normal file
18
src/input/i2cButton.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "InputBroker.h"
|
||||
#include "OneButton.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
|
||||
class i2cButtonThread : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
const char *_originName;
|
||||
explicit i2cButtonThread(const char *name);
|
||||
int32_t runOnce() override;
|
||||
};
|
||||
|
||||
extern i2cButtonThread *i2cButton;
|
||||
#endif
|
||||
127
src/main.cpp
127
src/main.cpp
@@ -192,8 +192,6 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE;
|
||||
uint8_t kb_model;
|
||||
// global bool to record that a kb is present
|
||||
bool kb_found = false;
|
||||
// global bool to record that on-screen keyboard (OSK) is present
|
||||
bool osk_found = false;
|
||||
|
||||
// The I2C address of the RTC Module (if found)
|
||||
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
|
||||
@@ -387,10 +385,9 @@ void setup()
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
#if ARCH_PORTDUINO
|
||||
SPISettings spiSettings(portduino_config.spiSpeed, MSBFIRST, SPI_MODE0);
|
||||
SPISettings spiSettings(settingsMap[spiSpeed], MSBFIRST, SPI_MODE0);
|
||||
#else
|
||||
SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
#endif
|
||||
@@ -535,9 +532,9 @@ void setup()
|
||||
#elif defined(I2C_SDA) && !defined(ARCH_RP2040)
|
||||
Wire.begin(I2C_SDA, I2C_SCL);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if (portduino_config.i2cdev != "") {
|
||||
LOG_INFO("Use %s as I2C device", portduino_config.i2cdev.c_str());
|
||||
Wire.begin(portduino_config.i2cdev.c_str());
|
||||
if (settingsStrings[i2cdev] != "") {
|
||||
LOG_INFO("Use %s as I2C device", settingsStrings[i2cdev].c_str());
|
||||
Wire.begin(settingsStrings[i2cdev].c_str());
|
||||
} else {
|
||||
LOG_INFO("No I2C device configured, Skip");
|
||||
}
|
||||
@@ -546,6 +543,12 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, 1);
|
||||
c6l_init();
|
||||
#endif
|
||||
|
||||
#ifdef PIN_LCD_RESET
|
||||
// FIXME - move this someplace better, LCD is at address 0x3F
|
||||
pinMode(PIN_LCD_RESET, OUTPUT);
|
||||
@@ -583,7 +586,7 @@ void setup()
|
||||
#if defined(I2C_SDA)
|
||||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if (portduino_config.i2cdev != "") {
|
||||
if (settingsStrings[i2cdev] != "") {
|
||||
LOG_INFO("Scan for i2c devices");
|
||||
i2cScanner->scanPort(ScanI2C::I2CPort::WIRE);
|
||||
}
|
||||
@@ -856,7 +859,7 @@ void setup()
|
||||
SPI.begin(false);
|
||||
#endif // HW_SPI1_DEVICE
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.lora_spi_dev != "ch341") {
|
||||
if (settingsStrings[spidev] != "ch341") {
|
||||
SPI.begin();
|
||||
}
|
||||
#elif !defined(ARCH_ESP32) // ARCH_RP2040
|
||||
@@ -879,10 +882,11 @@ void setup()
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_SPISSD1306)
|
||||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
|
||||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
|
||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
|
||||
}
|
||||
@@ -983,13 +987,13 @@ void setup()
|
||||
#endif
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
if (portduino_config.userButtonPin.enabled) {
|
||||
if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) {
|
||||
|
||||
LOG_DEBUG("Use GPIO%02d for button", portduino_config.userButtonPin.pin);
|
||||
LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]);
|
||||
UserButtonThread = new ButtonThread("UserButton");
|
||||
if (screen) {
|
||||
ButtonConfig config;
|
||||
config.pinNumber = (uint8_t)portduino_config.userButtonPin.pin;
|
||||
config.pinNumber = (uint8_t)settingsMap[userButtonPin];
|
||||
config.activeLow = true;
|
||||
config.activePullup = true;
|
||||
config.pullupSense = INPUT_PULLUP;
|
||||
@@ -1142,11 +1146,12 @@ void setup()
|
||||
// Don't call screen setup until after nodedb is setup (because we need
|
||||
// the current region name)
|
||||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_SPISSD1306)
|
||||
if (screen)
|
||||
screen->setup();
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || portduino_config.displayPanel) &&
|
||||
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
|
||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
screen->setup();
|
||||
}
|
||||
@@ -1162,10 +1167,15 @@ void setup()
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
const struct {
|
||||
configNames cfgName;
|
||||
std::string strName;
|
||||
} loraModules[] = {{use_rf95, "RF95"}, {use_sx1262, "sx1262"}, {use_sx1268, "sx1268"}, {use_sx1280, "sx1280"},
|
||||
{use_lr1110, "lr1110"}, {use_lr1120, "lr1120"}, {use_lr1121, "lr1121"}, {use_llcc68, "LLCC68"}};
|
||||
// as one can't use a function pointer to the class constructor:
|
||||
auto loraModuleInterface = [](LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy) {
|
||||
switch (portduino_config.lora_module) {
|
||||
auto loraModuleInterface = [](configNames cfgName, LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq,
|
||||
RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) {
|
||||
switch (cfgName) {
|
||||
case use_rf95:
|
||||
return (RadioInterface *)new RF95Interface(hal, cs, irq, rst, busy);
|
||||
case use_sx1262:
|
||||
@@ -1182,34 +1192,31 @@ void setup()
|
||||
return (RadioInterface *)new LR1121Interface(hal, cs, irq, rst, busy);
|
||||
case use_llcc68:
|
||||
return (RadioInterface *)new LLCC68Interface(hal, cs, irq, rst, busy);
|
||||
case use_simradio:
|
||||
return (RadioInterface *)new SimRadio;
|
||||
default:
|
||||
assert(0); // shouldn't happen
|
||||
return (RadioInterface *)nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
LOG_DEBUG("Activate %s radio on SPI port %s", portduino_config.loraModules[portduino_config.lora_module].c_str(),
|
||||
portduino_config.lora_spi_dev.c_str());
|
||||
if (portduino_config.lora_spi_dev == "ch341") {
|
||||
RadioLibHAL = ch341Hal;
|
||||
} else {
|
||||
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||
for (auto &loraModule : loraModules) {
|
||||
if (settingsMap[loraModule.cfgName] && !rIf) {
|
||||
LOG_DEBUG("Activate %s radio on SPI port %s", loraModule.strName.c_str(), settingsStrings[spidev].c_str());
|
||||
if (settingsStrings[spidev] == "ch341") {
|
||||
RadioLibHAL = ch341Hal;
|
||||
} else {
|
||||
RadioLibHAL = new LockingArduinoHal(SPI, spiSettings);
|
||||
}
|
||||
rIf = loraModuleInterface(loraModule.cfgName, (LockingArduinoHal *)RadioLibHAL, settingsMap[cs_pin],
|
||||
settingsMap[irq_pin], settingsMap[reset_pin], settingsMap[busy_pin]);
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No %s radio", loraModule.strName.c_str());
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
LOG_INFO("%s init success", loraModule.strName.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
rIf =
|
||||
loraModuleInterface((LockingArduinoHal *)RadioLibHAL, portduino_config.lora_cs_pin.pin, portduino_config.lora_irq_pin.pin,
|
||||
portduino_config.lora_reset_pin.pin, portduino_config.lora_busy_pin.pin);
|
||||
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No %s radio", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
LOG_INFO("%s init success", portduino_config.loraModules[portduino_config.lora_module].c_str());
|
||||
}
|
||||
|
||||
#elif defined(HW_SPI1_DEVICE)
|
||||
LockingArduinoHal *RadioLibHAL = new LockingArduinoHal(SPI1, spiSettings);
|
||||
#else // HW_SPI1_DEVICE
|
||||
@@ -1231,6 +1238,20 @@ void setup()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
if (!rIf) {
|
||||
rIf = new SimRadio;
|
||||
if (!rIf->init()) {
|
||||
LOG_WARN("No simulated radio");
|
||||
delete rIf;
|
||||
rIf = NULL;
|
||||
} else {
|
||||
LOG_INFO("Use SIMULATED radio!");
|
||||
radioType = SIM_RADIO;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(RF95_IRQ) && RADIOLIB_EXCLUDE_SX127X != 1
|
||||
if ((!rIf) && (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) {
|
||||
rIf = new RF95Interface(RadioLibHAL, LORA_CS, RF95_IRQ, RF95_RESET, RF95_DIO1);
|
||||
@@ -1434,10 +1455,6 @@ void setup()
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
|
||||
osk_found = true;
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
|
||||
// Start web server thread.
|
||||
webServerThread = new WebServerThread();
|
||||
@@ -1445,7 +1462,7 @@ void setup()
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#if __has_include(<ulfius.h>)
|
||||
if (portduino_config.webserverport != -1) {
|
||||
if (settingsMap[webserverport] != -1) {
|
||||
piwebServerThread = new PiWebServerThread();
|
||||
std::atexit([] { delete piwebServerThread; });
|
||||
}
|
||||
@@ -1506,9 +1523,6 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||||
deviceMetadata.hw_model = HW_VENDOR;
|
||||
deviceMetadata.hasRemoteHardware = moduleConfig.remote_hardware.enabled;
|
||||
deviceMetadata.excluded_modules = meshtastic_ExcludedModules_EXCLUDED_NONE;
|
||||
#if MESHTASTIC_EXCLUDE_MQTT
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_MQTT_CONFIG;
|
||||
#endif
|
||||
#if MESHTASTIC_EXCLUDE_REMOTEHARDWARE
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_REMOTEHARDWARE_CONFIG;
|
||||
#endif
|
||||
@@ -1531,21 +1545,10 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||||
#if NO_EXT_GPIO && NO_GPS || MESHTASTIC_EXCLUDE_SERIAL
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_SERIAL_CONFIG;
|
||||
#endif
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
// PAXCOUNTER is only supported on ESP32 due to memory constraints
|
||||
#else
|
||||
#ifndef ARCH_ESP32
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_PAXCOUNTER_CONFIG;
|
||||
#endif
|
||||
#if MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_STOREFORWARD_CONFIG;
|
||||
#endif
|
||||
#if MESHTASTIC_EXCLUDE_RANGETEST
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_RANGETEST_CONFIG;
|
||||
#endif
|
||||
#if MESHTASTIC_EXCLUDE_NEIGHBORINFO
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_NEIGHBORINFO_CONFIG;
|
||||
#endif
|
||||
#if (!defined(HAS_RGB_LED) && !defined(RAK_4631)) || defined(MESHTASTIC_EXCLUDE_AMBIENTLIGHTING)
|
||||
#if !defined(HAS_RGB_LED) && !RAK_4631
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AMBIENTLIGHTING_CONFIG;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ extern ScanI2C::DeviceAddress screen_found;
|
||||
extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern uint8_t kb_model;
|
||||
extern bool kb_found;
|
||||
extern bool osk_found;
|
||||
extern ScanI2C::DeviceAddress rtc_found;
|
||||
extern ScanI2C::DeviceAddress accelerometer_found;
|
||||
extern ScanI2C::FoundDevice rgb_found;
|
||||
|
||||
@@ -88,4 +88,16 @@ uint32_t MemGet::getPsramSize()
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void displayPercentHeapFree()
|
||||
{
|
||||
uint32_t freeHeap = memGet.getFreeHeap();
|
||||
uint32_t totalHeap = memGet.getHeapSize();
|
||||
if (totalHeap == 0 || totalHeap == UINT32_MAX) {
|
||||
LOG_INFO("Heap size unavailable");
|
||||
return;
|
||||
}
|
||||
int percent = (int)((freeHeap * 100) / totalHeap);
|
||||
LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
||||
// specified (may be dangerous if using external PA and LR11x0 power config forgotten)
|
||||
#if ARCH_PORTDUINO
|
||||
#define LR1110_MAX_POWER portduino_config.lr1110_max_power
|
||||
#define LR1110_MAX_POWER settingsMap[lr1110_max_power]
|
||||
#endif
|
||||
#ifndef LR1110_MAX_POWER
|
||||
#define LR1110_MAX_POWER 22
|
||||
@@ -30,7 +30,7 @@ static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
// the 2.4G part maxes at 13dBm
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#define LR1120_MAX_POWER portduino_config.lr1120_max_power
|
||||
#define LR1120_MAX_POWER settingsMap[lr1120_max_power]
|
||||
#endif
|
||||
#ifndef LR1120_MAX_POWER
|
||||
#define LR1120_MAX_POWER 13
|
||||
@@ -55,7 +55,7 @@ template <typename T> bool LR11x0Interface<T>::init()
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||
float tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000;
|
||||
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
|
||||
#elif !defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
||||
float tcxoVoltage =
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "PointerQueue.h"
|
||||
#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
|
||||
|
||||
template <class T> class Allocator
|
||||
{
|
||||
@@ -14,13 +15,14 @@ template <class T> class Allocator
|
||||
Allocator() : deleter([this](T *p) { this->release(p); }) {}
|
||||
virtual ~Allocator() {}
|
||||
|
||||
/// Return a queable object which has been prefilled with zeros. Panic if no buffer is available
|
||||
/// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available
|
||||
/// Note: this method is safe to call from regular OR ISR code
|
||||
T *allocZeroed()
|
||||
{
|
||||
T *p = allocZeroed(0);
|
||||
|
||||
assert(p); // FIXME panic instead
|
||||
if (!p) {
|
||||
LOG_WARN("Failed to allocate zeroed memory");
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -39,10 +41,12 @@ template <class T> class Allocator
|
||||
T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY)
|
||||
{
|
||||
T *p = alloc(maxWait);
|
||||
assert(p);
|
||||
if (!p) {
|
||||
LOG_WARN("Failed to allocate memory for copy");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (p)
|
||||
*p = src;
|
||||
*p = src;
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -83,7 +87,11 @@ template <class T> class MemoryDynamic : public Allocator<T>
|
||||
/// Return a buffer for use by others
|
||||
virtual void release(T *p) override
|
||||
{
|
||||
assert(p);
|
||||
if (p == nullptr)
|
||||
return;
|
||||
|
||||
LOG_HEAP("Freeing 0x%x", p);
|
||||
|
||||
free(p);
|
||||
}
|
||||
|
||||
@@ -96,3 +104,58 @@ template <class T> class MemoryDynamic : public Allocator<T>
|
||||
return p;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A static memory pool that uses a fixed buffer instead of heap allocation
|
||||
*/
|
||||
template <class T, int MaxSize> class MemoryPool : public Allocator<T>
|
||||
{
|
||||
private:
|
||||
T pool[MaxSize];
|
||||
bool used[MaxSize];
|
||||
|
||||
public:
|
||||
MemoryPool() : pool{}, used{}
|
||||
{
|
||||
// Arrays are now zero-initialized by member initializer list
|
||||
// pool array: all elements are default-constructed (zero for POD types)
|
||||
// used array: all elements are false (zero-initialized)
|
||||
}
|
||||
|
||||
/// Return a buffer for use by others
|
||||
virtual void release(T *p) override
|
||||
{
|
||||
if (!p) {
|
||||
LOG_DEBUG("Failed to release memory, pointer is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the index of this pointer in our pool
|
||||
int index = p - pool;
|
||||
if (index >= 0 && index < MaxSize) {
|
||||
assert(used[index]); // Should be marked as used
|
||||
used[index] = false;
|
||||
LOG_HEAP("Released static pool item %d at 0x%x", index, p);
|
||||
} else {
|
||||
LOG_WARN("Pointer 0x%x not from our pool!", p);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// Alloc some storage from our static pool
|
||||
virtual T *alloc(TickType_t maxWait) override
|
||||
{
|
||||
// Find first free slot
|
||||
for (int i = 0; i < MaxSize; i++) {
|
||||
if (!used[i]) {
|
||||
used[i] = true;
|
||||
LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]);
|
||||
return &pool[i];
|
||||
}
|
||||
}
|
||||
|
||||
// No free slots available - return nullptr instead of asserting
|
||||
LOG_WARN("No free slots available in static memory pool!");
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,7 +100,6 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src)
|
||||
// Was this message directed to us specifically? Will be false if we are sniffing someone elses packets
|
||||
auto ourNodeNum = nodeDB->getNodeNum();
|
||||
bool toUs = isBroadcast(mp.to) || isToUs(&mp);
|
||||
bool fromUs = mp.from == ourNodeNum;
|
||||
|
||||
for (auto i = modules->begin(); i != modules->end(); ++i) {
|
||||
auto &pi = **i;
|
||||
|
||||
@@ -46,11 +46,14 @@ the new node can build its node db)
|
||||
|
||||
MeshService *service;
|
||||
|
||||
static MemoryDynamic<meshtastic_MqttClientProxyMessage> staticMqttClientProxyMessagePool;
|
||||
#define MAX_MQTT_PROXY_MESSAGES 16
|
||||
static MemoryPool<meshtastic_MqttClientProxyMessage, MAX_MQTT_PROXY_MESSAGES> staticMqttClientProxyMessagePool;
|
||||
|
||||
static MemoryDynamic<meshtastic_QueueStatus> staticQueueStatusPool;
|
||||
#define MAX_QUEUE_STATUS 4
|
||||
static MemoryPool<meshtastic_QueueStatus, MAX_QUEUE_STATUS> staticQueueStatusPool;
|
||||
|
||||
static MemoryDynamic<meshtastic_ClientNotification> staticClientNotificationPool;
|
||||
#define MAX_CLIENT_NOTIFICATIONS 4
|
||||
static MemoryPool<meshtastic_ClientNotification, MAX_CLIENT_NOTIFICATIONS> staticClientNotificationPool;
|
||||
|
||||
Allocator<meshtastic_MqttClientProxyMessage> &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool;
|
||||
|
||||
@@ -61,8 +64,10 @@ Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
|
||||
#include "Router.h"
|
||||
|
||||
MeshService::MeshService()
|
||||
: toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE),
|
||||
toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2)
|
||||
#ifdef ARCH_PORTDUINO
|
||||
: toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE),
|
||||
toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE)
|
||||
#endif
|
||||
{
|
||||
lastQueueStatus = {0, 0, 16, 0};
|
||||
}
|
||||
@@ -191,8 +196,10 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
|
||||
// (so we update our nodedb for the local node)
|
||||
|
||||
// Send the packet into the mesh
|
||||
|
||||
sendToMesh(packetPool.allocCopy(p), RX_SRC_USER);
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(p);
|
||||
DEBUG_HEAP_AFTER("MeshService::handleToRadio", a);
|
||||
sendToMesh(a, RX_SRC_USER);
|
||||
|
||||
bool loopback = false; // if true send any packet the phone sends back itself (for testing)
|
||||
if (loopback) {
|
||||
@@ -248,7 +255,11 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
|
||||
}
|
||||
|
||||
if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent
|
||||
sendToPhone(packetPool.allocCopy(*p));
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto a = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("MeshService::sendToMesh", a);
|
||||
|
||||
sendToPhone(a);
|
||||
}
|
||||
|
||||
// Router may ask us to release the packet if it wasn't sent
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
#include "MeshRadio.h"
|
||||
#include "MeshTypes.h"
|
||||
#include "Observer.h"
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "PointerQueue.h"
|
||||
#else
|
||||
#include "StaticPointerQueue.h"
|
||||
#endif
|
||||
#include "mesh-pb-constants.h"
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
#include "../platform/portduino/SimRadio.h"
|
||||
#endif
|
||||
@@ -37,16 +42,32 @@ class MeshService
|
||||
/// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure
|
||||
/// we never hang because android hasn't been there in a while
|
||||
/// FIXME - save this to flash on deep sleep
|
||||
#ifdef ARCH_PORTDUINO
|
||||
PointerQueue<meshtastic_MeshPacket> toPhoneQueue;
|
||||
#else
|
||||
StaticPointerQueue<meshtastic_MeshPacket, MAX_RX_TOPHONE> toPhoneQueue;
|
||||
#endif
|
||||
|
||||
// keep list of QueueStatus packets to be send to the phone
|
||||
#ifdef ARCH_PORTDUINO
|
||||
PointerQueue<meshtastic_QueueStatus> toPhoneQueueStatusQueue;
|
||||
#else
|
||||
StaticPointerQueue<meshtastic_QueueStatus, MAX_RX_QUEUESTATUS_TOPHONE> toPhoneQueueStatusQueue;
|
||||
#endif
|
||||
|
||||
// keep list of MqttClientProxyMessages to be send to the client for delivery
|
||||
#ifdef ARCH_PORTDUINO
|
||||
PointerQueue<meshtastic_MqttClientProxyMessage> toPhoneMqttProxyQueue;
|
||||
#else
|
||||
StaticPointerQueue<meshtastic_MqttClientProxyMessage, MAX_RX_MQTTPROXY_TOPHONE> toPhoneMqttProxyQueue;
|
||||
#endif
|
||||
|
||||
// keep list of ClientNotifications to be send to the client (phone)
|
||||
#ifdef ARCH_PORTDUINO
|
||||
PointerQueue<meshtastic_ClientNotification> toPhoneClientNotificationQueue;
|
||||
#else
|
||||
StaticPointerQueue<meshtastic_ClientNotification, MAX_RX_NOTIFICATION_TOPHONE> toPhoneClientNotificationQueue;
|
||||
#endif
|
||||
|
||||
// This holds the last QueueStatus send
|
||||
meshtastic_QueueStatus lastQueueStatus;
|
||||
|
||||
@@ -175,12 +175,18 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
|
||||
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
|
||||
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it
|
||||
cancelSending(getFrom(p), p->id);
|
||||
// now free the pooled copy for retransmission too
|
||||
packetPool.release(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't
|
||||
// get scheduled again. (This is the core of stopRetransmission.)
|
||||
auto numErased = pending.erase(key);
|
||||
assert(numErased == 1);
|
||||
|
||||
// When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call
|
||||
// to startRetransmission.
|
||||
packetPool.release(p);
|
||||
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
|
||||
@@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
config.bluetooth.fixed_pin = defaultBLEPin;
|
||||
|
||||
#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
|
||||
defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS)
|
||||
defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306)
|
||||
bool hasScreen = true;
|
||||
#ifdef HELTEC_MESH_NODE_T114
|
||||
uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET);
|
||||
@@ -673,7 +673,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
|
||||
#endif
|
||||
#elif ARCH_PORTDUINO
|
||||
bool hasScreen = false;
|
||||
if (portduino_config.displayPanel)
|
||||
if (settingsMap[displayPanel])
|
||||
hasScreen = true;
|
||||
else
|
||||
hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C;
|
||||
@@ -775,9 +775,7 @@ void NodeDB::installDefaultModuleConfig()
|
||||
|
||||
moduleConfig.version = DEVICESTATE_CUR_VER;
|
||||
moduleConfig.has_mqtt = true;
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST
|
||||
moduleConfig.has_range_test = true;
|
||||
#endif
|
||||
moduleConfig.has_serial = true;
|
||||
moduleConfig.has_store_forward = true;
|
||||
moduleConfig.has_telemetry = true;
|
||||
@@ -843,12 +841,6 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
|
||||
#endif
|
||||
moduleConfig.has_canned_message = true;
|
||||
#if !MESHTASTIC_EXCLUDE_AUDIO
|
||||
moduleConfig.has_audio = true;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
moduleConfig.has_paxcounter = true;
|
||||
#endif
|
||||
#if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT
|
||||
moduleConfig.mqtt.enabled = true;
|
||||
#endif
|
||||
@@ -891,14 +883,12 @@ void NodeDB::installDefaultModuleConfig()
|
||||
moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
|
||||
moduleConfig.detection_sensor.minimum_broadcast_secs = 45;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING
|
||||
moduleConfig.has_ambient_lighting = true;
|
||||
moduleConfig.ambient_lighting.current = 10;
|
||||
// Default to a color based on our node number
|
||||
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
||||
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
||||
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
||||
#endif
|
||||
|
||||
initModuleConfigIntervals();
|
||||
}
|
||||
@@ -1344,8 +1334,8 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
#if ARCH_PORTDUINO
|
||||
// set any config overrides
|
||||
if (portduino_config.has_configDisplayMode) {
|
||||
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
|
||||
if (settingsMap[has_configDisplayMode]) {
|
||||
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)settingsMap[configDisplayMode];
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1438,25 +1428,15 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
moduleConfig.has_canned_message = true;
|
||||
moduleConfig.has_external_notification = true;
|
||||
moduleConfig.has_mqtt = true;
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST
|
||||
moduleConfig.has_range_test = true;
|
||||
#endif
|
||||
moduleConfig.has_serial = true;
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
moduleConfig.has_store_forward = true;
|
||||
#endif
|
||||
moduleConfig.has_telemetry = true;
|
||||
moduleConfig.has_neighbor_info = true;
|
||||
moduleConfig.has_detection_sensor = true;
|
||||
#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING
|
||||
moduleConfig.has_ambient_lighting = true;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_AUDIO
|
||||
moduleConfig.has_audio = true;
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
moduleConfig.has_paxcounter = true;
|
||||
#endif
|
||||
|
||||
success &=
|
||||
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
||||
|
||||
@@ -34,21 +34,6 @@
|
||||
// Flag to indicate a heartbeat was received and we should send queue status
|
||||
bool heartbeatReceived = false;
|
||||
|
||||
// Helper function to skip excluded module configs and advance state
|
||||
size_t PhoneAPI::skipExcludedModuleConfig(uint8_t *buf)
|
||||
{
|
||||
config_state++;
|
||||
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_CONFIG) {
|
||||
state = STATE_SEND_FILEMANIFEST;
|
||||
} else {
|
||||
state = STATE_SEND_OTHER_NODEINFOS;
|
||||
}
|
||||
config_state = 0;
|
||||
}
|
||||
return getFromRadio(buf);
|
||||
}
|
||||
|
||||
PhoneAPI::PhoneAPI()
|
||||
{
|
||||
lastContactMsec = millis();
|
||||
@@ -115,6 +100,7 @@ void PhoneAPI::close()
|
||||
config_nonce = 0;
|
||||
config_state = 0;
|
||||
pauseBluetoothLogging = false;
|
||||
heartbeatReceived = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,35 +355,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.payload_variant.serial = moduleConfig.serial;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_external_notification_tag:
|
||||
#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION)
|
||||
LOG_DEBUG("Send module config: ext notification");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.external_notification = moduleConfig.external_notification;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("External Notification module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
case meshtastic_ModuleConfig_store_forward_tag:
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
LOG_DEBUG("Send module config: store forward");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.store_forward = moduleConfig.store_forward;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("Store & Forward module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
case meshtastic_ModuleConfig_range_test_tag:
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST
|
||||
LOG_DEBUG("Send module config: range test");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.range_test = moduleConfig.range_test;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("Range Test module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
case meshtastic_ModuleConfig_telemetry_tag:
|
||||
LOG_DEBUG("Send module config: telemetry");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag;
|
||||
@@ -409,15 +380,10 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.payload_variant.canned_message = moduleConfig.canned_message;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_audio_tag:
|
||||
#if !MESHTASTIC_EXCLUDE_AUDIO
|
||||
LOG_DEBUG("Send module config: audio");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.audio = moduleConfig.audio;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("Audio module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
case meshtastic_ModuleConfig_remote_hardware_tag:
|
||||
LOG_DEBUG("Send module config: remote hardware");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag;
|
||||
@@ -434,25 +400,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_ambient_lighting_tag:
|
||||
#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING
|
||||
LOG_DEBUG("Send module config: ambient lighting");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("Ambient Lighting module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
case meshtastic_ModuleConfig_paxcounter_tag:
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
LOG_DEBUG("Send module config: paxcounter");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||
break;
|
||||
#else
|
||||
LOG_DEBUG("Paxcounter module excluded from build, skipping");
|
||||
return skipExcludedModuleConfig(buf);
|
||||
#endif
|
||||
default:
|
||||
LOG_ERROR("Unknown module config type %d", config_state);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,4 @@ class PhoneAPI
|
||||
|
||||
/// If the mesh service tells us fromNum has changed, tell the phone
|
||||
virtual int onNotify(uint32_t newValue) override;
|
||||
|
||||
/// Helper function to skip excluded module configs and advance state
|
||||
size_t skipExcludedModuleConfig(uint8_t *buf);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#define RF95_MAX_POWER portduino_config.rf95_max_power
|
||||
#define RF95_MAX_POWER settingsMap[rf95_max_power]
|
||||
#endif
|
||||
#ifndef RF95_MAX_POWER
|
||||
#define RF95_MAX_POWER 20
|
||||
@@ -94,16 +94,16 @@ void RF95Interface::setTransmitEnable(bool txon)
|
||||
#ifdef RF95_TXEN
|
||||
digitalWrite(RF95_TXEN, txon ? 1 : 0);
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, txon ? 1 : 0);
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[txen_pin], txon ? 1 : 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef RF95_RXEN
|
||||
digitalWrite(RF95_RXEN, txon ? 0 : 1);
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, txon ? 0 : 1);
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[rxen_pin], txon ? 0 : 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -164,13 +164,13 @@ bool RF95Interface::init()
|
||||
digitalWrite(RF95_RXEN, 1);
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
pinMode(portduino_config.lora_txen_pin.pin, OUTPUT);
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, 0);
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
pinMode(settingsMap[txen_pin], OUTPUT);
|
||||
digitalWrite(settingsMap[txen_pin], 0);
|
||||
}
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT);
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, 0);
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
pinMode(settingsMap[rxen_pin], OUTPUT);
|
||||
digitalWrite(settingsMap[rxen_pin], 0);
|
||||
}
|
||||
#endif
|
||||
setTransmitEnable(false);
|
||||
|
||||
@@ -32,12 +32,9 @@ const RegionInfo regions[] = {
|
||||
RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false),
|
||||
|
||||
/*
|
||||
EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21]
|
||||
|
||||
https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf
|
||||
FIXME: https://github.com/meshtastic/firmware/issues/3371
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
*/
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false),
|
||||
RDEF(EU_433, 433.0f, 434.0f, 10, 0, 12, true, false, false),
|
||||
|
||||
/*
|
||||
https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/
|
||||
|
||||
@@ -417,13 +417,12 @@ void RadioLibInterface::handleReceiveInterrupt()
|
||||
|
||||
int state = iface->readData((uint8_t *)&radioBuffer, length);
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.logoutputlevel == level_trace) {
|
||||
if (settingsMap[logoutputlevel] == level_trace) {
|
||||
printBytes("Raw incoming packet: ", (uint8_t *)&radioBuffer, length);
|
||||
}
|
||||
#endif
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state,
|
||||
radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags);
|
||||
LOG_ERROR("Ignore received packet due to error=%d", state);
|
||||
rxBad++;
|
||||
|
||||
airTime->logAirtime(RX_ALL_LOG, xmitMsec);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Default.h"
|
||||
#include "MeshTypes.h"
|
||||
#include "configuration.h"
|
||||
#include "memGet.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "modules/RoutingModule.h"
|
||||
@@ -21,8 +22,10 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
|
||||
if (p->hop_limit == 0) {
|
||||
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
}
|
||||
|
||||
DEBUG_HEAP_BEFORE;
|
||||
auto copy = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
|
||||
|
||||
startRetransmission(copy, NUM_RELIABLE_RETX);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "detect/LoRaRadioType.h"
|
||||
#include "main.h"
|
||||
@@ -27,14 +28,24 @@
|
||||
|
||||
// I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX
|
||||
// And every TX packet might have a retransmission packet or an ack alive at any moment
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes
|
||||
#define MAX_PACKETS \
|
||||
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
|
||||
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
|
||||
|
||||
// static MemoryPool<MeshPacket> staticPool(MAX_PACKETS);
|
||||
static MemoryDynamic<meshtastic_MeshPacket> staticPool;
|
||||
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool;
|
||||
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
|
||||
#else
|
||||
// Embedded targets use static memory pools with compile-time constants
|
||||
#define MAX_PACKETS_STATIC \
|
||||
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
|
||||
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
|
||||
|
||||
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool;
|
||||
Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
|
||||
#endif
|
||||
|
||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
|
||||
|
||||
@@ -275,7 +286,10 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
|
||||
// If the packet is not yet encrypted, do so now
|
||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
|
||||
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("Router::send", p_decoded);
|
||||
|
||||
auto encodeResult = perhapsEncode(p);
|
||||
if (encodeResult != meshtastic_Routing_Error_NONE) {
|
||||
@@ -446,7 +460,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
#if ENABLE_JSON_LOGGING
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) {
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerialize(p, false).c_str());
|
||||
}
|
||||
#endif
|
||||
@@ -607,8 +621,11 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
bool skipHandle = false;
|
||||
// Also, we should set the time from the ISR and it should have msec level resolution
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
|
||||
// Store a copy of encrypted packet for MQTT
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
||||
|
||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||
auto decodedState = perhapsDecode(p);
|
||||
@@ -656,7 +673,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
|
||||
// call modules here
|
||||
// If this could be a spoofed packet, don't let the modules see it.
|
||||
if (!skipHandle && p->from != nodeDB->getNodeNum()) {
|
||||
if (!skipHandle) {
|
||||
MeshModule::callModules(*p, src);
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_MQTT
|
||||
@@ -670,8 +687,6 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
!isFromUs(p) && mqtt)
|
||||
mqtt->onSend(*p_encrypted, *p, p->channel);
|
||||
#endif
|
||||
} else if (p->from == nodeDB->getNodeNum() && !skipHandle) {
|
||||
MeshModule::callModules(*p, src);
|
||||
}
|
||||
|
||||
packetPool.release(p_encrypted); // Release the encrypted packet
|
||||
@@ -685,7 +700,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||
#elif ARCH_PORTDUINO
|
||||
// Even ignored packets get logged in the trace
|
||||
if (portduino_config.traceFilename != "" || portduino_config.logoutputlevel == level_trace) {
|
||||
if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) {
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
LOG_TRACE("%s", MeshPacketSerializer::JsonSerializeEncrypted(p).c_str());
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// Particular boards might define a different max power based on what their hardware can do, default to max power output if not
|
||||
// specified (may be dangerous if using external PA and SX126x power config forgotten)
|
||||
#if ARCH_PORTDUINO
|
||||
#define SX126X_MAX_POWER portduino_config.sx126x_max_power
|
||||
#define SX126X_MAX_POWER settingsMap[sx126x_max_power]
|
||||
#endif
|
||||
#ifndef SX126X_MAX_POWER
|
||||
#define SX126X_MAX_POWER 22
|
||||
@@ -53,10 +53,10 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
|
||||
if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_sx126x_ant_sw_pin.pin, HIGH);
|
||||
pinMode(portduino_config.lora_sx126x_ant_sw_pin.pin, OUTPUT);
|
||||
tcxoVoltage = (float)settingsMap[dio3_tcxo_voltage] / 1000;
|
||||
if (settingsMap[sx126x_ant_sw_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[sx126x_ant_sw_pin], HIGH);
|
||||
pinMode(settingsMap[sx126x_ant_sw_pin], OUTPUT);
|
||||
}
|
||||
#endif
|
||||
if (tcxoVoltage == 0.0)
|
||||
@@ -98,7 +98,7 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
bool dio2AsRfSwitch = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
bool dio2AsRfSwitch = false;
|
||||
if (portduino_config.dio2_as_rf_switch) {
|
||||
if (settingsMap[dio2_as_rf_switch]) {
|
||||
dio2AsRfSwitch = true;
|
||||
}
|
||||
#else
|
||||
@@ -112,9 +112,9 @@ template <typename T> bool SX126xInterface<T>::init()
|
||||
// no effect
|
||||
#if ARCH_PORTDUINO
|
||||
if (res == RADIOLIB_ERR_NONE) {
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", portduino_config.lora_rxen_pin.pin,
|
||||
portduino_config.lora_txen_pin.pin);
|
||||
lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin);
|
||||
LOG_DEBUG("Use MCU pin %i as RXEN and pin %i as TXEN to control RF switching", settingsMap[rxen_pin],
|
||||
settingsMap[txen_pin]);
|
||||
lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]);
|
||||
}
|
||||
#else
|
||||
#ifndef SX126X_RXEN
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
// Particular boards might define a different max power based on what their hardware can do
|
||||
#if ARCH_PORTDUINO
|
||||
#define SX128X_MAX_POWER portduino_config.sx128x_max_power
|
||||
#define SX128X_MAX_POWER settingsMap[sx128x_max_power]
|
||||
#endif
|
||||
#ifndef SX128X_MAX_POWER
|
||||
#define SX128X_MAX_POWER 13
|
||||
@@ -41,13 +41,13 @@ template <typename T> bool SX128xInterface<T>::init()
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
pinMode(portduino_config.lora_rxen_pin.pin, OUTPUT);
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, LOW); // Set low before becoming an output
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
pinMode(settingsMap[rxen_pin], OUTPUT);
|
||||
digitalWrite(settingsMap[rxen_pin], LOW); // Set low before becoming an output
|
||||
}
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
pinMode(portduino_config.lora_txen_pin.pin, OUTPUT);
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, LOW); // Set low before becoming an output
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
pinMode(settingsMap[txen_pin], OUTPUT);
|
||||
digitalWrite(settingsMap[txen_pin], LOW); // Set low before becoming an output
|
||||
}
|
||||
#else
|
||||
#if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // set not rx or tx mode
|
||||
@@ -93,9 +93,8 @@ template <typename T> bool SX128xInterface<T>::init()
|
||||
lora.setRfSwitchPins(SX128X_RXEN, SX128X_TXEN);
|
||||
}
|
||||
#elif ARCH_PORTDUINO
|
||||
if (res == RADIOLIB_ERR_NONE && portduino_config.lora_rxen_pin.pin != RADIOLIB_NC &&
|
||||
portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
lora.setRfSwitchPins(portduino_config.lora_rxen_pin.pin, portduino_config.lora_txen_pin.pin);
|
||||
if (res == RADIOLIB_ERR_NONE && settingsMap[rxen_pin] != RADIOLIB_NC && settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
lora.setRfSwitchPins(settingsMap[rxen_pin], settingsMap[txen_pin]);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -175,11 +174,11 @@ template <typename T> void SX128xInterface<T>::setStandby()
|
||||
LOG_ERROR("SX128x standby %s%d", radioLibErr, err);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, LOW);
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[rxen_pin], LOW);
|
||||
}
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, LOW);
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[txen_pin], LOW);
|
||||
}
|
||||
#else
|
||||
#if defined(SX128X_RXEN) && (SX128X_RXEN != RADIOLIB_NC) // we have RXEN/TXEN control - turn off RX and TX power
|
||||
@@ -211,11 +210,11 @@ template <typename T> void SX128xInterface<T>::addReceiveMetadata(meshtastic_Mes
|
||||
template <typename T> void SX128xInterface<T>::configHardwareForSend()
|
||||
{
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, HIGH);
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[txen_pin], HIGH);
|
||||
}
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, LOW);
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[rxen_pin], LOW);
|
||||
}
|
||||
|
||||
#else
|
||||
@@ -242,11 +241,11 @@ template <typename T> void SX128xInterface<T>::startReceive()
|
||||
setStandby();
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_rxen_pin.pin, HIGH);
|
||||
if (settingsMap[rxen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[rxen_pin], HIGH);
|
||||
}
|
||||
if (portduino_config.lora_txen_pin.pin != RADIOLIB_NC) {
|
||||
digitalWrite(portduino_config.lora_txen_pin.pin, LOW);
|
||||
if (settingsMap[txen_pin] != RADIOLIB_NC) {
|
||||
digitalWrite(settingsMap[txen_pin], LOW);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
77
src/mesh/StaticPointerQueue.h
Normal file
77
src/mesh/StaticPointerQueue.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "freertosinc.h"
|
||||
#include <cassert>
|
||||
|
||||
/**
|
||||
* A static circular buffer queue for pointers.
|
||||
* This provides the same interface as PointerQueue but uses a statically allocated
|
||||
* buffer instead of dynamic allocation.
|
||||
*/
|
||||
template <class T, int MaxElements> class StaticPointerQueue
|
||||
{
|
||||
static_assert(MaxElements > 0, "MaxElements must be greater than 0");
|
||||
|
||||
T *buffer[MaxElements];
|
||||
int head = 0;
|
||||
int tail = 0;
|
||||
int count = 0;
|
||||
concurrency::OSThread *reader = nullptr;
|
||||
|
||||
public:
|
||||
StaticPointerQueue()
|
||||
{
|
||||
// Initialize all buffer elements to nullptr to silence warnings and ensure clean state
|
||||
for (int i = 0; i < MaxElements; i++) {
|
||||
buffer[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int numFree() const { return MaxElements - count; }
|
||||
|
||||
bool isEmpty() const { return count == 0; }
|
||||
|
||||
int numUsed() const { return count; }
|
||||
|
||||
bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY)
|
||||
{
|
||||
if (count >= MaxElements) {
|
||||
return false; // Queue is full
|
||||
}
|
||||
|
||||
if (reader) {
|
||||
reader->setInterval(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
}
|
||||
|
||||
buffer[tail] = x;
|
||||
tail = (tail + 1) % MaxElements;
|
||||
count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY)
|
||||
{
|
||||
if (count == 0) {
|
||||
return false; // Queue is empty
|
||||
}
|
||||
|
||||
*p = buffer[head];
|
||||
head = (head + 1) % MaxElements;
|
||||
count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// returns a ptr or null if the queue was empty
|
||||
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY)
|
||||
{
|
||||
T *p;
|
||||
return dequeue(&p, maxWait) ? p : nullptr;
|
||||
}
|
||||
|
||||
void setReader(concurrency::OSThread *t) { reader = t; }
|
||||
|
||||
// For compatibility with PointerQueue interface
|
||||
int getMaxLen() const { return MaxElements; }
|
||||
};
|
||||
@@ -66,6 +66,8 @@ typedef enum _meshtastic_Language {
|
||||
meshtastic_Language_UKRAINIAN = 16,
|
||||
/* Bulgarian */
|
||||
meshtastic_Language_BULGARIAN = 17,
|
||||
/* Czech */
|
||||
meshtastic_Language_CZECH = 18,
|
||||
/* Simplified Chinese (experimental) */
|
||||
meshtastic_Language_SIMPLIFIED_CHINESE = 30,
|
||||
/* Traditional Chinese (experimental) */
|
||||
|
||||
@@ -259,8 +259,8 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_T_DECK_PRO = 102,
|
||||
/* Lilygo TLora Pager */
|
||||
meshtastic_HardwareModel_T_LORA_PAGER = 103,
|
||||
/* GAT562 Mesh Trial Tracker */
|
||||
meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104,
|
||||
/* M5Stack Reserved */
|
||||
meshtastic_HardwareModel_M5STACK_RESERVED = 104, /* 0x68 */
|
||||
/* RAKwireless WisMesh Tag */
|
||||
meshtastic_HardwareModel_WISMESH_TAG = 105,
|
||||
/* RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ */
|
||||
@@ -274,6 +274,8 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_T_ECHO_LITE = 109,
|
||||
/* New Heltec LoRA32 with ESP32-S3 CPU */
|
||||
meshtastic_HardwareModel_HELTEC_V4 = 110,
|
||||
/* M5Stack C6L */
|
||||
meshtastic_HardwareModel_M5STACK_C6L = 111,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
|
||||
@@ -292,11 +292,14 @@ JSONArray htmlListDir(const char *dirname, uint8_t levels)
|
||||
JSONObject thisFileMap;
|
||||
thisFileMap["size"] = new JSONValue((int)file.size());
|
||||
#ifdef ARCH_ESP32
|
||||
thisFileMap["name"] = new JSONValue(String(file.path()).substring(1).c_str());
|
||||
String fileName = String(file.path()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#else
|
||||
thisFileMap["name"] = new JSONValue(String(file.name()).substring(1).c_str());
|
||||
String fileName = String(file.name()).substring(1);
|
||||
thisFileMap["name"] = new JSONValue(fileName.c_str());
|
||||
#endif
|
||||
if (String(file.name()).substring(1).endsWith(".gz")) {
|
||||
String tempName = String(file.name()).substring(1);
|
||||
if (tempName.endsWith(".gz")) {
|
||||
#ifdef ARCH_ESP32
|
||||
String modifiedFile = String(file.path()).substring(1);
|
||||
#else
|
||||
@@ -339,7 +342,8 @@ void handleFsBrowseStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
|
||||
delete value;
|
||||
|
||||
@@ -367,7 +371,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
} else {
|
||||
@@ -376,7 +381,8 @@ void handleFsDeleteStatic(HTTPRequest *req, HTTPResponse *res)
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("Error");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
return;
|
||||
}
|
||||
@@ -622,10 +628,7 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
|
||||
tempArray.push_back(new JSONValue((int)logArray[i]));
|
||||
}
|
||||
JSONValue *result = new JSONValue(tempArray);
|
||||
// Clean up original array to prevent memory leak
|
||||
for (auto *val : tempArray) {
|
||||
delete val;
|
||||
}
|
||||
// Note: Don't delete tempArray elements here - JSONValue now owns them
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -656,7 +659,9 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
|
||||
// data->wifi
|
||||
JSONObject jsonObjWifi;
|
||||
jsonObjWifi["rssi"] = new JSONValue(WiFi.RSSI());
|
||||
jsonObjWifi["ip"] = new JSONValue(WiFi.localIP().toString().c_str());
|
||||
String wifiIPString = WiFi.localIP().toString();
|
||||
std::string wifiIP = wifiIPString.c_str();
|
||||
jsonObjWifi["ip"] = new JSONValue(wifiIP.c_str());
|
||||
|
||||
// data->memory
|
||||
JSONObject jsonObjMemory;
|
||||
@@ -702,7 +707,8 @@ void handleReport(HTTPRequest *req, HTTPResponse *res)
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
}
|
||||
|
||||
@@ -773,7 +779,8 @@ void handleNodes(HTTPRequest *req, HTTPResponse *res)
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
|
||||
// Clean up the nodesArray to prevent memory leak
|
||||
@@ -926,7 +933,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res)
|
||||
JSONObject jsonObjOuter;
|
||||
jsonObjOuter["status"] = new JSONValue("ok");
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
}
|
||||
|
||||
@@ -968,7 +976,8 @@ void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
|
||||
|
||||
// serialize and write it to the stream
|
||||
JSONValue *value = new JSONValue(jsonObjOuter);
|
||||
res->print(value->Stringify().c_str());
|
||||
std::string jsonString = value->Stringify();
|
||||
res->print(jsonString.c_str());
|
||||
delete value;
|
||||
|
||||
// Clean up the networkObjs to prevent memory leak
|
||||
|
||||
@@ -15,8 +15,27 @@
|
||||
// FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in
|
||||
// RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0]))
|
||||
#ifndef MAX_RX_TOPHONE
|
||||
#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
|
||||
#define MAX_RX_TOPHONE 8
|
||||
#else
|
||||
#define MAX_RX_TOPHONE 32
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// max number of QueueStatus packets which can be waiting for delivery to phone
|
||||
#ifndef MAX_RX_QUEUESTATUS_TOPHONE
|
||||
#define MAX_RX_QUEUESTATUS_TOPHONE 2
|
||||
#endif
|
||||
|
||||
/// max number of MqttClientProxyMessage packets which can be waiting for delivery to phone
|
||||
#ifndef MAX_RX_MQTTPROXY_TOPHONE
|
||||
#define MAX_RX_MQTTPROXY_TOPHONE 8
|
||||
#endif
|
||||
|
||||
/// max number of ClientNotification packets which can be waiting for delivery to phone
|
||||
#ifndef MAX_RX_NOTIFICATION_TOPHONE
|
||||
#define MAX_RX_NOTIFICATION_TOPHONE 2
|
||||
#endif
|
||||
|
||||
/// Verify baseline assumption of node size. If it increases, we need to reevaluate
|
||||
/// the impact of its memory footprint, notably on MAX_NUM_NODES.
|
||||
|
||||
@@ -65,8 +65,8 @@ mail: marchammermann@googlemail.com
|
||||
#define DEFAULT_REALM "default_realm"
|
||||
#define PREFIX ""
|
||||
|
||||
#define KEY_PATH portduino_config.webserver_ssl_key_path.c_str()
|
||||
#define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str()
|
||||
#define KEY_PATH settingsStrings[websslkeypath].c_str()
|
||||
#define CERT_PATH settingsStrings[websslcertpath].c_str()
|
||||
|
||||
struct _file_config configWeb;
|
||||
|
||||
@@ -458,8 +458,8 @@ PiWebServerThread::PiWebServerThread()
|
||||
}
|
||||
}
|
||||
|
||||
if (portduino_config.webserverport != 0) {
|
||||
webservport = portduino_config.webserverport;
|
||||
if (settingsMap[webserverport] != 0) {
|
||||
webservport = settingsMap[webserverport];
|
||||
LOG_INFO("Use webserver port from yaml config %i ", webservport);
|
||||
} else {
|
||||
LOG_INFO("Webserver port in yaml config set to 0, defaulting to port 9443");
|
||||
@@ -490,7 +490,7 @@ PiWebServerThread::PiWebServerThread()
|
||||
u_map_put(&configWeb.mime_types, ".ico", "image/x-icon");
|
||||
u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml");
|
||||
|
||||
webrootpath = portduino_config.webserver_root_path;
|
||||
webrootpath = settingsStrings[webserverrootpath];
|
||||
|
||||
configWeb.files_path = (char *)webrootpath.c_str();
|
||||
configWeb.url_prefix = "";
|
||||
|
||||
@@ -1040,32 +1040,19 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
|
||||
LOG_INFO("Get module config: External Notification");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
||||
res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
|
||||
#else
|
||||
LOG_DEBUG("External Notification module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
LOG_INFO("Get module config: Store & Forward");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
||||
res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
|
||||
#else
|
||||
LOG_DEBUG("Store & Forward module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST
|
||||
LOG_INFO("Get module config: Range Test");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
||||
res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
|
||||
#else
|
||||
LOG_DEBUG("Range Test module excluded from build, skipping config");
|
||||
// Don't set payload variant - will result in empty response
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG:
|
||||
LOG_INFO("Get module config: Telemetry");
|
||||
@@ -1078,13 +1065,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_AUDIO
|
||||
LOG_INFO("Get module config: Audio");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
||||
res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
|
||||
#else
|
||||
LOG_DEBUG("Audio module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG:
|
||||
LOG_INFO("Get module config: Remote Hardware");
|
||||
@@ -1097,31 +1080,19 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const
|
||||
res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG:
|
||||
#if !(NO_EXT_GPIO || MESHTASTIC_EXCLUDE_DETECTIONSENSOR)
|
||||
LOG_INFO("Get module config: Detection Sensor");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
|
||||
res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
||||
#else
|
||||
LOG_DEBUG("Detection Sensor module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_AMBIENTLIGHTING
|
||||
LOG_INFO("Get module config: Ambient Lighting");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
||||
res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
||||
#else
|
||||
LOG_DEBUG("Ambient Lighting module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG:
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
LOG_INFO("Get module config: Paxcounter");
|
||||
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
||||
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
||||
#else
|
||||
LOG_DEBUG("Paxcounter module excluded from build, skipping config");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,12 @@
|
||||
#include "detect/ScanI2C.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/NotificationRenderer.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h" // for cardkb_found
|
||||
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
@@ -42,7 +38,6 @@
|
||||
|
||||
extern ScanI2C::DeviceAddress cardkb_found;
|
||||
extern bool graphics::isMuted;
|
||||
extern bool osk_found;
|
||||
|
||||
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
||||
static NodeNum lastDest = NODENUM_BROADCAST;
|
||||
@@ -156,13 +151,10 @@ int CannedMessageModule::splitConfiguredMessages()
|
||||
int tempCount = 0;
|
||||
// Insert at position 0 (top)
|
||||
tempMessages[tempCount++] = "[Select Destination]";
|
||||
|
||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||
// Add a "Free Text" entry at the top if using a touch screen virtual keyboard
|
||||
// Add a "Free Text" entry at the top if using a keyboard
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
#else
|
||||
if (osk_found && screen) {
|
||||
tempMessages[tempCount++] = "[-- Free Text --]";
|
||||
}
|
||||
#endif
|
||||
|
||||
// First message always starts at buffer start
|
||||
@@ -349,8 +341,6 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
||||
case CANNED_MESSAGE_RUN_STATE_FREETEXT:
|
||||
return handleFreeTextInput(event); // All allowed input for this state
|
||||
|
||||
// Virtual keyboard mode: Show virtual keyboard and handle input
|
||||
|
||||
// If sending, block all input except global/system (handled above)
|
||||
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
|
||||
return 1;
|
||||
@@ -637,57 +627,6 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo
|
||||
notifyObservers(&e);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (strcmp(current, "[-- Free Text --]") == 0) {
|
||||
if (osk_found && screen) {
|
||||
char headerBuffer[64];
|
||||
if (this->dest == NODENUM_BROADCAST) {
|
||||
snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel));
|
||||
} else {
|
||||
snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest));
|
||||
}
|
||||
screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) {
|
||||
if (!text.empty()) {
|
||||
this->freetext = text.c_str();
|
||||
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||
runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
currentMessageIndex = -1;
|
||||
|
||||
// IMPORTANT: Don't delete virtual keyboard here - it's still executing!
|
||||
// Just clear the callback to prevent further input
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
|
||||
// Schedule keyboard cleanup for next run cycle
|
||||
// This allows the current submitText() method to complete safely
|
||||
setIntervalFromNow(500);
|
||||
return;
|
||||
} else {
|
||||
// Don't delete virtual keyboard immediately - it might still be executing
|
||||
// Instead, just clear the callback and reset banner to stop input processing
|
||||
graphics::NotificationRenderer::textInputCallback = nullptr;
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
|
||||
// Return to inactive state
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
|
||||
// Force display update to show normal screen
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->notifyObservers(&e);
|
||||
screen->forceDisplay();
|
||||
|
||||
// Schedule cleanup for next loop iteration to ensure safe deletion
|
||||
setIntervalFromNow(50);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Normal canned message selection
|
||||
@@ -1004,52 +943,12 @@ int32_t CannedMessageModule::runOnce()
|
||||
|
||||
// Normal module disable/idle handling
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) {
|
||||
// Clean up virtual keyboard if needed when going inactive, but only if virtual keyboard is not actively being used
|
||||
if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr &&
|
||||
graphics::NotificationRenderer::current_notification_type != graphics::notificationTypeEnum::text_input) {
|
||||
LOG_INFO("Performing delayed virtual keyboard cleanup");
|
||||
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||
}
|
||||
|
||||
temporaryMessage = "";
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
// Handle delayed virtual keyboard message sending
|
||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
// Virtual keyboard message sending case - text was not empty
|
||||
if (this->freetext.length() > 0) {
|
||||
LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str());
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
|
||||
// Clean up virtual keyboard after sending
|
||||
if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||
LOG_INFO("Cleaning up virtual keyboard after message send");
|
||||
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
// Clear payload to indicate virtual keyboard processing is complete
|
||||
// But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds
|
||||
this->payload = 0;
|
||||
} else {
|
||||
// Empty message, just go inactive
|
||||
LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
return 2000;
|
||||
}
|
||||
|
||||
UIFrameEvent e;
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 &&
|
||||
this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) ||
|
||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) ||
|
||||
(this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
@@ -1059,18 +958,6 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
// Handle SENDING_ACTIVE state transition after virtual keyboard message
|
||||
else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) {
|
||||
// This happens after virtual keyboard message sending is complete
|
||||
LOG_INFO("Virtual keyboard message sending completed, returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
temporaryMessage = "";
|
||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||
this->currentMessageIndex = -1;
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->notifyObservers(&e);
|
||||
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
|
||||
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
|
||||
// Reset module on inactivity
|
||||
@@ -1079,24 +966,9 @@ int32_t CannedMessageModule::runOnce()
|
||||
this->freetext = "";
|
||||
this->cursor = 0;
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
|
||||
// Clean up virtual keyboard if it exists during timeout, but only if it's not actively being used by another process
|
||||
if (graphics::NotificationRenderer::virtualKeyboard &&
|
||||
graphics::NotificationRenderer::current_notification_type == graphics::notificationTypeEnum::text_input) {
|
||||
LOG_INFO("Virtual keyboard is active - not cleaning up due to CannedMessage timeout");
|
||||
} else if (graphics::NotificationRenderer::virtualKeyboard) {
|
||||
LOG_INFO("Cleaning up virtual keyboard due to module timeout");
|
||||
graphics::OnScreenKeyboardModule::instance().stop(false);
|
||||
graphics::NotificationRenderer::resetBanner();
|
||||
}
|
||||
|
||||
this->notifyObservers(&e);
|
||||
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) {
|
||||
if (this->payload == 0) {
|
||||
// [Exit] button pressed - return to inactive state
|
||||
LOG_INFO("Processing [Exit] action - returning to inactive state");
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||
} else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||
if (this->freetext.length() > 0) {
|
||||
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
||||
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
||||
@@ -1560,10 +1432,17 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O
|
||||
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
||||
if (node) {
|
||||
if (node->is_favorite) {
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(entryText, sizeof(entryText), "* %s", node->user.short_name);
|
||||
} else {
|
||||
snprintf(entryText, sizeof(entryText), "%s", node->user.short_name);
|
||||
}
|
||||
#else
|
||||
snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name);
|
||||
} else {
|
||||
snprintf(entryText, sizeof(entryText), "%s", node->user.long_name);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1734,7 +1613,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int yOffset = y + 10;
|
||||
#else
|
||||
display->setFont(FONT_MEDIUM);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int yOffset = y;
|
||||
#else
|
||||
int yOffset = y + 10;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// --- Delivery Status Message ---
|
||||
@@ -1759,13 +1642,20 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
}
|
||||
|
||||
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
yOffset += lineCount * FONT_HEIGHT_MEDIUM - 5; // only 1 line gap, no extra padding
|
||||
#else
|
||||
yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding
|
||||
|
||||
#endif
|
||||
#ifndef USE_EINK
|
||||
// --- SNR + RSSI Compact Line ---
|
||||
if (this->ack) {
|
||||
display->setFont(FONT_SMALL);
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
snprintf(buffer, sizeof(buffer), "SNR: %.1f dB \nRSSI: %d", this->lastRxSnr, this->lastRxRssi);
|
||||
#else
|
||||
snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi);
|
||||
#endif
|
||||
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
#include "input/RotaryEncoderImpl.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "input/i2cButton.h"
|
||||
#include "modules/SystemCommandsModule.h"
|
||||
#if HAS_TRACKBALL
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#endif
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_I2C
|
||||
#include "input/cardKbI2cImpl.h"
|
||||
#endif
|
||||
@@ -135,13 +139,20 @@ void setupModules()
|
||||
traceRouteModule = new TraceRouteModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_NEIGHBORINFO
|
||||
neighborInfoModule = new NeighborInfoModule();
|
||||
if (moduleConfig.has_neighbor_info && moduleConfig.neighbor_info.enabled) {
|
||||
neighborInfoModule = new NeighborInfoModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_DETECTIONSENSOR
|
||||
detectionSensorModule = new DetectionSensorModule();
|
||||
if (moduleConfig.has_detection_sensor && moduleConfig.detection_sensor.enabled) {
|
||||
detectionSensorModule = new DetectionSensorModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_ATAK
|
||||
atakPluginModule = new AtakPluginModule();
|
||||
if (IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TAK,
|
||||
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER)) {
|
||||
atakPluginModule = new AtakPluginModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
keyVerificationModule = new KeyVerificationModule();
|
||||
@@ -163,7 +174,7 @@ void setupModules()
|
||||
#endif
|
||||
// Example: Put your module here
|
||||
// new ReplyModule();
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER && !MESHTASTIC_EXCLUDE_I2C
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1();
|
||||
if (!rotaryEncoderInterruptImpl1->init()) {
|
||||
@@ -186,6 +197,9 @@ void setupModules()
|
||||
#endif
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
i2cButton = new i2cButtonThread("i2cButtonThread");
|
||||
#endif
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
kbMatrixImpl = new KbMatrixImpl();
|
||||
kbMatrixImpl->init();
|
||||
@@ -207,7 +221,7 @@ void setupModules()
|
||||
aLinuxInputImpl->init();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
trackballInterruptImpl1 = new TrackballInterruptImpl1();
|
||||
trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS);
|
||||
@@ -227,11 +241,14 @@ void setupModules()
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
#endif
|
||||
// TODO: How to improve this?
|
||||
#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
new EnvironmentTelemetryModule();
|
||||
if (moduleConfig.has_telemetry &&
|
||||
(moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) {
|
||||
new EnvironmentTelemetryModule();
|
||||
}
|
||||
#if __has_include("Adafruit_PM25AQI.h")
|
||||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
|
||||
if (moduleConfig.has_telemetry && moduleConfig.telemetry.air_quality_enabled &&
|
||||
nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_PMSA003I].first > 0) {
|
||||
new AirQualityTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
@@ -243,12 +260,16 @@ void setupModules()
|
||||
#endif
|
||||
#endif
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
new PowerTelemetryModule();
|
||||
if (moduleConfig.has_telemetry &&
|
||||
(moduleConfig.telemetry.power_measurement_enabled || moduleConfig.telemetry.power_screen_enabled)) {
|
||||
new PowerTelemetryModule();
|
||||
}
|
||||
#endif
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \
|
||||
!defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
#if !MESHTASTIC_EXCLUDE_SERIAL
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
if (moduleConfig.has_serial && moduleConfig.serial.enabled &&
|
||||
config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||||
new SerialModule();
|
||||
}
|
||||
#endif
|
||||
@@ -259,19 +280,26 @@ void setupModules()
|
||||
audioModule = new AudioModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
paxcounterModule = new PaxcounterModule();
|
||||
if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
|
||||
paxcounterModule = new PaxcounterModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO)
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
storeForwardModule = new StoreForwardModule();
|
||||
if (moduleConfig.has_store_forward && moduleConfig.store_forward.enabled) {
|
||||
storeForwardModule = new StoreForwardModule();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION
|
||||
externalNotificationModule = new ExternalNotificationModule();
|
||||
if (moduleConfig.has_external_notification && moduleConfig.external_notification.enabled) {
|
||||
externalNotificationModule = new ExternalNotificationModule();
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS
|
||||
new RangeTestModule();
|
||||
if (moduleConfig.has_range_test && moduleConfig.range_test.enabled)
|
||||
new RangeTestModule();
|
||||
#endif
|
||||
} else {
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
|
||||
@@ -12,12 +12,12 @@ NodeInfoModule *nodeInfoModule;
|
||||
|
||||
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
|
||||
{
|
||||
auto p = *pptr;
|
||||
|
||||
if (mp.from == nodeDB->getNodeNum()) {
|
||||
LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto p = *pptr;
|
||||
if (p.is_licensed != owner.is_licensed) {
|
||||
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
|
||||
return true;
|
||||
@@ -44,7 +44,10 @@ void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t cha
|
||||
if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
|
||||
service->cancelSending(prevPacketId);
|
||||
shorterTimeout = _shorterTimeout;
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p = allocReply();
|
||||
DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p);
|
||||
|
||||
if (p) { // Check whether we didn't ignore it
|
||||
p->to = dest;
|
||||
p->decoded.want_response = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/draw/NotificationRenderer.h" // drawInvertedNotificationBox signature reuse
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
#include "modules/OnScreenKeyboardModule.h"
|
||||
#include <Arduino.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
OnScreenKeyboardModule &OnScreenKeyboardModule::instance()
|
||||
{
|
||||
static OnScreenKeyboardModule inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
OnScreenKeyboardModule::~OnScreenKeyboardModule()
|
||||
{
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t,
|
||||
std::function<void(const std::string &)> cb)
|
||||
{
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
keyboard = new VirtualKeyboard();
|
||||
callback = cb;
|
||||
if (header)
|
||||
keyboard->setHeader(header);
|
||||
if (initialText)
|
||||
keyboard->setInputText(initialText);
|
||||
|
||||
// Route VK submission/cancel events back into the module
|
||||
keyboard->setCallback([this](const std::string &text) {
|
||||
if (text.empty()) {
|
||||
this->onCancel();
|
||||
} else {
|
||||
this->onSubmit(text);
|
||||
}
|
||||
});
|
||||
|
||||
// Maintain legacy compatibility hooks
|
||||
NotificationRenderer::virtualKeyboard = keyboard;
|
||||
NotificationRenderer::textInputCallback = callback;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::stop(bool callEmptyCallback)
|
||||
{
|
||||
auto cb = callback;
|
||||
callback = nullptr;
|
||||
if (keyboard) {
|
||||
delete keyboard;
|
||||
keyboard = nullptr;
|
||||
}
|
||||
// Keep NotificationRenderer legacy pointers in sync
|
||||
NotificationRenderer::virtualKeyboard = nullptr;
|
||||
NotificationRenderer::textInputCallback = nullptr;
|
||||
clearPopup();
|
||||
if (callEmptyCallback && cb)
|
||||
cb("");
|
||||
}
|
||||
|
||||
bool OnScreenKeyboardModule::isActive() const
|
||||
{
|
||||
return keyboard != nullptr;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::handleInput(const InputEvent &event)
|
||||
{
|
||||
if (!keyboard)
|
||||
return;
|
||||
|
||||
// Auto-timeout check handled in draw() to centralize state transitions.
|
||||
switch (event.inputEvent) {
|
||||
case INPUT_BROKER_UP: {
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1)
|
||||
keyboard->moveCursorLeft();
|
||||
else
|
||||
keyboard->moveCursorUp();
|
||||
break;
|
||||
}
|
||||
case INPUT_BROKER_DOWN: {
|
||||
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1)
|
||||
keyboard->moveCursorRight();
|
||||
else
|
||||
keyboard->moveCursorDown();
|
||||
break;
|
||||
}
|
||||
case INPUT_BROKER_LEFT:
|
||||
keyboard->moveCursorLeft();
|
||||
break;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
keyboard->moveCursorRight();
|
||||
break;
|
||||
case INPUT_BROKER_UP_LONG:
|
||||
keyboard->moveCursorUp();
|
||||
break;
|
||||
case INPUT_BROKER_DOWN_LONG:
|
||||
keyboard->moveCursorDown();
|
||||
break;
|
||||
case INPUT_BROKER_ALT_PRESS:
|
||||
keyboard->moveCursorLeft();
|
||||
break;
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
keyboard->moveCursorRight();
|
||||
break;
|
||||
case INPUT_BROKER_SELECT:
|
||||
keyboard->handlePress();
|
||||
break;
|
||||
case INPUT_BROKER_SELECT_LONG:
|
||||
keyboard->handleLongPress();
|
||||
break;
|
||||
case INPUT_BROKER_CANCEL:
|
||||
onCancel();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool OnScreenKeyboardModule::draw(OLEDDisplay *display)
|
||||
{
|
||||
if (!keyboard)
|
||||
return false;
|
||||
|
||||
// Timeout
|
||||
if (keyboard->isTimedOut()) {
|
||||
onCancel();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear full screen behind keyboard
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, display->getWidth(), display->getHeight());
|
||||
display->setColor(WHITE);
|
||||
keyboard->draw(display, 0, 0);
|
||||
|
||||
// Draw popup overlay if needed
|
||||
drawPopup(display);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::onSubmit(const std::string &text)
|
||||
{
|
||||
auto cb = callback;
|
||||
stop(false);
|
||||
if (cb)
|
||||
cb(text);
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::onCancel()
|
||||
{
|
||||
stop(true);
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs)
|
||||
{
|
||||
if (!title || !content)
|
||||
return;
|
||||
strncpy(popupTitle, title, sizeof(popupTitle) - 1);
|
||||
popupTitle[sizeof(popupTitle) - 1] = '\0';
|
||||
strncpy(popupMessage, content, sizeof(popupMessage) - 1);
|
||||
popupMessage[sizeof(popupMessage) - 1] = '\0';
|
||||
popupUntil = millis() + durationMs;
|
||||
popupVisible = true;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::clearPopup()
|
||||
{
|
||||
popupTitle[0] = '\0';
|
||||
popupMessage[0] = '\0';
|
||||
popupUntil = 0;
|
||||
popupVisible = false;
|
||||
}
|
||||
|
||||
void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display)
|
||||
{
|
||||
if (!popupVisible)
|
||||
return;
|
||||
if (millis() > popupUntil || popupMessage[0] == '\0') {
|
||||
popupVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build lines and leverage NotificationRenderer inverted box drawing for consistent style
|
||||
constexpr uint16_t maxContentLines = 3;
|
||||
const bool hasTitle = popupTitle[0] != '\0';
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
const uint16_t maxWrapWidth = display->width() - 40;
|
||||
|
||||
auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector<std::string> {
|
||||
std::vector<std::string> wrapped;
|
||||
std::string current;
|
||||
std::string word;
|
||||
const char *p = text;
|
||||
while (*p && wrapped.size() < maxContentLines) {
|
||||
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
|
||||
if (*p == '\n') {
|
||||
if (!current.empty()) {
|
||||
wrapped.push_back(current);
|
||||
current.clear();
|
||||
if (wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
}
|
||||
}
|
||||
++p;
|
||||
}
|
||||
if (!*p || wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
word.clear();
|
||||
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r')
|
||||
word += *p++;
|
||||
if (word.empty())
|
||||
continue;
|
||||
std::string test = current.empty() ? word : (current + " " + word);
|
||||
uint16_t w = display->getStringWidth(test.c_str(), test.length(), true);
|
||||
if (w <= availableWidth)
|
||||
current = test;
|
||||
else {
|
||||
if (!current.empty()) {
|
||||
wrapped.push_back(current);
|
||||
current = word;
|
||||
if (wrapped.size() >= maxContentLines)
|
||||
break;
|
||||
} else {
|
||||
current = word;
|
||||
while (current.size() > 1 &&
|
||||
display->getStringWidth(current.c_str(), current.length(), true) > availableWidth)
|
||||
current.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!current.empty() && wrapped.size() < maxContentLines)
|
||||
wrapped.push_back(current);
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
std::vector<std::string> allLines;
|
||||
if (hasTitle)
|
||||
allLines.emplace_back(popupTitle);
|
||||
|
||||
char buf[sizeof(popupMessage)];
|
||||
strncpy(buf, popupMessage, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
char *paragraph = strtok(buf, "\n");
|
||||
while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) {
|
||||
auto wrapped = wrapText(paragraph, maxWrapWidth);
|
||||
for (const auto &ln : wrapped) {
|
||||
if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0))
|
||||
break;
|
||||
allLines.push_back(ln);
|
||||
}
|
||||
paragraph = strtok(nullptr, "\n");
|
||||
}
|
||||
|
||||
std::vector<const char *> ptrs;
|
||||
for (const auto &ln : allLines)
|
||||
ptrs.push_back(ln.c_str());
|
||||
ptrs.push_back(nullptr);
|
||||
|
||||
// Use the inverted notification box already present in NotificationRenderer
|
||||
NotificationRenderer::drawInvertedNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "graphics/Screen.h" // InputEvent
|
||||
#include "graphics/VirtualKeyboard.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
// Lightweight UI module to manage on-screen keyboard (non-touch).
|
||||
class OnScreenKeyboardModule
|
||||
{
|
||||
public:
|
||||
static OnScreenKeyboardModule &instance();
|
||||
|
||||
// Begin a keyboard session
|
||||
void start(const char *header, const char *initialText,
|
||||
uint32_t /*durationMs unused here - NotificationRenderer controls banner timeout*/,
|
||||
std::function<void(const std::string &)> callback);
|
||||
|
||||
// Stop current session (optionally call callback with empty string)
|
||||
void stop(bool callEmptyCallback);
|
||||
|
||||
// Session status
|
||||
bool isActive() const;
|
||||
|
||||
// Event handling + drawing
|
||||
void handleInput(const InputEvent &event);
|
||||
// Draw keyboard and any overlay popup; return false if session ended (timeout or submit/cancel)
|
||||
bool draw(OLEDDisplay *display);
|
||||
|
||||
// Popup helpers (title/content shown above keyboard)
|
||||
void showPopup(const char *title, const char *content, uint32_t durationMs);
|
||||
void clearPopup();
|
||||
|
||||
// Compatibility: expose underlying keyboard pointer for existing callsites
|
||||
VirtualKeyboard *getKeyboard() const { return keyboard; }
|
||||
|
||||
private:
|
||||
OnScreenKeyboardModule() = default;
|
||||
~OnScreenKeyboardModule();
|
||||
OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete;
|
||||
OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete;
|
||||
|
||||
// Internal helpers
|
||||
void onSubmit(const std::string &text);
|
||||
void onCancel();
|
||||
|
||||
// Popup rendering
|
||||
void drawPopup(OLEDDisplay *display);
|
||||
|
||||
VirtualKeyboard *keyboard = nullptr;
|
||||
std::function<void(const std::string &)> callback;
|
||||
|
||||
// Popup state
|
||||
char popupTitle[64] = {0};
|
||||
char popupMessage[256] = {0};
|
||||
uint32_t popupUntil = 0;
|
||||
bool popupVisible = false;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif // HAS_SCREEN
|
||||
@@ -172,7 +172,10 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
||||
telemetry.variant.device_metrics.battery_level, telemetry.variant.device_metrics.voltage,
|
||||
telemetry.variant.device_metrics.uptime_seconds);
|
||||
|
||||
DEBUG_HEAP_BEFORE;
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(telemetry);
|
||||
DEBUG_HEAP_AFTER("DeviceTelemetryModule::sendTelemetry", p);
|
||||
|
||||
p->to = dest;
|
||||
p->decoded.want_response = false;
|
||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert,
|
||||
bool show_date);
|
||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
|
||||
}
|
||||
#if __has_include(<Adafruit_AHTX0.h>)
|
||||
#include "Sensor/AHT10.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user